Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotation scanning in enclosing class hierarchy results in NoClassDefFound #24136

Closed
onobc opened this issue Nov 8, 2019 · 12 comments
Closed
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug
Milestone

Comments

@onobc
Copy link
Contributor

onobc commented Nov 8, 2019

Version: 2.2.[0|1].RELEASE

Context:

  • Custom starter library has webflux and webmvc at "provided" scope
  • Custom starter has auto-configuration that has a conditional nested configuration on each tech
  • Consuming webmvc based application includes starter (has no webflux dep)

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

Starter snippet pom.xml

<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>

Starter auto-config

@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()");
        }
    }
}

Consuming app pom.xml snippet

<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.

@onobc
Copy link
Contributor Author

onobc commented Nov 8, 2019

Custom starter

demolib.zip

Consuming app

demo.zip

@OLPMO
Copy link
Contributor

OLPMO commented Nov 14, 2019

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.

@onobc
Copy link
Contributor Author

onobc commented Nov 26, 2019

@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.

@philwebb philwebb transferred this issue from spring-projects/spring-boot Dec 4, 2019
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 4, 2019
@rstoyanchev rstoyanchev added in: core Issues in core modules (aop, beans, core, context, expression) type: regression A bug that is also a regression and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 9, 2019
@rstoyanchev rstoyanchev added this to the 5.2.3 milestone Dec 9, 2019
@rstoyanchev
Copy link
Contributor

@jhoeller, this does look like a regression, so I've scheduled tentatively for 5.2.3.

@sbrannen
Copy link
Member

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 #24136 (comment), 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?

@sbrannen
Copy link
Member

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.

@sbrannen
Copy link
Member

sbrannen commented Dec 10, 2019

In any case, AnnotatedElementUtils.searchWithFindSemantics() used to handle all exceptions by invoking AnnotationUtils.handleIntrospectionFailure():

AnnotationUtils.handleIntrospectionFailure() was removed in b91ccf0 and added back as a private method in 37255af. 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?

@philwebb
Copy link
Member

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.

@sbrannen
Copy link
Member

I wonder if the TYPE_HIERARCHY_AND_ENCLOSING_CLASSES should 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.

sbrannen added a commit that referenced this issue Dec 10, 2019
…rategy

Prior to this commit, the enclosing class was always eagerly loaded
even if the annotation search strategy was not explicitly
TYPE_HIERARCHY_AND_ENCLOSING_CLASSES.

See gh-24136
@sbrannen sbrannen self-assigned this Dec 10, 2019
@sbrannen sbrannen changed the title Starter w/ conditional configurations on both WebFluxConfigurer and WebMvcConfigurer cause NoClassDefFound Annotation scanning in enclosing class hierarchy results in NoClassDefFound Dec 10, 2019
@sbrannen sbrannen added type: bug A general bug and removed type: regression A bug that is also a regression labels Dec 10, 2019
sbrannen added a commit that referenced this issue Dec 10, 2019
…defensive

Prior to this commit, when searching for annotations using the
TYPE_HIERARCHY_AND_ENCLOSING_CLASSES strategy an exception could be
thrown while attempting to load the enclosing class (e.g., a
NoClassDefFoundError), thereby halting the entire annotation scanning
process.

This commit makes this search strategy defensive by logging exceptions
encountered while processing the enclosing class hierarchy instead of
allowing the exception to halt the entire annotation scanning process.

The exception handling is performed by
AnnotationUtils.handleIntrospectionFailure() which only allows an
AnnotationConfigurationException to propagate.

See gh-24136
@sbrannen
Copy link
Member

OK, I have made the annotation scanning algorithm a bit more relaxed for enclosing class hierarchies in 16ed7e2 and 6e21b19.

@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.

@onobc
Copy link
Contributor Author

onobc commented Dec 11, 2019

@sbrannen - nice work!

Yep - that does it. I ensured I could still reproduce it.

Screen Shot 2019-12-10 at 8 06 29 PM

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>

Screen Shot 2019-12-10 at 8 00 09 PM

You can see the new logging about the failure as per 6e21b19.

Failed to introspect annotations on class com.example.demolib.DemoAutoConfiguration$DemoWebMvcConfiguration: java.lang.NoClassDefFoundError: org/springframework/web/reactive/config/WebFluxConfigurer

@sbrannen
Copy link
Member

sbrannen commented Dec 11, 2019

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

6 participants