记我的Springboot2.6.4从集成swagger到springdoc的坎坷路~
项目背景
主要依赖及jdk信息:
Springboot:2.6.4
Jdk: 1.8
最近新搭建了一套管理系统,前端部分没有公司的前端团队,自己在github上找了一个star较多使用相对也简单的框架。在这个管理系统搭建好上线之后,给组内的小伙伴们分享时,前端同事第一次提出,他们可以参与进来,前端部分由他们改造,我这边只需要提供接口就行。
这时,接口是现成的,但缺文档,于是,第一想到的,就是集成swagger,想着添加依赖、在一些controller上添加对应注解,就能满足需求,之后调整代码也不用再单独去维护文档。所以,就开始我这为期2天的集成路~~~
集成过程:
集成方式一:直接添加swagger依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
增加swagger配置
import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
}
启动项目,正常情况下可访问:http://host:port/context-path/swagger-ui.html查看swagger界面
遇到的问题
然而,项目启动失败,报错信息如下:
2024-12-25 12:19:59.619 [main] WARN AbstractApplicationContext.java:591 - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
WARN AbstractApplicationContext.java:591 - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
详细信息:
2024-12-25 13:29:04.634 [main] ERROR SpringApplication.java:830 - Application run failed
org.springframework.context.ApplicationContextException: Failed to start 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:145)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:740)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:415)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301)
at com.ifeng.hm.novel.admin.NovelAdminApplication.main(NovelAdminApplication.java:19)
Caused by: java.lang.NullPointerException: null
at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:112)
at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:109)
at com.google.common.collect.ComparatorOrdering.compare(ComparatorOrdering.java:40)
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
at java.util.TimSort.sort(TimSort.java:220)
at java.util.Arrays.sort(Arrays.java:1438)
at com.google.common.collect.Ordering.sortedCopy(Ordering.java:854)
at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:57)
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:138)
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:135)
at com.google.common.collect.Iterators$6.transform(Iterators.java:829)
at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:52)
at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:52)
at com.google.common.collect.Iterators$ConcatenatedIterator.hasNext(Iterators.java:1400)
at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:275)
at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:239)
at com.google.common.collect.FluentIterable.toList(FluentIterable.java:631)
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.defaultContextBuilder(DocumentationPluginsBootstrapper.java:111)
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.buildContext(DocumentationPluginsBootstrapper.java:96)
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:167)
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178)
... 14 common frames omitted
出现这个异常的原因:
在SpringBoot2.6之后,Spring MVC 处理程序映射匹配请求路径的默认策略已从 AntPathMatcher 更改为PathPatternParser。
解决方案
那既然定位到原因就好办了,针对问题解决问题,于是,借助网络资源,开始尝试~~~
网上解决方案一:
降低springboot版本,降到2.6以下,但我项目中还依赖了spring-cloud-alibaba,nacos,setinel等等,降版本比较麻烦,这个方案pass掉了,继续探索
网上解决方案二:
在springboot的配置文件(yaml或properties)中添加如下配置(示例为yaml):
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
这里的matching-strategy可选值有:
原因也很简单,既然springboot2.6之后,映射配置请求路径的默认策略从AntPathMatcher更改为了PathPatternParser,那通过配置手动改回应该就可以。
漫长探索之路
然页,添加配置后,项目启动,又失败了!!!这!不!科!学!为什么大家都留意说亲测有效,我这么配置就不行呢?
没有办法,从自身找问题吧,遇事不决先debug看看~
先找到setMatchingStrategy方法看看配置有没有生效,是没读取到配置,还是就使用了默认配置?
(org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties)
(org.springframework.boot.context.properties.bind.JavaBeanBinder.BeanProperty)
通过这里看,正确读取了配置,并调用set方法成功设置属性值。
近一步验证,配置没有问题!
继续debug...
在执行springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper#start之前的日志:
从这里的日志信息来看,Swagger2Controller注入也没有问题
Ok!那接下来重点看springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper#start方法
找到一个plugin,for循环中调用:
scanDocumentation(buildContext(each));
继续debug,到这里抛出了异常:
对这个array做sort操作时,出现了NPE!那么,继续进到这个array看为什么会是这样的(重点0-2这三个元素,actuator)
在执行Arrays.sort(array)时用到的comparator是springfox.documentation.spi.service.contexts.Orderings
所以,出现了NullPointerException~
初现柳暗花明
那么,先不进一步看为什么,先来做个尝试,把/actuator排除或者直接移除依赖可不可行?试试看~
<exclusions>
<exclusion>
<artifactId>spring-boot-actuator</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
项目成功启动,打开http://localhost:8080/swagger-ui.html
正常!
终于正常了!
但是,如果actuator强依赖,不能exclude掉呢?对的,加回来,继续尝试~~~
尝试一:通过swagger配置,只匹配某些路径
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.ant("/admin/**"))
.build();
}
/**
* 添加摘要信息
*/
private ApiInfo apiInfo()
{
// 用ApiInfoBuilder进行定制
return new ApiInfoBuilder()
// 设置标题
.title("标题:管理系统_接口文档")
// 描述
.description("描述:用于管理xxx信息,具体包括XXX,XXX模块...")
// 作者信息
.contact(new Contact("xx@abc.com", null, null))
// 版本
.version("版本号:1.0.0")
.build();
}
}
虽然,这里配置了只select带有ApiOperation注解的method以及路径/admin/**,但是,对前面那个异常无效,并不妨碍它失败!!!
究其原因:
这个AddtionalHealthEndpointPathsWebMvcHandlerMapping依然会把/actuator/**加入之前的arrays中,而在array的sort时会因为PatternsCondition为null从而出现NPE!
那么,接下来,对WebMvcRequestHandlerProvider进行改造。
方法一:
自定义BeanPostProcessor在postProcessAfterInitialization方法中对PatternParser进行过滤
@Bean
public BeanPostProcessor springfoxHandlerProviderBeanPostProcessor()
{
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
};
}
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);
}
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);
}
}
重点:customizeSpringfoxHandlerMappings方法
经过filter之后的array
NullPointerException不见了!
方法二:
hack一个WebMvcRequestHandlerProvider,处理handlerMapping属性
在src目录下创建package:
springfox.documentation.spring.web.plugins
复制WebMvcRequestHandlerProvider到这个自建的同名的包下
修改构造方法:
同样,启动成功~~~
到这里,springboot2集成swagger过程中遇到的这个问题算是解决了。但是这个过程太痛苦了,费时,又费力~~~~
集成方式二:使用springfox-boot-starter
添加依赖:
<!-- Swagger3依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${swagger.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
与集成方式一相同,会遇到同样的问题,解决方式也相同,不再赘述!
但是它有个问题
2020年之后,不再维护了~,这让人很不安啊!!!
集成方式三:接入springdoc,放弃springfox
添加依赖
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.8.0</version>
</dependency>
注:1.8.0是最新支持springboot2.x和1.x的版本
增加简单配置类:
@Configuration
public class OpenApiConfig {
}
启动项目:
正常启动,没有出现方式一、方式二中遇到的问题,喜大普奔~~~~
springdoc的继续探索
分组与接口过滤
/**
* 配置过滤规则
* 若不配置该GroupedOpenApi, 默认扫描所有接口并生成文档
* @return
*/
@Bean
public GroupedOpenApi bookApi() {
return GroupedOpenApi.builder()
// 接口过滤,据此增加接口扫描规则(扫描@Operation注解标注的接口)。想皮一下的话,亦可自定义注解
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))
.group("book") // 分组名,可建多个不同分组,分别扫描不同位置接口
.pathsToMatch("/admin/book/**")
.build();
}
/**
* 配置过滤规则
* 若不配置该GroupedOpenApi, 默认扫描所有接口并生成文档
* @return
*/
@Bean
public GroupedOpenApi bookCategoryApi() {
return GroupedOpenApi.builder()
// 接口过滤,据此增加接口扫描规则(扫描@Operation注解标注的接口)。想皮一下的话,亦可自定义注解
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))
.group("bookCategory") // 分组名,可建多个不同分组,分别扫描不同位置接口
.pathsToMatch("/admin/category/**")
.build();
}
效果:
常见配置
见官方文档:OpenAPI 3 Library for spring-boot
接口鉴权与认证
启动类增加@SecurityScheme注解
@SecurityScheme(name = "api_token", type = SecuritySchemeType.HTTP, scheme ="bearer", in = SecuritySchemeIn.HEADER)
public class BookAdminApplication {
public static void main(String[] args) {
SpringApplication.run(NovelAdminApplication.class, args);
}
}
配置类中配置API安全策略:
@Bean
public OpenAPI demoOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Novel/Book API")
.description("novel-admin book,book-category,book-list related api docs")
.version("v1.0.0")
)
.security(List.of(new SecurityRequirement().addList("api_token")))//注意这里的api_token与启动类中的配置相同
;
}
如果某个接口不需要安全认证(如login,logout之类),则可以在接口方法中添加如下注解:
@RestController
@RequestMapping("/healthChk")
public class HealthChkController {
//@SecurityRequirements() 里面需要一个String数组,里面列出需要使用的@SecurityScheme,例如我们这里的api_token。如果不写就说明不需要任何的安全模式,这里就是这种情况。
@SecurityRequirements()
@RequestMapping("/check")
public String healthChk() {
return "ok";
}
}
@SecurityRequirements() 里面需要一个String数组,里面列出需要使用的@SecurityScheme,例如我们这里的api_token。如果不写就说明不需要任何的安全模式,这里就是这种情况。
其他更详细的使用,可以阅读springdoc官方文档学习~~~