ClassCastException when running in AOT mode applying aspect to @RefreshScope bean
Was referred from https://github.com/spring-projects/spring-boot/issues/34433
When running an AOT build of an application with a @RefreshScope bean that has an aspect applied to it, the application fails during runtime startup with the following stack trace.
I have created a reproducer at https://github.com/justin-tay/spring-boot-refresh-aspect-issue which demonstrates the issue.
In my actual application both the @RefreshScope RestController and the aspect are being contributed by two separate libraries so I'm not sure if I'm doing something wrong.
Running mvn spring-boot:run shows that it starts up normally.
Running mvn clean package -Pnative and then java -Dspring.aot.enabled=true -jar target/demo-0.0.1-SNAPSHOT.jar shows the exception.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoController': Unexpected AOP exception
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:917) ~[spring-context-6.0.5.jar!/:6.0.5]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:584) ~[spring-context-6.0.5.jar!/:6.0.5]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.0.3.jar!/:3.0.3]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[spring-boot-3.0.3.jar!/:3.0.3]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) ~[spring-boot-3.0.3.jar!/:3.0.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[spring-boot-3.0.3.jar!/:3.0.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1304) ~[spring-boot-3.0.3.jar!/:3.0.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1293) ~[spring-boot-3.0.3.jar!/:3.0.3]
at com.example.demo.DemoApplication.main(DemoApplication.java:10) ~[classes!/:0.0.1-SNAPSHOT]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) ~[demo-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
at org.springframework.boot.loader.Launcher.launch(Launcher.java:95) ~[demo-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) ~[demo-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:65) ~[demo-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
Caused by: org.springframework.aop.framework.AopConfigException: Unexpected AOP exception
at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:222) ~[spring-aop-6.0.5.jar!/:6.0.5]
at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:158) ~[spring-aop-6.0.5.jar!/:6.0.5]
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[spring-aop-6.0.5.jar!/:6.0.5]
at org.springframework.aop.scope.ScopedProxyFactoryBean.setBeanFactory(ScopedProxyFactoryBean.java:115) ~[spring-aop-6.0.5.jar!/:6.0.5]
at org.springframework.cloud.context.scope.GenericScope$LockedScopedProxyFactoryBean.setBeanFactory(GenericScope.java:448) ~[spring-cloud-context-4.0.1.jar!/:4.0.1]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeAwareMethods(AbstractAutowireCapableBeanFactory.java:1773) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1740) ~[spring-beans-6.0.5.jar!/:6.0.5]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.0.5.jar!/:6.0.5]
... 23 common frames omitted
Caused by: java.lang.ClassCastException: class org.springframework.aop.framework.CglibAopProxy$SerializableNoOp cannot be cast to class org.springframework.cglib.proxy.Dispatcher (org.springframework.aop.framework.CglibAopProxy$SerializableNoOp and org.springframework.cglib.proxy.Dispatcher are in unnamed module of loader org.springframework.boot.loader.LaunchedURLClassLoader @3eb07fd3)
at com.example.demo.api.DemoController$$SpringCGLIB$$0.setCallbacks(<generated>) ~[classes!/:0.0.1-SNAPSHOT]
at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:91) ~[spring-aop-6.0.5.jar!/:6.0.5]
at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:213) ~[spring-aop-6.0.5.jar!/:6.0.5]
... 30 common frames omitted
After debugging I can somewhat see the sequence of events but I don't really know how this can be fixed.
During a normal run (-Dspring.aot.enabled=false) the proxy generation follows the following sequence
-
org.springframework.cloud.context.scope.GenericScope$LockedScopedProxyFactoryBean.setBeanFactoryruns first -
org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.postProcessAfterInitializationruns next due to the bean being initialized with the aspect
Due the AOT compilation run the proxy generation follows the following sequence
-
org.springframework.context.aot.ApplicationContextAotGeneratorcausesorg.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.buildProxyto run -> writesDemoController$$SpringCGLIB$$0.class -
org.springframework.context.aot.ApplicationContextAotGeneratorcausesorg.springframework.cloud.context.scope.GenericScope$LockedScopedProxyFactoryBean.setBeanFactoryto run -> writesDemoController$$SpringCGLIB$$1.class
During the AOT run (-Dspring.aot.enabled=true) the proxy generation seems to follow the normal sequence.
-
org.springframework.cloud.context.scope.GenericScope$LockedScopedProxyFactoryBean.setBeanFactoryruns first and attempts to useDemoController$$SpringCGLIB$$0.classbut the signature of the callbacks isn't correct as the correct one is inDemoController$$SpringCGLIB$$1.classand throws
I'm not entirely sure but I think that this can be fixed if org.springframework.cloud.context.scope.GenericScope implements org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor.
https://github.com/spring-projects/spring-framework/blob/c9aba1eaad3ce0bcf68b09305a61d67c942d3660/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java#L421
same problem, wait for update. AOT and native image support is not available
Hello, @justin-tay. Thanks for reporting the issue and providing the sample. We'll work on the fix.
@PorcoRosso5 the link you've provided is for SC Config and it just says it doesn't work with legacy bootstrap mode. Otherwise, the client has been supported from 2022.0.0 and the server support has been introduced from 2023.0.0.
@justin-tay, refresh scope is not supported with AOT/ native, but it would be better if an application containing @RefreshScope-annotated beans worked when spring.cloud.refresh.enabled is set to false, so that same code can be used, with just the prop change. Will look into it.
A more in-detail analysis based on the provided sample:
JVM runtime
As the context is being refreshed, configuration bean definition processing takes place, triggering scanning for candidates. As ContextAotProcessor runs performAotProcessing(...), ApplicationContextGenerator’s processAheadOfTime(...) method is called, in turn calling refreshForAotProcessing(...) on GenericApplicationContext. demoController is found, and since it has ScopedProxyMode.TARGET_CLASS, ScopedProxyCreator.createScopedProxy(...) is triggered.
While finishing BeanFactory initialisation, preInstanatiateSingletons() is called and, consequently, getSingleton(...) and createBean(...) methods for demoController. Since it’s a ScopedProxyFactoryBean, and therefore, BeanFactoryAware, setBeanFactory(...) is called on it, and setBeanFactory(...) in ScopedProxyFactoryBean finishes its execution with proxy creation. That’s when the CGLiB proxy with corresponding file indexed $$0 gets created.
Subsequently, finishRefresh() is called and ContextRefreshedEvent is published and RefreshScope is initialised, which includes context.getBean(...) being called for every bean within the scope, including scopedTarget.demoController. As the bean is post-processed during after initialisation by AbstractAutoProxyCreator, the aspect-specific interceptors are found, and the proxy creation is carried out through a call to createProxyClass(). That’s when the CGLIB proxy with corresponding file indexed $$1.
AOT processing
As ContextAotProcessor runs performAotProcessing(...), ApplicationContextGenerator’s processAheadOfTime(...) method is called, in turn calling refreshForAotProcessing(...) on GenericApplicationContext.
That triggers post-processing of BeanDefinitionRegistry and processing configuration classes, including scanning for candidates. demoController is found, and since it has ScopedProxyMode.TARGET_CLASS, ScopedProxyCreator.createScopedProxy(...) is triggered. targetBeanName is evaluated and scopedTarget.demoController bean definition is registered, and the bean definition for the proxied demoController bean is registered as well.
Up until here the order looks right.
As the last step within refreshForAotProcessing(...), predetermineBeanTypes(...) is called in determineBeanType() in AutoProxyCreator runs for scopedTarget.demoController. As part of this process, interceptors for that bean are searched with getAdvicesAndAdvisorsForBean(...) method, which recognises the existence of DemoControllerAspect. Since interceptors are found, the type determination will be done for proxy type, but in order to verify the relevant proxy type, actual proxy creation is attempted through a call to createProxyClass(). That’s when the CGLIB proxy with corresponding file indexed $$0, so this seems to be the root of the problem. (Maybe there would be a way to reason about the proxy type without finalising the proxy creation?)
Later on, the predetermineBeanTypes(...) execution reaches the demoController (without scopedTarget prefix) bean definition. getType(...) is called for that bean and, consequently, getSingleton(...) and createBean(...). Since it’s a ScopedProxyFactoryBean, and therefore, BeanFactoryAware, setBeanFactory(...) is called on it, and setBeanFactory(...) in ScopedProxyFactoryBean finishes its execution with proxy creation. That’s when the CGLiB proxy with corresponding file indexed $$1 gets created.
AOT-processed jar execution
This phase executes in an order akin to the one from a simple JVM runtime execution. As AotApplicationContextInitializer runs, both scopedTarget.demoController and demoController bean definitions are registered. While finishing BeanFactory initialisation, preInstanatiateSingletons() is called and, consequently, getSingleton(...) and createBean(...) for demoController. In keeping with the AOT generation run, setBeanFactory(...) in ScopedProxyFactoryBean is called for the bean, triggering proxy creation. Since the proxy files are matched per class and then by index, and the proxy creation order for the DemoController class is different, wrong file gets picked (based on the order of processing during AOT generation, the corresponding one would be indexed $$1, but since $$0 has not yet been used during the execution, that one is picked), the callback types do not match the file and we get:
java.lang.ClassCastException: class org.springframework.aop.framework.CglibAopProxy$SerializableNoOp cannot be cast to class org.springframework.cglib.proxy.Dispatcher (org.springframework.aop.framework.CglibAopProxy$SerializableNoOp and org.springframework.cglib.proxy.Dispatcher are in unnamed module of loader org.springframework.boot.loader.launch.LaunchedClassLoader @6e2c634b)
This is not specifically related to Spring Cloud, but rather is a Framework issue. The same will happen if any other Scope with ScopedProxyMode.TARGET_CLASS is applied. Can be reproduced in the same demo with @ApplicationScope annotation used instead of @RefreshScope.
@justin-tay, I have further verified the issue. I will report the issue to the Framework team (see comments above for the details), however I also wonder about what your use case is here. As documented, we don't support refresh scope in any form for AOT-processed and native applications and you need to build and run them with spring.cloud.refresh.enabled set to false. On the other hand, if you attempt using a @RefreshScope-annotated bean with that prop disabled, the app will fail, since this has never been supported (independent of any AOT-specific changes). The only way you could use that app in a regular JVM runtime with spring.cloud.refresh.enabled=false is if you never call that @RefreshScope-annotated controller, so, in any case, it's not going to be a good practice (or very useful) to have @RefreshScope-annotated beans in any AOT-processed app.
Just to elaborate a bit on the scenario. There was a library that had some @AutoConfiguration that was creating beans that were annotated with @RefreshScope. Separately there was another library contributing an aspect. My application was just using these two libraries as dependencies so it was difficult to remove the @RefreshScope other than by copying the @AutoConfiguration beans and removing the @RefreshScope as I didn't actually need the beans to be refreshable. It would just have been nice to have a means to continue using the @AutoConfiguration as is. In the end I managed to get changes merged into the upstream library to have two separate sets of configuration depending on whether spring.cloud.refresh.enabled was true or false.
Thanks for taking the time to look into this!
Related issue opened in Framework: https://github.com/spring-projects/spring-framework/issues/32669.