opengoofy/crane4j

在启动类添加 `@EnableCrane4j` 注解后,启动应用报错 “No ServletContext set”

Closed this issue · 4 comments

springboot 版本为 2.2.13.RELEASE,在启动类添加 @EnableCrane4j 注解后,启动应用报错:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
19:09:57.511 ERROR [main] [] [TID: N/A] org.springframework.boot.SpringApplication 826 reportFailure - Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'resourceHandlerMapping' defined in class path resource [cn/net/nova/component/config/WebMvcConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerMapping]: Factory method 'resourceHandlerMapping' threw exception; nested exception is java.lang.IllegalStateException: No ServletContext set
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:657) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:637) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1336) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1176) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879) ~[spring-context-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551) ~[spring-context-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) [spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:405) [spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) [spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at cn.net.nova.self.MbossSelfServiceApplication.main(MbossSelfServiceApplication.java:40) [classes/:?]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerMapping]: Factory method 'resourceHandlerMapping' threw exception; nested exception is java.lang.IllegalStateException: No ServletContext set
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:652) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	... 19 more
Caused by: java.lang.IllegalStateException: No ServletContext set
	at org.springframework.util.Assert.state(Assert.java:76) ~[spring-core-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.resourceHandlerMapping(WebMvcConfigurationSupport.java:534) ~[spring-webmvc-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at cn.net.nova.component.config.WebMvcConfig$$EnhancerBySpringCGLIB$$cbf82e93.CGLIB$resourceHandlerMapping$23(<generated>) ~[mboss-component-1.0-20230619.070641-242.jar:?]
	at cn.net.nova.component.config.WebMvcConfig$$EnhancerBySpringCGLIB$$cbf82e93$$FastClassBySpringCGLIB$$c78f2e22.invoke(<generated>) ~[mboss-component-1.0-20230619.070641-242.jar:?]
	at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[spring-context-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at cn.net.nova.component.config.WebMvcConfig$$EnhancerBySpringCGLIB$$cbf82e93.resourceHandlerMapping(<generated>) ~[mboss-component-1.0-20230619.070641-242.jar:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_92]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_92]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_92]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_92]
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:652) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	... 19 more

Disconnected from the target VM, address: '127.0.0.1:54353', transport: 'socket'

Process finished with exit code 1

根据排查,似乎是在通过 WebMvcConfigurationSupport#resourceHandlerMapping 创建 HandlerMapping 时,由于当前实例仍然没有 ServletContext 所以报错:
image
其中,实例中的 ServletContext 来自于 ServletContextAware 回调接口的 setServletContext 方法,此时通过调试发现,用于在后处理阶段调用 ServletContextAware 回调接口的后处理器 ServletContextAwareProcessor 在容器中并不存在:
image
由于未经过后处理器的处理,因此该 WebMvcConfigurationSupport 实例并没有获得 ServletContext ,然后创建 HandlerMapping 由于 ServletContext 为空所以直接报错了。

经过试验,不再直接通过 @EnableCrane4j 引入配置,而是直接引入 Crane4jAutoConfiguration 配置类即可:

/**
 * 在项目里面另外建一个配置类继承 Crane4jAutoConfiguration
 * 
 * @author huangchengxing
 */
@Configuration
public class Crane4jConfig extends Crane4jAutoConfiguration {
}

不过具体原因还需要进一步排查,怀疑是 crane4j-spring-boot-starter 中引入的 springboot 依赖中自带了部分自动配置类,导致依赖或配置冲突。

经过验证,也可以在自己的项目中的 META-INF 文件夹下通过 SPI 文件引入 crane4j 配置类,这种方式也不会报错:

  1. 在 springboot 2.7 及以上版本,你需要在 spirng 文件夹下提供一个 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,里面内容如下:

    cn.crane4j.spring.boot.config.Crane4jAutoConfiguration
    cn.crane4j.spring.boot.config.Crane4jJacksonConfiguration
    cn.crane4j.spring.boot.config.Crane4jMybatisPlusAutoConfiguration
    
  2. 在 springboot 2.7 以下版本,你需要提供一个 spring.factories 文件,里面内容如下:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      cn.crane4j.spring.boot.config.Crane4jAutoConfiguration,\
      cn.crane4j.spring.boot.config.Crane4jJacksonConfiguration,\
      cn.crane4j.spring.boot.config.Crane4jMybatisPlusAutoConfiguration
    

2.4.0 版本这个问题仍然还未解决,经过研究,发现了问题确实是因为配置的加载顺序导致的,不过并不是之前说的 ServletContextAwareProcessor 不存在(实际上生效的是它的子类 WebApplicationContextServletContextAwareProcessor),而是 ServletContextAwareProcessor 生效时并没有获取到 ServletContext

首先,Crane4jAutoConfiguration 这个配置类里面有一个 BeanMethodContainerPostProcessor,它是一个 BeanPostProcessor 类型的 Bean,用来将方法适配为方法容器,在容器启动时,它会在 registerBeanPostProcessors 这一步提前将其初始化。

然而, BeanMethodContainerPostProcessor 依赖了非常多的上级组件,这导致它们会跟着该后处理器一并发生初始化,这个依赖链最后导致提前触发了 WebMvcAutoConfiguration 的初始化:

  • beanMethodContainerPostProcessor
  • cacheableMethodContainerFactory
  • methodInvokerContainerCreator
  • propertyOperator
  • springConverterManager
  • mvcConversionService
  • org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration

EnableWebMvcConfiguration 实现了 ServletContextAware 接口,当它加载时,ServletContextAwareProcessor 这个后处理器依然获取不到 ServletContext,因此 EnableWebMvcConfiguration 也不能通过回调接口得到 ServletContextAware

再往后,当容器加载所有单例 Bean 时,将会根据 EnableWebMvcConfiguration.resourceHandlerMapping 工厂方法创建 HandlerMapping,此时它将会尝试获取 EnableWebMvcConfiguration 中通过 ServletContextAware 回调注入的 ServletContext,然而由于该配置类并没有获得 ServletContext,因此就会报错 “No ServletContext set”。

SpringBoot 在 Web 环境的启动这一块我不是很熟,但是根据目前的观察,将 Crane4j 的配置类延迟加载可以解决这个问题,比如将注解加载项目的配置类上,或者将令本地的配置类继承 Crane4j 的配置类。

不过,这个问题也反映了通过 BeanPostProcessor 实现将方法适配为容器的功能并不合理,它会导致在自动装配时,仅仅为了加载这么一个 Bean 就让非常多的关联组件被提前加载。在下一个 2.5.0 版本,将会重构它,参照 Spring 的 EventListenerProcessor,改为基于 SmartInitializingSingleton 实现。