前言
Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。
StepDescription1引入Maven依赖2在Spring Boot中启用Swagger3创建SwaggerConfig类4创建Docket Bean5提供API信息6配置Swagger UI7应用Swagger
项目背景
版本
SpringBoot 2.7.*
springfox 3.0
Maven依赖
<!-- Swagger --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>3.0.0</version></dependency><!-- 或--><dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version></dependency>
@Configuration@EnableSwagger2@EnableAutoConfiguration@ConditionalOnProperty(name ="swagger.enabled", matchIfMissing =true)publicclassSwaggerAutoConfiguration{/**
* 默认的排除路径,排除Spring Boot默认的错误处理路径和端点
*/privatestaticfinalList<String> DEFAULT_EXCLUDE_PATH =Arrays.asList("/error","/actuator/**");privatestaticfinalString BASE_PATH ="/**";@Bean@ConditionalOnMissingBeanpublicSwaggerPropertiesswaggerProperties(){returnnewSwaggerProperties();}@BeanpublicDocketapi(SwaggerProperties swaggerProperties){// base-path处理if(swaggerProperties.getBasePath().isEmpty()){
swaggerProperties.getBasePath().add(BASE_PATH);}// noinspection uncheckedList<Predicate<String>> basePath =newArrayList<Predicate<String>>();
swaggerProperties.getBasePath().forEach(path -> basePath.add(PathSelectors.ant(path)));// exclude-path处理if(swaggerProperties.getExcludePath().isEmpty()){
swaggerProperties.getExcludePath().addAll(DEFAULT_EXCLUDE_PATH);}List<Predicate<String>> excludePath =newArrayList<>();
swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path)));ApiSelectorBuilder builder =newDocket(DocumentationType.SWAGGER_2).host(swaggerProperties.getHost()).apiInfo(apiInfo(swaggerProperties)).select().apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()));
swaggerProperties.getBasePath().forEach(p -> builder.paths(PathSelectors.ant(p)));
swaggerProperties.getExcludePath().forEach(p -> builder.paths(PathSelectors.ant(p).negate()));return builder.build().securitySchemes(securitySchemes()).securityContexts(securityContexts()).pathMapping("/");}/**
* 安全模式,这里指定token通过Authorization头请求头传递
*/privateList<SecurityScheme>securitySchemes(){List<SecurityScheme> apiKeyList =newArrayList<SecurityScheme>();
apiKeyList.add(newApiKey("Authorization","Authorization","header"));return apiKeyList;}/**
* 安全上下文
*/privateList<SecurityContext>securityContexts(){List<SecurityContext> securityContexts =newArrayList<>();
securityContexts.add(SecurityContext.builder().securityReferences(defaultAuth()).operationSelector(o -> o.requestMappingPattern().matches("/.*")).build());return securityContexts;}/**
* 默认的全局鉴权策略
*
* @return
*/privateList<SecurityReference>defaultAuth(){AuthorizationScope authorizationScope =newAuthorizationScope("global","accessEverything");AuthorizationScope[] authorizationScopes =newAuthorizationScope[1];
authorizationScopes[0]= authorizationScope;List<SecurityReference> securityReferences =newArrayList<>();
securityReferences.add(newSecurityReference("Authorization", authorizationScopes));return securityReferences;}privateApiInfoapiInfo(SwaggerProperties swaggerProperties){returnnewApiInfoBuilder().title(swaggerProperties.getTitle()).description(swaggerProperties.getDescription()).license(swaggerProperties.getLicense()).licenseUrl(swaggerProperties.getLicenseUrl()).termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl()).contact(newContact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), swaggerProperties.getContact().getEmail())).version(swaggerProperties.getVersion()).build();}}
@Component@ConfigurationProperties("swagger")publicclassSwaggerProperties{/**
* 是否开启swagger
*/privateBoolean enabled;/**
* swagger会解析的包路径
**/privateString basePackage ="";/**
* swagger会解析的url规则
**/privateList<String> basePath =newArrayList<>();/**
* 在basePath基础上需要排除的url规则
**/privateList<String> excludePath =newArrayList<>();/**
* 标题
**/privateString title ="";/**
* 描述
**/privateString description ="";/**
* 版本
**/privateString version ="";/**
* 许可证
**/privateString license ="";/**
* 许可证URL
**/privateString licenseUrl ="";/**
* 服务条款URL
**/privateString termsOfServiceUrl ="";/**
* host信息
**/privateString host ="";/**
* 联系人信息
*/privateContact contact =newContact();/**
* 全局统一鉴权配置
**/privateAuthorization authorization =newAuthorization();publicBooleangetEnabled(){return enabled;}publicvoidsetEnabled(Boolean enabled){this.enabled = enabled;}publicStringgetBasePackage(){return basePackage;}publicvoidsetBasePackage(String basePackage){this.basePackage = basePackage;}publicList<String>getBasePath(){return basePath;}publicvoidsetBasePath(List<String> basePath){this.basePath = basePath;}publicList<String>getExcludePath(){return excludePath;}publicvoidsetExcludePath(List<String> excludePath){this.excludePath = excludePath;}publicStringgetTitle(){return title;}publicvoidsetTitle(String title){this.title = title;}publicStringgetDescription(){return description;}publicvoidsetDescription(String description){this.description = description;}publicStringgetVersion(){return version;}publicvoidsetVersion(String version){this.version = version;}publicStringgetLicense(){return license;}publicvoidsetLicense(String license){this.license = license;}publicStringgetLicenseUrl(){return licenseUrl;}publicvoidsetLicenseUrl(String licenseUrl){this.licenseUrl = licenseUrl;}publicStringgetTermsOfServiceUrl(){return termsOfServiceUrl;}publicvoidsetTermsOfServiceUrl(String termsOfServiceUrl){this.termsOfServiceUrl = termsOfServiceUrl;}publicStringgetHost(){return host;}publicvoidsetHost(String host){this.host = host;}publicContactgetContact(){return contact;}publicvoidsetContact(Contact contact){this.contact = contact;}publicAuthorizationgetAuthorization(){return authorization;}publicvoidsetAuthorization(Authorization authorization){this.authorization = authorization;}publicstaticclassContact{/**
* 联系人
**/privateString name ="";/**
* 联系人url
**/privateString url ="";/**
* 联系人email
**/privateString email ="";publicStringgetName(){return name;}publicvoidsetName(String name){this.name = name;}publicStringgetUrl(){return url;}publicvoidsetUrl(String url){this.url = url;}publicStringgetEmail(){return email;}publicvoidsetEmail(String email){this.email = email;}}publicstaticclassAuthorization{/**
* 鉴权策略ID,需要和SecurityReferences ID保持一致
*/privateString name ="";/**
* 需要开启鉴权URL的正则
*/privateString authRegex ="^.*$";/**
* 鉴权作用域列表
*/privateList<AuthorizationScope> authorizationScopeList =newArrayList<>();privateList<String> tokenUrlList =newArrayList<>();publicStringgetName(){return name;}publicvoidsetName(String name){this.name = name;}publicStringgetAuthRegex(){return authRegex;}publicvoidsetAuthRegex(String authRegex){this.authRegex = authRegex;}publicList<AuthorizationScope>getAuthorizationScopeList(){return authorizationScopeList;}publicvoidsetAuthorizationScopeList(List<AuthorizationScope> authorizationScopeList){this.authorizationScopeList = authorizationScopeList;}publicList<String>getTokenUrlList(){return tokenUrlList;}publicvoidsetTokenUrlList(List<String> tokenUrlList){this.tokenUrlList = tokenUrlList;}}publicstaticclassAuthorizationScope{/**
* 作用域名称
*/privateString scope ="";/**
* 作用域描述
*/privateString description ="";publicStringgetScope(){return scope;}publicvoidsetScope(String scope){this.scope = scope;}publicStringgetDescription(){return description;}publicvoidsetDescription(String description){this.description = description;}}
/**
* swagger 资源映射路径
*
*/@ConfigurationpublicclassSwaggerWebConfigurationimplementsWebMvcConfigurer{@OverridepublicvoidaddResourceHandlers(ResourceHandlerRegistry registry){/** swagger-ui 地址 */
registry.addResourceHandler("/swagger-ui/**").addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");}}
报错一:: Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerException
org.springframework.context.ApplicationContextException:Failedtostart bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181)
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54)
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356)
at 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 *Application.main(Application.java:22)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethod)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)Caused by:java.lang.NullPointerException: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$3(Orderings.java:89)
at java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:469)
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
at java.util.TimSort.sort(TimSort.java:234)
at java.util.Arrays.sort(Arrays.java:1512)
at java.util.ArrayList.sort(ArrayList.java:1454)
at java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:387)
at java.util.stream.Sink$ChainedReference.end(Sink.java:258)
at java.util.stream.Sink$ChainedReference.end(Sink.java:258)
at java.util.stream.Sink$ChainedReference.end(Sink.java:258)
at java.util.stream.Sink$ChainedReference.end(Sink.java:258)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:81)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
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)...19 common frames omitted
从报错顺序跟踪源码查看执行步骤
step操作1SpringBoot 启动同时Bean初始化完毕2初始化DocumentationPluginsBootstrapper同时将RequestHandlerProvider通过构造器驻入3调用DocumentationPluginsBootstrapper start 方法加载Docket插件4解析RequestHandlerProvider并将数据存到DocumentationCache中5请求Swagger2Controller 的v2/api-docs接口,通过groupName从DocumentationCache中将数据取出,在将数据统一封装到Swagger类中,序列化成json返回给
报错二:No operations defined in spec!
解决
SpringBoot 2.6.0开始,请求路径与SpringMVC处理映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser。可以通过设置spring.mvc.pathmatch.matching-strategy为ant-path-matcher来改变。
除了basePackage包路径配错以外。以下方案可解决以上问题。
/**
* swagger 在 springboot 2.6.x 不兼容问题的处理
*
*/@ComponentpublicclassSwaggerBeanPostProcessorimplementsBeanPostProcessor{@OverridepublicObjectpostProcessAfterInitialization(Object bean,String beanName)throwsBeansException{if(bean instanceofWebMvcRequestHandlerProvider|| bean instanceofWebFluxRequestHandlerProvider){customizeSpringfoxHandlerMappings(getHandlerMappings(bean));}return bean;}private<TextendsRequestMappingInfoHandlerMapping>voidcustomizeSpringfoxHandlerMappings(List<T> mappings){List<T> copy = mappings.stream().filter(mapping -> mapping.getPatternParser()==null).collect(Collectors.toList());
mappings.clear();
mappings.addAll(copy);}@SuppressWarnings("unchecked")privateList<RequestMappingInfoHandlerMapping>getHandlerMappings(Object bean){try{Field field =ReflectionUtils.findField(bean.getClass(),"handlerMappings");
field.setAccessible(true);return(List<RequestMappingInfoHandlerMapping>) field.get(bean);}catch(IllegalArgumentException|IllegalAccessException e){thrownewIllegalStateException(e);}}}
配置:
spring:mvc:pathmatch:matching-strategy: ant_path_matcher
心如欲壑,后土难填。
版权归原作者 三省同学 所有, 如有侵权,请联系我们删除。