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.

Comment From: sbrannen

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 https://github.com/spring-projects/spring-framework/issues/30939#issuecomment-1722278320)?

Comment From: danthe1st

  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

Comment From: sbrannen

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.

Comment From: sbrannen

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

Comment From: sbrannen

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

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