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

CGLIB proxy classes are no longer cached properly #31238

Closed
danthe1st opened this issue Sep 14, 2023 · 5 comments
Closed

CGLIB proxy classes are no longer cached properly #31238

danthe1st opened this issue Sep 14, 2023 · 5 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) theme: aot An issue related to Ahead-of-time processing type: regression A bug that is also a regression
Milestone

Comments

@danthe1st
Copy link

danthe1st commented Sep 14, 2023

Affects:

  • Spring 6.0.10 / Spring Boot 3.0.10
    • it also works when overwriting the Spring version to 6.0.11 using the property <spring-framework.version>6.0.12</spring-framework.version>
  • Spring 6.0.11 / Spring Boot 3.1.3
    • See branch 3.1 in the reproducer, build log here

When a Spring project using @Transactional and @DirtiesContext is running tests in native mode, it fails with an error like this:

CGLIB runtime enhancement not supported on native image. Make sure to include a pre-generated class on the classpath instead: io.github.danthe1st.spring_demo.SomethingWithTransactional$$SpringCGLIB$$6

Steps to reproduce:

  • Create a project with Spring Initializr
    • Spring Data JPA
    • GraalVM Native-Image support
  • Add H2 as a test database
  • Create a @Component with a @Transactional method.
  • Create a @SpringBootTest with a test method annotated with @DirtiesContext
  • Run the tests in native mode using mvn test -PnativeTest

This results in an error like the following when running the tests:

=> java.lang.IllegalStateException: Failed to load ApplicationContext for [AotMergedContextConfiguration@579f911b testClass = io.github.danthe1st.spring_demo.TestWithDirtiesContext, contextInitializerClass = io.github.danthe1st.spring_demo.TestWithDirti
   org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:143)
   org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:127)
   org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependenciesInAotMode(DependencyInjectionTestExecutionListener.java:148)
   org.springframework.test.context.support.DependencyInjectionTestExecutionListener.beforeTestMethod(DependencyInjectionTestExecutionListener.java:118)
   org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:288)
   [...]
   Suppressed: java.lang.IllegalStateException: Failed to load ApplicationContext for [AotMergedContextConfiguration@2ebc43f8 testClass = io.github.danthe1st.spring_demo.TestWithDirtiesContext, contextInitializerClass = io.github.danthe1st.spring_demo.T
     org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:143)
     org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:127)
     org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener.afterTestMethod(WebDriverTestExecutionListener.java:42)
     org.springframework.test.context.TestContextManager.afterTestMethod(TestContextManager.java:440)
     org.springframework.test.context.junit.jupiter.SpringExtension.afterEach(SpringExtension.java:206)
     [...]
     Suppressed: java.lang.IllegalStateException: Failed to load ApplicationContext for [AotMergedContextConfiguration@6be801a testClass = io.github.danthe1st.spring_demo.TestWithDirtiesContext, contextInitializerClass = io.github.danthe1st.spring_demo.
       org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:143)
       org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:127)
       org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener.afterTestMethod(MockMvcPrintOnlyOnFailureTestExecutionListener.java:39)
       [...]
     Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'somethingWithTransactional': Unexpected AOP exception
       org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:605)
       org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
       org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
       org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
       org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
       [...]
     Caused by: org.springframework.aop.framework.AopConfigException: Unexpected AOP exception
       org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:228)
       org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:155)
       org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
       org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.buildProxy(AbstractAutoProxyCreator.java:517)
       org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:464)
       [...]
     Caused by: java.lang.UnsupportedOperationException: CGLIB runtime enhancement not supported on native image. Make sure to include a pre-generated class on the classpath instead: io.github.danthe1st.spring_demo.SomethingWithTransactional$$SpringCGLI
       org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
       org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:575)
       org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.lambda$new$1(AbstractClassGenerator.java:107)
       org.springframework.cglib.core.internal.LoadingCache.lambda$createEntry$1(LoadingCache.java:52)
       java.base@20.0.2/java.util.concurrent.FutureTask.run(FutureTask.java:317)
       [...]
     Suppressed: java.lang.IllegalStateException: Failed to load ApplicationContext for [AotMergedContextConfiguration@42dfa7e6 testClass = io.github.danthe1st.spring_demo.TestWithDirtiesContext, contextInitializerClass = io.github.danthe1st.spring_demo
       org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:143)
       org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:127)
       org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener.afterTestMethod(MockRestServiceServerResetTestExecutionListener.java:40)
       [...]
     Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'somethingWithTransactional': Unexpected AOP exception
       org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:605)
       org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
       org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
       org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
       org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
       [...]
     Caused by: org.springframework.aop.framework.AopConfigException: Unexpected AOP exception
       org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:228)
       org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:155)
       org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
       org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.buildProxy(AbstractAutoProxyCreator.java:517)
       org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:464)
       [...]
     Caused by: java.lang.UnsupportedOperationException: CGLIB runtime enhancement not supported on native image. Make sure to include a pre-generated class on the classpath instead: io.github.danthe1st.spring_demo.SomethingWithTransactional$$SpringCGLI
       org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
       org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:575)
       org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.lambda$new$1(AbstractClassGenerator.java:107)
       org.springframework.cglib.core.internal.LoadingCache.lambda$createEntry$1(LoadingCache.java:52)
       java.base@20.0.2/java.util.concurrent.FutureTask.run(FutureTask.java:317)
       [...]
   Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'somethingWithTransactional': Unexpected AOP exception
     org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:605)
     org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
     org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
     org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
     org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
     [...]
   Caused by: org.springframework.aop.framework.AopConfigException: Unexpected AOP exception
     org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:228)
     org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:155)
     org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
     org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.buildProxy(AbstractAutoProxyCreator.java:517)
     org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:464)
     [...]
   Caused by: java.lang.UnsupportedOperationException: CGLIB runtime enhancement not supported on native image. Make sure to include a pre-generated class on the classpath instead: io.github.danthe1st.spring_demo.SomethingWithTransactional$$SpringCGLIB$
     org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
     org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:575)
     org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.lambda$new$1(AbstractClassGenerator.java:107)
     org.springframework.cglib.core.internal.LoadingCache.lambda$createEntry$1(LoadingCache.java:52)
     java.base@20.0.2/java.util.concurrent.FutureTask.run(FutureTask.java:317)
     [...]
 Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'somethingWithTransactional': Unexpected AOP exception
   org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:605)
   org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
   org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
   org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
   org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
   [...]
 Caused by: org.springframework.aop.framework.AopConfigException: Unexpected AOP exception
   org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:228)
   org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:155)
   org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
   org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.buildProxy(AbstractAutoProxyCreator.java:517)
   org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:464)
   [...]
 Caused by: java.lang.UnsupportedOperationException: CGLIB runtime enhancement not supported on native image. Make sure to include a pre-generated class on the classpath instead: io.github.danthe1st.spring_demo.SomethingWithTransactional$$SpringCGLIB$$6
   org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
   org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:575)
   org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.lambda$new$1(AbstractClassGenerator.java:107)
   org.springframework.cglib.core.internal.LoadingCache.lambda$createEntry$1(LoadingCache.java:52)
   java.base@20.0.2/java.util.concurrent.FutureTask.run(FutureTask.java:317)
   [...]

