前言
Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。
Step | Description |
---|---|
1 | 引入Maven依赖 |
2 | 在Spring Boot中启用Swagger |
3 | 创建SwaggerConfig类 |
4 | 创建Docket Bean |
5 | 提供API信息 |
6 | 配置Swagger UI |
7 | 应用Swagger |
项目背景
版本
SpringBoot 2.7.*
springfox 3.0
Maven依赖
<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)public class SwaggerAutoConfiguration{ /** * 默认的排除路径,排除Spring Boot默认的错误处理路径和端点 */ private static final List<String> DEFAULT_EXCLUDE_PATH = Arrays.asList("/error", "/actuator/**"); private static final String BASE_PATH = "/**"; @Bean @ConditionalOnMissingBean public SwaggerProperties swaggerProperties() { return new SwaggerProperties(); } @Bean public Docket api(SwaggerProperties swaggerProperties) { // base-path处理 if (swaggerProperties.getBasePath().isEmpty()) { swaggerProperties.getBasePath().add(BASE_PATH); } // noinspection unchecked List<Predicate<String>> basePath = new ArrayList<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 = new ArrayList<>(); swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path))); ApiSelectorBuilder builder = new Docket(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头请求头传递 */ private List<SecurityScheme> securitySchemes() { List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>(); apiKeyList.add(new ApiKey("Authorization", "Authorization", "header")); return apiKeyList; } /** * 安全上下文 */ private List<SecurityContext> securityContexts() { List<SecurityContext> securityContexts = new ArrayList<>(); securityContexts.add( SecurityContext.builder() .securityReferences(defaultAuth()) .operationSelector(o -> o.requestMappingPattern().matches("/.*")) .build()); return securityContexts; } /** * 默认的全局鉴权策略 * * @return */ private List<SecurityReference> defaultAuth() { AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; authorizationScopes[0] = authorizationScope; List<SecurityReference> securityReferences = new ArrayList<>(); securityReferences.add(new SecurityReference("Authorization", authorizationScopes)); return securityReferences; } private ApiInfo apiInfo(SwaggerProperties swaggerProperties) { return new ApiInfoBuilder() .title(swaggerProperties.getTitle()) .description(swaggerProperties.getDescription()) .license(swaggerProperties.getLicense()) .licenseUrl(swaggerProperties.getLicenseUrl()) .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl()) .contact(new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), swaggerProperties.getContact().getEmail())) .version(swaggerProperties.getVersion()) .build(); }}
@Component@ConfigurationProperties("swagger")public class SwaggerProperties{ /** * 是否开启swagger */ private Boolean enabled; /** * swagger会解析的包路径 **/ private String basePackage = ""; /** * swagger会解析的url规则 **/ private List<String> basePath = new ArrayList<>(); /** * 在basePath基础上需要排除的url规则 **/ private List<String> excludePath = new ArrayList<>(); /** * 标题 **/ private String title = ""; /** * 描述 **/ private String description = ""; /** * 版本 **/ private String version = ""; /** * 许可证 **/ private String license = ""; /** * 许可证URL **/ private String licenseUrl = ""; /** * 服务条款URL **/ private String termsOfServiceUrl = ""; /** * host信息 **/ private String host = ""; /** * 联系人信息 */ private Contact contact = new Contact(); /** * 全局统一鉴权配置 **/ private Authorization authorization = new Authorization(); public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public String getBasePackage() { return basePackage; } public void setBasePackage(String basePackage) { this.basePackage = basePackage; } public List<String> getBasePath() { return basePath; } public void setBasePath(List<String> basePath) { this.basePath = basePath; } public List<String> getExcludePath() { return excludePath; } public void setExcludePath(List<String> excludePath) { this.excludePath = excludePath; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getLicense() { return license; } public void setLicense(String license) { this.license = license; } public String getLicenseUrl() { return licenseUrl; } public void setLicenseUrl(String licenseUrl) { this.licenseUrl = licenseUrl; } public String getTermsOfServiceUrl() { return termsOfServiceUrl; } public void setTermsOfServiceUrl(String termsOfServiceUrl) { this.termsOfServiceUrl = termsOfServiceUrl; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public Contact getContact() { return contact; } public void setContact(Contact contact) { this.contact = contact; } public Authorization getAuthorization() { return authorization; } public void setAuthorization(Authorization authorization) { this.authorization = authorization; } public static class Contact { /** * 联系人 **/ private String name = ""; /** * 联系人url **/ private String url = ""; /** * 联系人email **/ private String email = ""; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } public static class Authorization { /** * 鉴权策略ID,需要和SecurityReferences ID保持一致 */ private String name = ""; /** * 需要开启鉴权URL的正则 */ private String authRegex = "^.*$"; /** * 鉴权作用域列表 */ private List<AuthorizationScope> authorizationScopeList = new ArrayList<>(); private List<String> tokenUrlList = new ArrayList<>(); public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthRegex() { return authRegex; } public void setAuthRegex(String authRegex) { this.authRegex = authRegex; } public List<AuthorizationScope> getAuthorizationScopeList() { return authorizationScopeList; } public void setAuthorizationScopeList(List<AuthorizationScope> authorizationScopeList) { this.authorizationScopeList = authorizationScopeList; } public List<String> getTokenUrlList() { return tokenUrlList; } public void setTokenUrlList(List<String> tokenUrlList) { this.tokenUrlList = tokenUrlList; } } public static class AuthorizationScope { /** * 作用域名称 */ private String scope = ""; /** * 作用域描述 */ private String description = ""; public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
/** * swagger 资源映射路径 * */@Configurationpublic class SwaggerWebConfiguration implements WebMvcConfigurer{ @Override public void addResourceHandlers(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: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerExceptionat 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(Native Method)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: nullat 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 | 操作 |
---|---|
1 | SpringBoot 启动同时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 不兼容问题的处理 * */@Componentpublic class SwaggerBeanPostProcessor implements BeanPostProcessor{ @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) { customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); } return bean; } private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) { List<T> copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null) .collect(Collectors.toList()); mappings.clear(); mappings.addAll(copy); } @SuppressWarnings("unchecked") private List<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) { throw new IllegalStateException(e); } }}
配置:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher
心如欲壑,后土难填。