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.
- Are you certain that the presence of
@DirtiesContext
causes this? - What happens if you remove
hibernate-enhance-maven-plugin
frompom.xml
(see also https://github.com/spring-projects/spring-framework/issues/30939#issuecomment-1722278320)?
Comment From: danthe1st
- Upon removing the
@DirtiesContext
, the tests succeeded in native mode. If I remember correctly, even theDirtiesContext.MethodMode.BEFORE_METHOD
is necessary for reproducing the issue. - 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.