org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'resourceHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.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:658) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:638) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.13.jar:5.3.13]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.13.jar:5.3.13]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.13.jar:5.3.13]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.6.1.jar:2.6.1]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730) ~[spring-boot-2.6.1.jar:2.6.1]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:412) ~[spring-boot-2.6.1.jar:2.6.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:302) ~[spring-boot-2.6.1.jar:2.6.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[spring-boot-2.6.1.jar:2.6.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1290) ~[spring-boot-2.6.1.jar:2.6.1]
at com.example.demo.DemoApplication.main(DemoApplication.java:10) ~[main/:na]
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.3.13.jar:5.3.13]
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653) ~[spring-beans-5.3.13.jar:5.3.13]
... 19 common frames omitted
Caused by: java.lang.IllegalStateException: No ServletContext set
at org.springframework.util.Assert.state(Assert.java:76) ~[spring-core-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.resourceHandlerMapping(WebMvcConfigurationSupport.java:591) ~[spring-webmvc-5.3.13.jar:5.3.13]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.3.13.jar:5.3.13]
... 20 common frames omitted
The attached project demonstrates this problem. It seems to require a very specific combination of dependencies (actuator, data rest and sleuth) and for the presence of a health indicator which takes a RestTemplateBuilder.
Adding the following annotation to the HealthIndicator seems to resolve the problem.
@DependsOn("healthContributorRegistry")
This took ages to track down and was detected during an upgrade from Spring Boot 2.5.7 to 2.6.1.
Comment From: wilkinsona
Thanks very much for the sample, @smcgregor83. I can well imagine that this took a long time to track down.
Without your workaround in place, Spring Boot's EnableWebMvcConfiguration
(a WebMvcConfigurationSupport
sub-class) is created before the ServletContext
is available. This means that when it's post-processed by WebApplicationContextServletContextAwareProcessor
, no call to setServletContext
is made. This is what causes the subsequent call to resourceHandlerMapping
to fail. The chain of dependencies that leads to this early initialization of EnableWebMvcConfiguration
is sizeable. We'll need to spend some time digging through it to identify the cause.
Comment From: wilkinsona
Here's the chain of beans that are created:
tomcatServletWebServerFactory
traceTomcatWebServerFactoryCustomizer
braveHttpServerHandler
httpTracing
sleuthSkipPatternProvider
WebEndpointDiscoverer getEndpoints()
healthEndpoint
healthContributorRegistry
getBeansOfType(HealthContributor.class)
testHealthIndicator
restTemplateBuilder
restTemplateBuilderConfigurer
messageConverters
jacksonHttpMessageConverter (Spring Data REST)
linkCollector
selfLinkProvider
mvcConversionService
EnableWebMvcConfiguration
The problem begins almost immediately as Sleuth's traceTomcatWebServerFactoryCustomizer
triggers the creation of a large number of beans via sleuthSkipPatternProvider
which triggers actuator endpoint discovery. This appears to have caused problems in the past. As far as I can tell, this will prevent any endpoint bean or bean that is required by an endpoint from having the ServletContext
injected when they are ServletContextAware
. Sleuth could perhaps fix this by accessing the HttpServerHandler
lazily.
I've opened https://github.com/spring-cloud/spring-cloud-sleuth/issues/2074 so that the Sleuth team can investigate.
Many thanks again for the sample, @smcgregor83, it was a huge help in quickly identify the cause of the problem you're facing.
Comment From: smcgregor83
Thanks @wilkinsona for the quick response. Something to highlight with the workaround I mentioned is that this effectively disables the health indicator as it won't make it into the registry. Currently trying to find some other workaround.
Comment From: wilkinsona
As long as you don't use it in the health indicator's constructor, marking the injection of RestTemplateBuilder
as @Lazy
allows your sample application to start:
package com.example.demo;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
public class TestHealthIndicator implements HealthIndicator {
private final RestTemplateBuilder builder;
public TestHealthIndicator(@Lazy RestTemplateBuilder builder) {
this.builder = builder;
}
@Override
public Health health() {
return Health.up().build();
}
}
This results in builder
being proxied. An alternative that just defers the bean creation without a proxy would be to use ObjectFactory
:
package com.example.demo;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.stereotype.Component;
@Component
public class TestHealthIndicator implements HealthIndicator {
private final ObjectFactory<RestTemplateBuilder> builder;
public TestHealthIndicator(ObjectFactory<RestTemplateBuilder> builder) {
this.builder = builder;
}
@Override
public Health health() {
return Health.up().build();
}
}
Again, this will only work if you don't call the factory's getObject()
method in the indicator's constructor. You could use Framework's SingletonSupplier
to take care of deferring the use of the RestTemplateBuilder
bean but still only building the rest template once:
package com.example.demo;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.function.SingletonSupplier;
import org.springframework.web.client.RestTemplate;
@Component
public class TestHealthIndicator implements HealthIndicator {
private final SingletonSupplier<RestTemplate> restTemplate;
public TestHealthIndicator(ObjectFactory<RestTemplateBuilder> builderFactory) {
this.restTemplate = new SingletonSupplier<>(() -> builderFactory.getObject().build(), null);
}
@Override
public Health health() {
RestTemplate rest = this.restTemplate.get();
return Health.up().build();
}
}
Comment From: smcgregor83
Thanks very much. I've done something along those lines using ObjectProvider.
Our real service has HealthIndicator
implementations which have a @Component
injected, this component is our client which in turn uses RestTemplateBuilder
within its own constructor to build a RestTemplate
.
If I use ObjectProvider
to inject the client @Component
into the HealthIndicator
, everything then works as expected.
Thanks again for the help on this one. Took a good two days to get to the bottom of.