Version: 2.2.[0|1].RELEASE
Context:
Behavior in 2.1.x.RELEASE was that the webflux gated config would be excluded and all was well.
Behavior w/ 2.2.x.RELEASE is startup fails w/ following:
java.lang.NoClassDefFoundError: org/springframework/web/reactive/config/WebFluxConfigurer
at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_102]
at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_102]
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[na:1.8.0_102]
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) ~[na:1.8.0_102]
at java.net.URLClassLoader.access$100(URLClassLoader.java:73) ~[na:1.8.0_102]
at java.net.URLClassLoader$1.run(URLClassLoader.java:368) ~[na:1.8.0_102]
at java.net.URLClassLoader$1.run(URLClassLoader.java:362) ~[na:1.8.0_102]
at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_102]
at java.net.URLClassLoader.findClass(URLClassLoader.java:361) ~[na:1.8.0_102]
at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_102]
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) ~[na:1.8.0_102]
at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_102]
at java.lang.Class.getDeclaringClass0(Native Method) ~[na:1.8.0_102]
at java.lang.Class.getDeclaringClass(Class.java:1235) ~[na:1.8.0_102]
at java.lang.Class.getEnclosingClass(Class.java:1277) ~[na:1.8.0_102]
at org.springframework.core.annotation.AnnotationsScanner.processClassHierarchy(AnnotationsScanner.java:233) ~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.core.annotation.AnnotationsScanner.processClassHierarchy(AnnotationsScanner.java:194) ~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.core.annotation.AnnotationsScanner.processClass(AnnotationsScanner.java:130) ~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.core.annotation.AnnotationsScanner.process(AnnotationsScanner.java:107) ~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.core.annotation.AnnotationsScanner.scan(AnnotationsScanner.java:97) ~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.core.annotation.AnnotationsScanner.scan(AnnotationsScanner.java:78) ~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.core.annotation.TypeMappedAnnotations.scan(TypeMappedAnnotations.java:242) ~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.core.annotation.TypeMappedAnnotations.isPresent(TypeMappedAnnotations.java:98) ~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider.isConstructorBindingAnnotatedType(ConfigurationPropertiesBindConstructorProvider.java:81) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider.getBindConstructor(ConfigurationPropertiesBindConstructorProvider.java:49) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.context.properties.ConfigurationPropertiesBean$BindMethod.forType(ConfigurationPropertiesBean.java:311) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator.validate(ConfigurationPropertiesBeanDefinitionValidator.java:61) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator.postProcessBeanFactory(ConfigurationPropertiesBeanDefinitionValidator.java:44) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:286) ~[spring-context-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:174) ~[spring-context-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706) ~[spring-context-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532) ~[spring-context-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) [spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) [spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at com.example.demo.DemoApplication.main(DemoApplication.java:10) [classes/:na]
Caused by: java.lang.ClassNotFoundException: org.springframework.web.reactive.config.WebFluxConfigurer
at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[na:1.8.0_102]
at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_102]
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) ~[na:1.8.0_102]
at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_102]
... 39 common frames omitted
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>provided</scope>
</dependency>
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(DemoProperties.class)
public class DemoAutoConfiguration {
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(WebMvcConfigurer.class)
@Configuration
static class DemoWebMvcConfiguration implements WebMvcConfigurer {
@PostConstruct
public void init() {
System.out.println("DemoWebMvcConfiguration.init()");
}
}
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@Configuration
static class DemoWebFluxConfiguration implements WebFluxConfigurer {
@PostConstruct
public void init() {
System.out.println("DemoWebFluxConfiguration.init()");
}
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>demolib</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
NOTE: If I comment out the DemoWebMvcConfiguration the conditional behaves and the webflux gated config is excluded and all is well. It seems that when they are both present the config properties annotation scan tries to load the classes.
I hvae made some debug and found some strange thing. In AnnotationsScanner.java:233 (spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE), source.getEnclosingClass() would cause loadClass("com.example.demolib.DemoAutoConfiguration$DemoWebFluxConfiguration", false) to be called, while the value of source is com.example.demolib.DemoAutoConfiguration$DemoWebMvcConfiguration Of java.lang.Class. However, in demolib.jar the enclosing class of com.example.demolib.DemoAutoConfiguration$DemoWebMvcConfiguration should be DemoAutoConfiguration.
@OLPMO I have done the same debugging and that call to
loadClass("com.example.demolib.DemoAutoConfiguration$DemoWebFluxConfiguration")
is a result from the call on native method
class com.example.demolib.DemoAutoConfiguration$DemoWebMvcConfiguration#getDeclaringClass0()
which as expected, first loads its outer class
com.example.demolib.DemoAutoConfiguration
, but that in turn causes the load on its nested static class
com.example.demolib.DemoAutoConfiguration$DemoWebFluxConfiguration
which then fails.
So it seems like the "safety" of the ASM processed annotation metdata (in this case @ConditionalOnClass(WebFluxConfigurer.class)) is not being respect in this case.
@jhoeller, this does look like a regression, so I've scheduled tentatively for 5.2.3.
I'm actually not certain that this is a regression.
Rather, it appears to me that the cause is this change in Spring Boot 2.2.
Specifically, prior to that commit, Spring Boot did not search for @ConstructorBinding on enclosing classes, and as pointed out by @bono007 in https://github.com/spring-projects/spring-framework/issues/24136#issuecomment-558461538, loading of the enclosing class appears to result in automatic loading of the sibling static nested class... which used to not be the case.
@philwebb, what are your thoughts on this?
So it seems like the "safety" of the ASM processed annotation metdata (in this case
@ConditionalOnClass(WebFluxConfigurer.class)) is not being respect in this case.
@bono007, the class that kicks this off is ConfigurationPropertiesBeanDefinitionValidator which is a BeanFactoryPostProcessor. Thus, ASM is not used for the annotation processing in this stage.
In any case, AnnotatedElementUtils.searchWithFindSemantics() used to handle all exceptions by invoking AnnotationUtils.handleIntrospectionFailure(): https://github.com/spring-projects/spring-framework/blob/da4e2710b48bd1759563c313194fc772f9cd62c8/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java#L1242
AnnotationUtils.handleIntrospectionFailure() was removed in b91ccf038f309dfd1f91101133598b79c0b7efdf and added back as a private method in 37255afca49b0787ecfd074a7ba4c91788694e5a. So it appears we completely lost that support outside of AnnotationUtils.
Thus, we should probably start using that in the newly introduced AnnotationsScanner.scan() methods as well.
@jhoeller and @philwebb, thoughts?
I wonder if the TYPE_HIERARCHY_AND_ENCLOSING_CLASSES should be more defensive? We can try to fix this back in Spring Boot but we'd probably end up duplicating that search logic.
I wonder if the
TYPE_HIERARCHY_AND_ENCLOSING_CLASSESshould be more defensive?
That would be one option: to introduce a try-catch around the enclosing class lookup.
However, I think we will have to reintroduce use of AnnotationUtils.handleIntrospectionFailure() at a higher level as well, in order to be backwards compatible with previous Spring Framework versions.
We can try to fix this back in Spring Boot but we'd probably end up duplicating that search logic.
Yes, we'd ideally like to avoid that.
OK, I have made the annotation scanning algorithm a bit more relaxed for enclosing class hierarchies in 16ed7e2a19c074b26a99acc43fddb373db752fb7 and 6e21b19999e967e4f4933bfda10b7a85206def34.
@bono007, please try it out with the next Spring Framework 5.2.3 BUILD-SNAPSHOT and let us know if the changes resolve your issue.
@sbrannen - nice work!
Yep - that does it. I ensured I could still reproduce it.

I then added snapshot repo and version to my pom.xml as such and got clean startup.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.3.BUILD-SNAPSHOT</version>
</dependency>
<repositories>
<repository>
<id>repository.springframework.maven.snapshot</id>
<name>Spring Framework Maven Snapshot Repository</name>
<url>http://repo.spring.io/snapshot/</url>
</repository>
</repositories>

You can see the new logging about the failure as per 6e21b19999e967e4f4933bfda10b7a85206def34.
Failed to introspect annotations on class com.example.demolib.DemoAutoConfiguration$DemoWebMvcConfiguration: java.lang.NoClassDefFoundError: org/springframework/web/reactive/config/WebFluxConfigurer
@bono007, that's great news!
I'm glad to hear that solved the issue for you, and thanks for the detailed feedback. 馃憤
In light of that, I will close this issue and open a new issue to address potential regressions due to lack of use of AnnotationUtils.handleIntrospectionFailure(). See #24188.