Reproducer: https://github.com/danthe1st/spring-dirty-cglib
failed run: https://github.com/danthe1st/spring-dirty-cglib/actions/runs/6190254596/job/16806115554
build log as file: 1_build.txt

native-image version:

native-image 17.0.7 2023-04-18
GraalVM Runtime Environment Oracle GraalVM 17.0.7+8.1 (build 17.0.7+8-LTS-jvmci-23.0-b12)
Substrate VM Oracle GraalVM 17.0.7+8.1 (build 17.0.7+8-LTS, serial gc, compressed references)

This issue might be related to #30939 but it occurs in a different Spring version and seems to be caused by different things.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Sep 14, 2023
@danthe1st danthe1st changed the title CGLIB error with DirtiesContext and different test configurations CGLIB runtime enhancement not supported on native-image with Transactional and DirtiesContext Sep 14, 2023
@sbrannen sbrannen changed the title CGLIB runtime enhancement not supported on native-image with Transactional and DirtiesContext CGLIB runtime enhancement not supported on native-image with @Transactional and @DirtiesContext Sep 16, 2023
@sbrannen
Copy link
Member

Hi @danthe1st,

Thanks for raising the issue.

Before we investigate this, I would appreciate it if you could answer the following questions.

  1. Are you certain that the presence of @DirtiesContext causes this?
  2. What happens if you remove hibernate-enhance-maven-plugin from pom.xml (see also CGLIB runtime enhancement not supported on native image #30939 (comment))?

@sbrannen sbrannen added status: waiting-for-feedback We need additional information before we can continue theme: aot An issue related to Ahead-of-time processing labels Sep 16, 2023
@danthe1st
Copy link
Author

  1. Upon removing the @DirtiesContext, the tests succeeded in native mode. If I remember correctly, even the DirtiesContext.MethodMode.BEFORE_METHOD is necessary for reproducing the issue.
  2. I did this in another branch of my reproducer now. You can find the build log here As you can see, it still fails.

@sbrannen

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Sep 16, 2023
@sbrannen
Copy link
Member

sbrannen commented Sep 17, 2023

Thanks for trying that out and providing feedback, @danthe1st!

That's very helpful to know and made me think...

It's likely not directly related to @DirtiestContext but rather due to the fact that the test's ApplicationContext gets loaded twice, which results in two attempts to create a CGLIB proxy for your @Transactional component.

Based on that hunch, I have been able to reproduce this bug in a stand-alone integration test without GraalVM involved.

package org.springframework.test;

// imports ...

@SpringJUnitConfig
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD)
class TransactionalComponentTests {

