Springboot 2.6.x整合springfox-swagger 3.0 报 Failed to start bean documentationPluginsBootstrapper的问题
版本号
- spring boot 2.6.11
- springfox-swagger 3.0.0
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.11</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
问题现象
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) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.22.jar:5.3.22]
at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_121]
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.6.11.jar:2.6.11]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:745) [spring-boot-2.6.11.jar:2.6.11]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:420) [spring-boot-2.6.11.jar:2.6.11]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.6.11.jar:2.6.11]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317) [spring-boot-2.6.11.jar:2.6.11]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) [spring-boot-2.6.11.jar:2.6.11]
at com.winsyun.emr.portal.EmrApplication.main(EmrApplication.java:36) [classes/:na]
Caused by: java.lang.NullPointerException: null
at springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns(WebMvcPatternsRequestConditionWrapper.java:56) ~[springfox-spring-webmvc-3.0.0.jar:3.0.0]
at springfox.documentation.RequestHandler.sortedPaths(RequestHandler.java:113) ~[springfox-core-3.0.0.jar:3.0.0]
at springfox.documentation.spi.service.contexts.Orderings.lambda$byPatternsCondition$3(Orderings.java:89) ~[springfox-spi-3.0.0.jar:3.0.0]
at java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:469) ~[na:1.8.0_121]
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:1.8.0_121]
at java.util.TimSort.sort(TimSort.java:234) ~[na:1.8.0_121]
at java.util.Arrays.sort(Arrays.java:1512) ~[na:1.8.0_121]
at java.util.ArrayList.sort(ArrayList.java:1454) ~[na:1.8.0_121]
at java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:387) ~[na:1.8.0_121]
at java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:1.8.0_121]
at java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:1.8.0_121]
at java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:1.8.0_121]
at java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:1.8.0_121]
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482) ~[na:1.8.0_121]
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) ~[na:1.8.0_121]
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) ~[na:1.8.0_121]
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_121]
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) ~[na:1.8.0_121]
at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:81) ~[springfox-spring-webmvc-3.0.0.jar:3.0.0]
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) ~[na:1.8.0_121]
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374) ~[na:1.8.0_121]
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) ~[na:1.8.0_121]
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) ~[na:1.8.0_121]
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) ~[na:1.8.0_121]
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_121]
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) ~[na:1.8.0_121]
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.withDefaults(AbstractDocumentationPluginsBootstrapper.java:107) ~[springfox-spring-web-3.0.0.jar:3.0.0]
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.buildContext(AbstractDocumentationPluginsBootstrapper.java:91) ~[springfox-spring-web-3.0.0.jar:3.0.0]
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.bootstrapDocumentationPlugins(AbstractDocumentationPluginsBootstrapper.java:82) ~[springfox-spring-web-3.0.0.jar:3.0.0]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:100) ~[springfox-spring-web-3.0.0.jar:3.0.0]
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.22.jar:5.3.22]
... 14 common frames omitted
注意
- 1 这里的异常一定是 NullPointerException
- 2 一定要是方法 springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns 中抛出的
如果你的问题不符合以上两点那可能是其它问题导致的本文方法不一定能解决
网上可查的解决方法
一 修改配置文件(推荐使用)
这种方法认为Spring 2.6.x 默认路径匹配策略从AntPathMatcher 更改为PathPatternParser,而 springfox-swagger 还是使用 AntPathMather导致的错误
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
二 添加继承自 WebMvcConfigurationSupport的配置类(有副作用谨慎使用)
并且要求将swagger前端资源webjars添加到resourceHandlers
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// swagger配置
registry.
addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
.resourceChain(false);
}
三 添加BeanPostProcessor(不要使用)
@Bean
public static 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;
}
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);
}
}
};
}
各方法的比较和不足
- 1、先说一下第一种方案,不可否认确实是有效的并且是Spring boot官方提供的配置,我不知道为什么网上有些人说这个方案是治标的,但是这种方案还有一个问题当引入spring-boot-starter-actuator依赖时还会出现上术错误所以这个方案还有待改进
- 2、第二种方案也是有效的但是网上并没有说原理,并且没有强调必须得继承自WebMvcConfigurationSupport,经测试加不加addResourceHandler 效果是一样的(swagger会处理的),并且配置类必须继承自WebMvcConfigurationSupport 而不能实现WebMvcConfigurer,不过这种方案是有副作用的,因为WebMvcConfigurationSupport 只有一个生效,会影响SpringBoot自带的配置,如果处理不好的话一些自定义配置也不生效(这种方案的本质也是通过使用自定义的WebMvcConfigurationSupport让Spring 自带的WebMvcConfigurationSupport 的pathMatche配置失效来达到目的,本质上和第一种方案一样)。
- 3、第三种方案有人说这是最优雅的并且是治本的,首先需要说明的是程序确实不报错误,但是你会发现swagger页面根本就不生成API文档,这也就失去了引入swagger的价值了
综上所术第一种方案和第二种方案是有效的第三种方案虽然解决了程序报错问题但是把需要swagger生成文档的接口全部给过虑掉了失去了使用swagger的初衷,所以第三种方案是不可取的
问题解析
正如其它网上所说确实是spring boot 2.6之后默认的路径匹配策略从AntPathMatcher 更改为PathPatternParser而新规则生成的url保存在了一个新的字段上,老的字段为null而springfox-swagger在对url排序的时候使用了路径(还是取的老字段)这就是为什么会报 NullPointerException
根据错误信息报空指针的地方为springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns下面看一下它的代码
@Override
public Set<String> getPatterns() {
//这里的condition为null
return this.condition.getPatterns().stream()
.map(p -> String.format("%s/%s", maybeChompTrailingSlash(contextPath), maybeChompLeadingSlash(p)))
.collect(Collectors.toSet());
}
下一步就是要确定这里的condition是怎么来的,通过代码跟踪发现在类WebMvcRequestHandler 实例化了 WebMvcPatternsRequestConditionWrapper,并且将requestMapping.getPatternsCondition()做为参数传给了condition 如下:
//这里实例化了WebMvcPatternsRequestConditionWrapper
@Override
public PatternsRequestCondition getPatternsCondition() {
return new WebMvcPatternsRequestConditionWrapper(
contextPath,
requestMapping.getPatternsCondition());
}
通过查看requestMapping的类型为RequestMappingInfo 是SpringMvc的一个类,并且是保存SpringMvc路径信息的,来看一下它的结构:
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
//...忽略一些不重要的信息
//路径名
@Nullable
private final String name;
//新的使用PathPatternParser 解析出来的路径保存在这里
@Nullable
private final PathPatternsRequestCondition pathPatternsCondition;
//原来的使用AntPathMatcher 解析出来的路径保存在这里
@Nullable
private final PatternsRequestCondition patternsCondition;
这里已经很明显 新版本Spring boot解析出来的路径信息使用了新的字段,而springfox-swagger使用的还是原字段patternsCondition
问题解决
问题弄清楚之后那么解决办法就很简单了
- 方案一 修改springfox-swagger代码使用 getPathPatternsCondition 替换 getPatternsCondition
- 方案二 想办法让patternsCondition 有值
由于springfox-swagger 不提供配置选项,所以第一种方案需要重新编译代码成本较高,所以选择第二种方案,Springboot是提供了对应的配置的也就是上文提到的spring.mvc.pathmatch.matching-strategy=ant_path_matcher
spring.mvc.pathmatch.matching-strategy 是怎么工作的呢
通过代码跟踪可以发现在自动配置类 WebMvcAutoConfiguration 中使用了这个配置代码如下:
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
//这里判断如果是PATH_PATTERN_PARSER 向配置类中加入了pathPatternParser
if (this.mvcProperties.getPathmatch()
.getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) {
configurer.setPatternParser(pathPatternParser);
}
configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
configurer.setUseRegisteredSuffixPatternMatch(
this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
this.dispatcherServletPath.ifAvailable((dispatcherPath) -> {
String servletUrlMapping = dispatcherPath.getServletUrlMapping();
if (servletUrlMapping.equals("/") && singleDispatcherServlet()) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setAlwaysUseFullPath(true);
configurer.setUrlPathHelper(urlPathHelper);
}
});
}
在默认情况下Url匹配策略为PATH_PATTERN_PARSER 所以会通过configurer.setPatternParser(pathPatternParser);配置进去一个路径解析类,然后在通过RequestMappingInfo.DefaultBuilder创建 RequestMappingInfo时会根据这个配置向不同的字段上赋值代码如下:
@Override
@SuppressWarnings("deprecation")
public RequestMappingInfo build() {
PathPatternsRequestCondition pathPatterns = null;
PatternsRequestCondition patterns = null;
//当patternParser 非空时 将值赋给pathPatterns
if (this.options.patternParser != null) {
pathPatterns = (ObjectUtils.isEmpty(this.paths) ?
EMPTY_PATH_PATTERNS :
new PathPatternsRequestCondition(this.options.patternParser, this.paths));
}
else { //当patternParser 为空时 将值赋给patterns
patterns = (ObjectUtils.isEmpty(this.paths) ?
EMPTY_PATTERNS :
new PatternsRequestCondition(
this.paths, null, this.options.getPathMatcher(),
this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
this.options.getFileExtensions()));
}
ContentNegotiationManager manager = this.options.getContentNegotiationManager();
return new RequestMappingInfo(
this.mappingName, pathPatterns, patterns,
ObjectUtils.isEmpty(this.methods) ?
EMPTY_REQUEST_METHODS : new RequestMethodsRequestCondition(this.methods),
ObjectUtils.isEmpty(this.params) ?
EMPTY_PARAMS : new ParamsRequestCondition(this.params),
ObjectUtils.isEmpty(this.headers) ?
EMPTY_HEADERS : new HeadersRequestCondition(this.headers),
ObjectUtils.isEmpty(this.consumes) && !this.hasContentType ?
EMPTY_CONSUMES : new ConsumesRequestCondition(this.consumes, this.headers),
ObjectUtils.isEmpty(this.produces) && !this.hasAccept ?
EMPTY_PRODUCES : new ProducesRequestCondition(this.produces, this.headers, manager),
this.customCondition != null ?
new RequestConditionHolder(this.customCondition) : EMPTY_CUSTOM,
this.options);
}
以上就是spring.mvc.pathmatch.matching-strategy配置工作的原理,也可以通过Java配置的方式修改这个配置WebMvcConfigurer.configurePathMatch(PathMatchConfigurer configurer)
解决引入spring-boot-starter-actuator后配置失效问题
熟悉actuator的都知道它向外暴露接口并不是通过RequestMapping注解实现的而是通过Endpoint注解来实现的所以咱们之前对RequestMappingHandlerMapping的配置并不会影响actuator的路径匹配规则,所以我们需要找到actuator对WebMvcEndpointHandlerMapping配置的部分进行配置修改,在配置类WebMvcEndpointManagementContextConfiguration中有以下配置:
@Bean
@ConditionalOnMissingBean //这里的条件注入使得我们可以定义自己的配置
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties, Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
shouldRegisterLinksMapping, WebMvcAutoConfiguration.pathPatternParser); //这里配置了pathPatternParser
}
所以这里的解决方案和之前的类似把这里的pathPatternParser置空就行了,在我们自己的项目中增加配置:
@Bean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes,
CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties,
Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
shouldRegisterLinksMapping, null);//这个地方置成了null
}
这样问题就全部解决了。
其它知识扩展
- 类 WebMvcRequestHandlerProvider 中处理了从Spring 中获取的路径信息,并将这些信息提供给Swagger
@Override
public List<RequestHandler> requestHandlers() {
//这里的handlerMappings是从SpringMvc中获取的所有路径
return nullToEmptyList(handlerMappings).stream()
.filter(requestMappingInfoHandlerMapping ->
!("org.springframework.integration.http.inbound.IntegrationRequestMappingHandlerMapping"
.equals(requestMappingInfoHandlerMapping.getClass()
.getName())))
.map(toMappingEntries())
.flatMap((entries -> StreamSupport.stream(entries.spliterator(), false)))
//这里进行了RequestHandler 转换 最终生成WebMvcRequestHandler
.map(toRequestHandler())
//这里调用WebMvcRequestHandler.getPatternsCondition 获取路径
.sorted(byPatternsCondition())
.collect(toList());
}
这个 WebMvcRequestHandlerProvider 相当于一个适配器把SpringMvc中的路径获取到springfox-swagger中并转换成需要的数据,上面代码在排序的时候使用到了 WebMvcRequestHandler.getPatternsCondition
public static Comparator<RequestHandler> byPatternsCondition() {
return Comparator
.comparing(requestHandler -> sortedPaths(requestHandler.getPatternsCondition()));
}
- 这里的toRequestHandler方法将路径信息转换成了 WebMvcRequestHandler。
private Function<Map.Entry<RequestMappingInfo, HandlerMethod>, RequestHandler> toRequestHandler() {
//这里的input 是 Map<RequestMappingInfo, HandlerMethod> 的一个元素,所以这里的key为对象RequestMappingInfo的实例
return input -> new WebMvcRequestHandler(
contextPath,
methodResolver,
input.getKey(),
input.getValue());
}
- WebMvcRequestHandlerProvider 注入了所有的RequestMappingInfoHandlerMapping
@Autowired
public WebMvcRequestHandlerProvider(
Optional<ServletContext> servletContext,
HandlerMethodResolver methodResolver,
List<RequestMappingInfoHandlerMapping> handlerMappings) {
this.handlerMappings = handlerMappings;//注入SpringMvc的路径处理器
this.methodResolver = methodResolver;
this.contextPath = servletContext
.map(ServletContext::getContextPath)
.orElse(ROOT);
}
- RequestMappingHandlerMapping 创建RequestMappingInfo
private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) {
return RequestMappingInfo.paths(this.endpointMapping.createSubPath(path)).options(this.builderConfig)
.methods(RequestMethod.valueOf(predicate.getHttpMethod().name()))
.consumes(predicate.getConsumes().toArray(new String[0]))
.produces(predicate.getProduces().toArray(new String[0])).build();
}
- RequestMappingHandlerMapping 获取全部路径,注意这里返回的Map是不能修改了
public Map<T, HandlerMethod> getHandlerMethods() {
this.mappingRegistry.acquireReadLock();
try {
//这里返回的是不能被修改的Map
return Collections.unmodifiableMap(
this.mappingRegistry.getRegistrations().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().handlerMethod)));
}
finally {
this.mappingRegistry.releaseReadLock();
}
}