Autoconfigure MustacheServletWebConfiguration conditional on ViewResolver
Fixes problem where if Mustache is present, but spring-webmvc isn't present, then Spring Boot Autoconfigure will fail with ClassNotFoundException:
java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigure.mustache.MustacheServletWebConfiguration.mustacheViewResolver
at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:60)
at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108)
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:193)
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:153)
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:129)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:343)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:247)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:311)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:112)
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:746)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:564)
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.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:136)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124)
at org.springframework.test.context.junit.jupiter.SpringExtension.getApplicationContext(SpringExtension.java:283)
at mil.af.kesselrun.adcp.adcpzerotrustclient.extension.RestAssuredExtension.beforeAll(RestAssuredExtension.java:29)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeBeforeAllCallbacks$10(ClassBasedTestDescriptor.java:381)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeBeforeAllCallbacks(ClassBasedTestDescriptor.java:381)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:205)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:80)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:148)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:95)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:91)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:60)
at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:98)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: java.lang.IllegalStateException: @ConditionalOnMissingBean did not specify a bean using type, name or annotation and the attempt to deduce the bean's type failed
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.validate(OnBeanCondition.java:494)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.<init>(OnBeanCondition.java:443)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchOutcome(OnBeanCondition.java:154)
at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47)
... 60 common frames omitted
Caused by: org.springframework.boot.autoconfigure.condition.OnBeanCondition$BeanTypeDeductionException: Failed to deduce bean type for org.springframework.boot.autoconfigure.mustache.MustacheServletWebConfiguration.mustacheViewResolver
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.deducedBeanTypeForBeanMethod(OnBeanCondition.java:524)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.deducedBeanType(OnBeanCondition.java:513)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.<init>(OnBeanCondition.java:436)
... 62 common frames omitted
Caused by: java.lang.NoClassDefFoundError: org/springframework/web/servlet/view/AbstractTemplateViewResolver
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1016)
at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:151)
at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:825)
at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:723)
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:646)
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:604)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:468)
at org.springframework.boot.autoconfigure.condition.FilteringSpringBootCondition.resolve(FilteringSpringBootCondition.java:108)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.getReturnType(OnBeanCondition.java:532)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.deducedBeanTypeForBeanMethod(OnBeanCondition.java:520)
... 64 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.springframework.web.servlet.view.AbstractTemplateViewResolver
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
... 78 common frames omitted
Comment From: bclozel
I'm confused. I cannot reproduce the issue you're describing with Spring Boot 2.6.5. Could you provide a sample application that shows the problem?
Also, checking for the presence of ViewResolver classes on the classpath looks strange to me. The @ConditionalOnWebApplication condition already performs classpath checks on classes that are in spring-webmvc or spring-webflux JARs.
Comment From: candrews
Here are two examples - the first is the one I experienced in the "real world" and the second is a minimally reproducing test case.
In both cases, run ./gradlew test to see the failure as described in this issue's description.
First, the "real word" is to add a dependency on org.mock-server:mockserver-spring-test-listener:5.13.0 which is how I discovered this issue. Here's an example build.gradle:
plugins {
id 'org.springframework.boot' version '2.6.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
ext {
mockserverVersion = '5.13.0'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation("org.mock-server:mockserver-spring-test-listener:${mockserverVersion}"){
exclude group: 'jakarta.validation', module: 'jakarta.validation-api'
exclude group: 'javax.validation', module: 'validation-api'
exclude group: 'org.slf4j', module: 'slf4j-ext' // slf4j-ext isn't used - it shouldn't be a dependency. See https://github.com/swagger-api/swagger-parser/pull/1560
}
}
tasks.named('test') {
useJUnitPlatform()
}
configurations {
all {
resolutionStrategy.dependencySubstitution {
substitute module('org.mock-server:mockserver-netty') using module("org.mock-server:mockserver-netty:${mockserverVersion}") withoutClassifier() because('org.mock-server:mockserver-spring-test-listener depends on org.mock-server:mockserver-netty with the "shaded" classifier. We want the non-shaded dependency. See: https://github.com/mock-server/mockserver/issues/1244')
}
}
}
Full project: demo.zip
And here's the minimal test case's build.gradle:
plugins {
id 'org.springframework.boot' version '2.6.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation("org.springframework:spring-web")
implementation("com.samskivert:jmustache:1.15")
implementation("javax.servlet:javax.servlet-api")
}
tasks.named('test') {
useJUnitPlatform()
}
Full project: demo.zip
Comment From: candrews
The
@ConditionalOnWebApplicationcondition already performs classpath checks on classes that are in spring-webmvc or spring-webflux JARs.
Looking at https://github.com/spring-projects/spring-boot/blob/v2.6.5/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnWebApplicationCondition.java, @ConditionalOnWebApplication checks for org.springframework.web.context.support.GenericWebApplicationContext, which is in spring-web. It doesn't check for anything in spring-webmvc - and that's the problem.
I don't think it makes sense to have @ConditionalOnWebApplication(type=SERVLET) require spring-webmvc which is why I added these @ConditionalOnClass annotations. That seems like it would be a significant break in backwards compatibility and break the expectations users have based on the name of that annotation.
However, maybe it makes sense to add a couple of new conditional annotations?
* @ConditionalOnWebMvc that's a composition of @ConditionalOnClass(org.springframework.web.servlet.ViewResolver.class) @ConditionalOnWebApplication(type=SERVLET)
* @ConditionalOnWebFlux that's a composition of @ConditionalOnClass(org.springframework.web.reactive.result.view.ViewResolver.class) @ConditionalOnWebApplication(type=REACTIVE)
Comment From: wilkinsona
Thanks, @candrews.
As far as I can tell, the problem is Servlet-specific. @ConditionalOnWebApplication(type = Type.SERVLET) can match if spring-web is on the classpath but spring-webmvc is not. The latter contains AbstractTemplateViewResolver so the problem you've seen can arise.
On the reactive side, @ConditionalOnWebApplication(type = Type.REACTIVE) will only match if org.springframework.web.reactive.HandlerResult is on the classpath. If HandlerResult is present then org.springframework.web.reactive.result.view.UrlBasedViewResolver most also be present as they are both in the spring-webflux module.
Comment From: candrews
As far as I can tell, the problem is Servlet-specific.
That logic makes sense to me - thank you. I've updated this PR accordingly.
Comment From: candrews
I think this problem also exists for FreeMarker:
https://github.com/spring-projects/spring-boot/blob/bb7811e7f87c38da6cd86e0d3ed6dcdc97384f7b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerServletWebConfiguration.java#L46
Comment From: wilkinsona
FreeMarker won't be affected as FreeMarkerConfigurer is in spring-webmvc.
Comment From: candrews
FreeMarker won't be affected as
FreeMarkerConfigureris inspring-webmvc.
:+1:
Perhaps this MR is now mergeable? Please let me know if there's anything more I can do.
Comment From: wilkinsona
Thanks very much, @candrews.
Comment From: candrews
Will this be backported to 2.6.x?
I'm hoping this change will be included in 2.6.6 :-)
Comment From: wilkinsona
It's already there: https://github.com/spring-projects/spring-boot/issues/30475.