	static Set<String> proxyClasses = new HashSet<>();

	@Autowired
	MyComponent component;

	@BeforeEach
	void trackProxyClass() {
		proxyClasses.add(component.getClass().getName());
	}

	@Test
	void test1() {
	}

	@Test
	void test2() {
	}

	@AfterAll
	static void checkProxiesCreated() {
		assertThat(proxyClasses)
			.singleElement()
			.isEqualTo("org.springframework.test.TransactionalComponentTests$MyComponent$$SpringCGLIB$$0");
	}


	@Configuration
	@Import(MyComponent.class)
	@EnableTransactionManagement
	static class Config {

		@Bean
		DataSourceTransactionManager transactionManager(DataSource dataSource) {
			return new DataSourceTransactionManager(dataSource);
		}

		@Bean
		DataSource dataSource() {
			return new EmbeddedDatabaseBuilder().generateUniqueName(true).build();
		}
	}

	@Component
	static class MyComponent {

		@Transactional
		void doWork() {
		}
	}

}

test1() and test2() both pass when run individually, but when the entire test class is run, the assertion in checkProxiesCreated() fails as follows.

Expected size: 1 but was: 2 in:
["org.springframework.test.TransactionalComponentTests$MyComponent$$SpringCGLIB$$1",
    "org.springframework.test.TransactionalComponentTests$MyComponent$$SpringCGLIB$$0"]

That should make it easier for us to debug the issue in the coming days.

@sbrannen
Copy link
Member

sbrannen commented Sep 17, 2023

That should make it easier for us to debug the issue in the coming days.

Out of curiosity, I performed some manual debugging to hone in on the underlying problem.

Adding a print statement to SpringNamingPolicy#getClassName() results in the following for the above test class.

attempt: org.springframework.test.TransactionalComponentTests$Config$$SpringCGLIB$$0
attempt: org.springframework.test.TransactionalComponentTests$Config$$SpringCGLIB$$1
attempt: org.springframework.test.TransactionalComponentTests$Config$$SpringCGLIB$$2
attempt: org.springframework.test.TransactionalComponentTests$MyComponent$$SpringCGLIB$$0
attempt: org.springframework.test.TransactionalComponentTests$MyComponent$$SpringCGLIB$$1

From that, we can see that 3 CGLIB proxy classes are generated for Config and 2 for MyComponent; whereas, we should ideally only be creating a single CGLIB proxy class for each of those.

We actually see that 3 CGLIB proxy classes are generated for Config even when loading the ApplicationContext only once (see #31272).

@sbrannen sbrannen added 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 Sep 17, 2023
@sbrannen sbrannen modified the milestones: 6.1.0-RC1, 6.0.13 Sep 17, 2023
@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label Sep 17, 2023
@sbrannen sbrannen self-assigned this Sep 18, 2023
@sbrannen sbrannen removed the status: feedback-provided Feedback has been provided label Sep 18, 2023
@sbrannen sbrannen changed the title CGLIB runtime enhancement not supported on native-image with @Transactional and @DirtiesContext CGLIB proxy classes are not cached across ApplicationContexts Sep 18, 2023
@sbrannen sbrannen changed the title CGLIB proxy classes are not cached across ApplicationContexts CGLIB proxy classes are not properly cached Sep 19, 2023
@sbrannen sbrannen changed the title CGLIB proxy classes are not properly cached CGLIB proxy classes are no longer cached properly Sep 20, 2023
@sbrannen
Copy link
Member

sbrannen commented Sep 20, 2023

This has been addressed in 6.0.x and main in 865fa33.

Feel free to try it out in the 6.0.13 and 6.1 RC1 snapshots.

sbrannen added a commit to sbrannen/spring-framework that referenced this issue Sep 22, 2023
This commit expands the scope of equality checks in the implementation
of equals() for PerTargetInstantiationModelPointcut to include all
fields instead of just the pointcut expression for the declared
pointcut.

See spring-projectsgh-31238
sbrannen added a commit that referenced this issue Sep 23, 2023
For equivalence, we only need to compare the preInstantiationPointcut
fields since they include the declaredPointcut fields. In addition, we
should not compare the aspectInstanceFactory fields since
LazySingletonAspectInstanceFactoryDecorator does not implement equals().

See gh-31238
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) theme: aot An issue related to Ahead-of-time processing type: regression A bug that is also a regression
Projects
None yet
Development

No branches or pull requests

3 participants