Swagger2(基于openApi3)已经在17年停止维护了,取而代之的是 sagger3(基于openApi3),而国内几乎没有 sagger3使用的文档,百度搜出来的大部分都是swagger2的使用,这篇文章将介绍如何在 java 中使用 openApi3(swagger3)。
Open API
OpenApi是业界真正的 api 文档标准,其是由 Swagger 来维护的,并被linux列为api标准,从而成为行业标准。
swagger 是一个 api 文档维护组织,后来成为了 Open API 标准的主要定义者,现在最新的版本为17年发布的 Swagger3(Open Api3)。国内绝大部分人还在用过时的swagger2(17年停止维护并更名为swagger3)swagger2的包名为 io.swagger,而swagger3的包名为 io.swagger.core.v3。
SpringFox是 spring 社区维护的一个项目(非官方),帮助使用者将 swagger2 集成到 Spring 中。常常用于 Spring 中帮助开发者生成文档,并可以轻松的在spring boot中使用。目前已经支持 OpenAPI3 标准。
升级到 OpenAPI3(java 中 swagger1.x 对应 OpenAPI2、swagger 2.x对应OpenAPI3)官方文档
@Api:用在请求的类上,表示对类的说明 tags: 说明该类的作用,可以在UI界面上看到的注解 value: 该参数没什么意义,在UI界面上也看到,所以不需要配置 @ApiOperation:用在请求的方法上,说明方法的用途、作用 value: 说明方法的用途、作用 notes: 方法的备注说明 @ApiImplicitParams:用在请求的方法上,表示一组参数说明 @ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面 name:参数名 value:参数的汉字说明、解释 required:参数是否必须传 paramType:参数放在哪个地方 · header --> 请求参数的获取:@RequestHeader · query --> 请求参数的获取:@RequestParam · path(用于restful接口)--> 请求参数的获取:@PathVariable · body(不常用) · form(不常用) dataType:参数类型,默认String,其它值dataType="Integer" defaultValue:参数的默认值 @ApiResponses:用在请求的方法上,表示一组响应 @ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息 code:数字,例如400 message:信息,例如"请求参数没填好" response:抛出异常的类 @ApiModel:用于响应类上,表示一个返回响应数据的信息,一般用在post创建的时候,使用@RequestBody这样的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候) @ApiModelProperty:用在属性上,描述响应类的属性 name:属性名 value:属性的汉字说明、解释 @ApiParam 用于 Controller 中方法的参数说明,放在方法签名当中 value:参数说明 required:是否必填 @ApiIgnore:使用该注解忽略这个API
io.springfox springfox-boot-starter3.0.0 io.springfox springfox-swagger-ui3.0.0 com.github.xiaoymin knife4j-spring-boot-starter3.0.3
Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher。
# ======================================================================== # 启动报错需要修改以下mvc配置 Failed to start bean 'documentationPluginsBootstrapper' spring: mvc: pathmatch: matching-strategy: ant_path_matcher # ========================================================================
升级spring boot到2.7.0,项目启动报错,项目使用swagger 3.0 ,具体版本是knife4j-spring-boot-starter的3.0.3,查找解决方案,都说修改配置文件(但是修改配置后仍然无效)或者新增配置类,过滤空情况,但是仍然还是解决了很长时间。报错信息:Caused by: java.lang.NullPointerException: Cannot invoke “org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()” because “this.condition” is null
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2022-06-04 12:30:54.079 [main] ERROR org.springframework.boot.SpringApplication - Application run failed org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) at org.springframework.context.support.DefaultLifecycleProcessor.access0(DefaultLifecycleProcessor.java:54) at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) at java.base/java.lang.Iterable.forEach(Iterable.java:75) at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) at com.cloud.user.UserApplication.main(UserApplication.java:41) Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null at springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns(WebMvcPatternsRequestConditionWrapper.java:56) at springfox.documentation.RequestHandler.sortedPaths(RequestHandler.java:113) at springfox.documentation.spi.service.contexts.Orderings.lambda$byPatternsCondition(Orderings.java:89) at java.base/java.util.Comparator.lambda$comparinga9974f(Comparator.java:473) at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) at java.base/java.util.TimSort.sort(TimSort.java:234) at java.base/java.util.Arrays.sort(Arrays.java:1307) at java.base/java.util.ArrayList.sort(ArrayList.java:1721) at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:392) at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:81) at java.base/java.util.stream.ReferencePipeline.accept(ReferencePipeline.java:197) at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.withDefaults(AbstractDocumentationPluginsBootstrapper.java:107) at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.buildContext(AbstractDocumentationPluginsBootstrapper.java:91) at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.bootstrapDocumentationPlugins(AbstractDocumentationPluginsBootstrapper.java:82) at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:100) at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ... 14 common frames omitted
解决方案
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.ReflectionUtils; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider; import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider; import java.lang.reflect.Field; import java.util.List; import java.util.stream.Collectors; @Slf4j @Configuration public class BeanPostProcessorConfig { @Bean public BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { return new BeanPostProcessor() { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) { customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); } return bean; } privatevoid customizeSpringfoxHandlerMappings(List mappings) { List copy = mappings.stream() .filter(mapping -> mapping.getPatternParser() == null) .collect(Collectors.toList()); mappings.clear(); mappings.addAll(copy); } @SuppressWarnings("unchecked") private List getHandlerMappings(Object bean) { try { Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings"); field.setAccessible(true); return (List ) field.get(bean); } catch (IllegalArgumentException | IllegalAccessException e) { throw new IllegalStateException(e); } } }; } }
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.context.request.async.DeferredResult; import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.oas.annotations.EnableOpenApi; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; /** * 文档访问地址:http://ip:port/swagger-ui/index.html * 添加Knife4j可以导出导出离线文档,访问地址:http://ip:port/doc.html * */ @Configuration @EnableKnife4j @EnableOpenApi @ConditionalOnProperty(value = "spring.profiles.active", havingValue = "dev") //@Profile({"dev","test"}) public class SwaggerConfiguration { @Bean public Docket createRestApis() { return new Docket(DocumentationType.OAS_30) .enable(true)//是否启用:注意生产环境需要关闭 .groupName("spring-boot-2.7.3") .genericModelSubstitutes(DeferredResult.class) .useDefaultResponseMessages(false) .forCodeGeneration(true) .ignoredParameterTypes(CookieValue.class) .apiInfo(apiInfo()) .select() //以下拦截配置可以三选一,根据需要进行添加 .apis(RequestHandlerSelectors.basePackage("com.qi.study.springboot.controller")) .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("使用swagger生成的接口文档") .description("开发测试") // 服务条款URL .termsOfServiceUrl("https://www.baidu.com/") // 作者信息 .contact(new Contact("qihh", "https://www.baidu.com/", "qihh@136.com")) .version("0.0.1") .build(); } }
import com.kfang.web.price.manager.interceptor.AuthorizationInterceptor; import com.kfang.web.price.manager.interceptor.ContextInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class InterceptorConfiguration implements WebMvcConfigurer { @Resource private Environment env; @Resource private AuthorizationInterceptor authorizationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { String[] swaggerExcludePathPatterns = {"/doc.html","/swagger**/**","/swagger-resources/**","/webjars/**","/v3/**"}; registry.addInterceptor(new ContextInterceptor()).addPathPatterns("/**"); if("dev".equals(env.getProperty("spring.profiles.active"))){ }else{ swaggerExcludePathPatterns = new String[0]; } registry.addInterceptor(authorizationInterceptor) .excludePathPatterns("/user/logout") .excludePathPatterns("/user/login") .excludePathPatterns(swaggerExcludePathPatterns) .addPathPatterns("/**").order(-1); } }
@Api(tags = "用户",value = "用户") @RestController @RequestMapping(value = "/user", produces = {"application/json;charset=UTF-8"}) public class UserController { @ApiOperation("添加") @PostMapping("/add") @ApiResponses({ @ApiResponse(message = "添加", code = 200, response = UserVO.class) }) public UserVO add(@RequestBody @Valid UserAddRequest userAddRequest) { // 将数据写到数据库 UserVO userVO = new UserVO(); BeanUtils.copyProperties(userAddRequest, userVO); userVO.setId(1L); userVO.setCreateTime(LocalDateTime.now()); userVO.setUpdateTime(LocalDateTime.now()); return userVO; } @ApiOperation("修改") @PostMapping("/edit") @ApiResponses({ @ApiResponse(message = "修改", code = 200, response = UserVO.class) }) public UserVO edit(@RequestBody @Valid UserEditRequest userEditRequest) { // 修改数据库的数据 UserVO userVO = new UserVO(); BeanUtils.copyProperties(userEditRequest, userVO); userVO.setUpdateTime(LocalDateTime.now()); return userVO; } @ApiOperation("查找") @GetMapping("/find") @ApiResponses({ @ApiResponse(message = "查找", code = 200, response = UserQueryRequest.class) }) public Listfind(UserQueryRequest userQueryRequest) { return new ArrayList<>(); } @ApiOperation("删除") @PostMapping("/delete") public void delete(Long id) { // 将数据库数据删除 } @PostMapping(value = "/fileUpload") @ApiOperation(value = "文件上传") @ApiResponses({ @ApiResponse(message = "文件上传", code = 200, response = AliyunUploadResult.class) }) public String uploadFile(@RequestPart("file") MultipartFile file) { AliyunUploadRequest uploadRequest = new AliyunUploadRequest(file); AliyunUploadResult result = aliyunOss.fileUpload(uploadRequest); return successInfo(result); } @PostMapping(value = "/importDataDetail") @ApiOperation(value = "导入Excel源数据明细") @ApiResponses({ @ApiResponse(message = "导入Excel源数据明细出参成功", code = 200, response = Boolean.class) }) public String importDataDetail(@RequestPart("file") MultipartFile file, @RequestParam("cityId") String cityId) { } }
1、访问swagger-ui页面:http://localhost:8080/swagger-ui/index.html
2、访问knife4j-ui页面:http://localhost:8080/doc.html
knife4j是springfox-swagger的增强UI实现,为Java开发者在使用Swagger的时候,提供了简洁、强大的接口文档体验。引入pom包,并开启@EnableKnife4j注解就可以使用。
server.port=8081 knife4j.enable=true knife4j.basic.enable=true knife4j.basic.username=admin knife4j.basic.password=admin knife4j.setting.language=zh-CN
登录界面:
当请求方法的请求参数类型不是String 或 MultipartFile / Part时,而是复杂的请求域时,@RequestParam 依赖Converter or PropertyEditor进行数据解析, RequestPart参考 ‘Content-Type’ header,依赖HttpMessageConverters 进行数据解析
当请求为multipart/form-data时,@RequestParam只能接收String类型的name-value值,@RequestPart可以接收复杂的请求域(像json、xml);@RequestParam 依赖Converter or PropertyEditor进行数据解析, @RequestPart参考'Content-Type' header,依赖HttpMessageConverters进行数据解析
前台请求:
jsonData为Person对象的json字符串
uploadFile为上传的图片
后台接收:
@RequestMapping("jsonDataAndUploadFile") @ResponseBody public String jsonDataAndUploadFile(@RequestPart("uploadFile") MultiPartFile uploadFile, @RequestPart("jsonData") Person person) { StringBuilder sb = new StringBuilder(); sb.append(uploadFile.getOriginalFilename()).append(";;;")); return person.toString() + ":::" + sb.toString(); }
@RequestMapping("jsonDataAndUploadFile") @ResponseBody public String jsonDataAndUploadFile(@RequestPart("uploadFile") MultiPartFile uploadFile, @RequestParam("josnData") String jsonData) { StringBuilder sb = new StringBuilder(); sb.append(uploadFile.getOriginalFilename()).append(";;;")); return person.toString() + ":::" + sb.toString(); }