I have project with aspect that automatically logs input and output of public methods, if class is annotated with @AutomaticLogger. My controller uses a bean that implements an interface, which makes it JDK proxy, as I understand. When aspect is enabled, it becomes a CGLIB bean, and CGLIB-enhanced code is missing in native image.

Attempt to run generated image with aspect ends with error:

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: org.example.demo.service.ServiceImpl$$SpringCGLIB$$0

Problem can be reproduced with this sample project.

Comment From: sbrannen

EDIT: my analysis below is incorrect, but I provide an updated analysis in later comments.

My controller uses a bean that implements an interface, which makes it JDK proxy, as I understand. When aspect is enabled, it becomes a CGLIB bean

I haven't run your sample application, but that is the expected behavior.

Your Controller is a class that does not implement any interface, and it is annotated with @AutomaticLogger which, in conjunction with your LoggingAspect, causes a CGLIB proxy to be created in order to apply the advice to your Controller class.

As for why you are encountering that exception, someone from the team will have to take a closer look to assess the cause of that.

Comment From: fenuks

In my example Controller doesn't implement any interface, but in a project where I see this error, all controllers implement interfaces generated from an openapi specification.

I wonder if spring-boot-maven-plugin shouldn't output CGLIB proxy for service implementation while running process-aot? It seems that all information that is needed is available at static level?

Comment From: sbrannen

Hi @fenuks,

Thanks for the feedback.

It seems there are a few things we need to sort out...


What exactly are you trying to achieve with the following pointcut expression?

@Around("within(org.example.demo..*) && execution(public * *(..)) && @target(org.example.demo.aspect.AutomaticLogger)")

In your sample application, do you expect public methods to be advised in Controller, in ServiceImpl, or in Controller and ServiceImpl?


From what I can tell, your pointcut is too broad: it also attempts to match against the Config class.

Thus, I would recommend modifying the pointcut to match only against types in subpackages of org.example.demo and moving your Controller to a subpackage if you also want Controller to be advised.

I believe you should be able to achieve that like this:

@Around("within(org.example.demo.*..*) && execution(public * *(..)) && @target(org.example.demo.aspect.AutomaticLogger)")

My controller uses a bean that implements an interface, which makes it JDK proxy, as I understand. When aspect is enabled, it becomes a CGLIB bean, and CGLIB-enhanced code is missing in native image.

Spring Boot configures proxying of target classes (using CGLIB) by default for AspectJ.

If you want to use dynamic proxies (interface based) by default, add the following to your application.properties (or YAML) file.

spring.aop.proxy-target-class=false

By making changes along the lines of what I suggested above plus annotating ServiceImpl with @AutomaticLogger, I was able to get your sample application working in a native image (with org.example.demo.service.ServiceImpl.getConstant() advised by your aspect), but I'd appreciate it if you could answer my questions so that we can ensure we have everything covered.

Thanks,

Sam

p.s., as a side note, we generally recommend @Configuration(proxyBeanMethods = false) and @SpringBootApplication(proxyBeanMethods = false) for use in a native image to avoid unnecessary proxying of @Configuration classes.

Comment From: fenuks

Hi @sbrannen, thank you for your answer and suggestions!

In your sample application, do you expect public methods to be advised in Controller, in ServiceImpl, or in Controller and ServiceImpl?

I want to advice every class within my project iff it is annotated with @AutomaticLogger. In this sample, I expect only Controller.java be affected.

From what I can tell, your pointcut is too broad: it also attempts to match against the Config class.

Thus, I would recommend modifying the pointcut to match only against types in subpackages of org.example.demo and moving your Controller to a subpackage if you also want Controller to be advised.

I will have to read aspect documentation again because I believed that only classes that are only explicitly marked with @AutomaticLogger will be affected. That's partially true, because ServiceImpl becomes CGLIB proxy, but no logging will be done unless it is marked with @AutomaticLogger annotation. In that sense, it works as expected, but ideally, ServiceImpl would be not advised at all, because it is missing annotation required by the aspect.

I will check if I uses of the aspect will allow me to further reduce its scope to subpackages.

Spring Boot configures proxying of target classes (using CGLIB) by default for AspectJ.

If you want to use dynamic proxies (interface based) by default, add the following to your application.properties (or YAML) file.

ini spring.aop.proxy-target-class=false

I tried that, but I then got an error I didn't understand back then, but now I do, thanks to your insights.

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'clockConfiguration': The program tried to reflectively access the proxy class inheriting [org.springframework.context.annotation.ConfigurationClassEnhancer$EnhancedConfiguration, org.springframework.aop.SpringProxy, org.springframework.aop.framework.Advised, org.springframework.core.DecoratingProxy] without it being registered for runtime reflection. Add [org.springframework.context.annotation.ConfigurationClassEnhancer$EnhancedConfiguration, org.springframework.aop.SpringProxy, org.springframework.aop.framework.Advised, org.springframework.core.DecoratingProxy] to the dynamic-proxy metadata to solve this problem. Note: The order of interfaces used to create proxies matters. See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#dynamic-proxy for help.

clockConfiguration is plain @Configuration class, it doesn't implement any interface, but it is also affected by aspect even though it has no @AutomaticLogger mark, so as I understand, forcing using dynamic proxy with current aspect behaviour won't work.

By making changes along the lines of what I suggested above plus annotating ServiceImpl with @AutomaticLogger, I was able to get your sample application working in a native image (with org.example.demo.service.ServiceImpl.getConstant() advised by your aspect), but I'd appreciate it if you could answer my questions so that we can ensure we have everything covered.

Yes, I think we have everything covered. I have now a good understanding of the underlying issue.

One thing that is not perfectly clear to me: even if my aspect advices some classes without visible effect because no logging will be done due to missing annotation, is it expected behaviour of the spring-boot-maven-plugin to not save CGLIB proxies created by aspect to the disk for the graalvm when spring.aop.proxy-target-class: true, or it can be considered a bug? I couldn't find any information if Ahead-of-Time Processing works with custom aspects.

p.s., as a side note, we generally recommend @Configuration(proxyBeanMethods = false) for use in a native image to avoid unnecessary proxying of @Configuration classes.

Will do, thank you very much!

Comment From: sbrannen

Hi @sbrannen, thank you for your answer and suggestions!

You're very welcome!

I want to advice every class within my project iff it is annotated with @AutomaticLogger. In this sample, I expect only Controller.java be affected.

OK, but then the methods to be proxied in Controller need to be public; otherwise, the pointcut needs to be modified to ignore the method's visibility.

I will have to read aspect documentation again because I believed that only classes that are only explicitly marked with @AutomaticLogger will be affected. That's partially true, because ServiceImpl becomes CGLIB proxy, but no logging will be done unless it is marked with @AutomaticLogger annotation. In that sense, it works as expected, but ideally, ServiceImpl would be not advised at all, because it is missing annotation required by the aspect.

To be honest, I was also a bit puzzled by that. I also assumed that only beans annotated with @AutomaticLogger would be proxied, but perhaps the use of @target causes that as a side effect.

In any case, I'll likely try to clarify that with @jhoeller next week.

I will check if I uses of the aspect will allow me to further reduce its scope to subpackages.

Sounds like a good plan.

I tried that, but I then got an error I didn't understand back then, but now I do, thanks to your insights.

👍

Yes, I think we have everything covered. I have now a good understanding of the underlying issue.

Great.

One thing that is not perfectly clear to me: even if my aspect advices some classes without visible effect because no logging will be done due to missing annotation, is it expected behaviour of the spring-boot-maven-plugin to not save CGLIB proxies created by aspect to the disk for the graalvm when spring.aop.proxy-target-class: true, or it can be considered a bug? I couldn't find any information if Ahead-of-Time Processing works with custom aspects.

The cause of that behavior is a configuration error that is specific to AOT processing. Although it's typically fine for your @Bean method to declare its return type as Service when running in standard JVM mode, it is not sufficient for AOT processing. If you change the return type of your @Bean service() method to ServiceImpl, you should find that Spring saves the generated ServiceImpl$$SpringCGLIB$$0.class file to disk with the default Spring Boot configuration (i.e., spring.aop.proxy-target-class=true). The reason is that Spring's AOT support can deduce that a CGLIB proxy can be generated for ServiceImpl (without actually invoking the service() method), but it cannot deduce that solely based on the Service interface type and effectively assumes it has to generate dynamic interface-based proxy instead.

That is indirectly documented in the Expose The Most Precise Bean Type section of the reference manual.

Comment From: sbrannen

To be honest, I was also a bit puzzled by that. I also assumed that only beans annotated with @AutomaticLogger would be proxied, but perhaps the use of @target causes that as a side effect.

Based on that hunch, I determined that switching to @within makes it work like we'd expect (i.e., it only proxies types that are actually annotated with @AutomaticLogger).

@Around("within(org.example.demo.*..*) && execution(public * *(..)) && @within(org.example.demo.aspect.AutomaticLogger)")

See if that addresses the issue for you!

Comment From: fenuks

The cause of that behavior is a configuration error that is specific to AOT processing. Although it's fine for your @Bean method to declare its return type as Service when running in standard JVM mode, it is not sufficient for AOT processing. If you change the return type of your @Bean service() method to ServiceImpl, you should find that Spring saves the generated ServiceImpl$$SpringCGLIB$$0.class file to disk with the default Spring Boot configuration (i.e., spring.aop.proxy-target-class=true). The reason is that Spring's AOT support can deduce that a CGLIB proxy can be generated for ServiceImpl (without actually invoking the service() method), but it cannot deduce that solely based on the Service interface type and effectively assumes it has to generate dynamic interface-based proxy instead.

I didn't know that it is advisable to declare beans with concrete types. Will keep that in mind and perhaps it's time to read documentation again.

I think my situation is related, but a bit different. When aspect is missing, then ServiceImpl is dynamic proxy, because bean method returns interface, just as you explained. If aspect is present, though, it CGLIB-fies selected classes. In theory I guess, AOP processing could look at aspect and know which classes need to be advised in conjunction with bean definition processing?

Based on that hunch, I determined that switching to @within makes it work like we'd expect (i.e., it only proxies types that are actually annotated with @AutomaticLogger).

java @Around("within(org.example.demo.*..*) && execution(public * *(..)) && @within(org.example.demo.aspect.AutomaticLogger)")

See if that addresses the issue for you!

It does, thank you very much again! Native version is now up and running with aspect enabled. ;)

Comment From: sbrannen

I didn't know that it is advisable to declare beans with concrete types. Will keep that in mind and perhaps it's time to read documentation again.

Yes, indeed. Regarding AOT support, it's always advisable to read the latest documentation for tips and best practices.

I think my situation is related, but a bit different. When aspect is missing, then ServiceImpl is dynamic proxy, because bean method returns interface, just as you explained. If aspect is present, though, it CGLIB-fies selected classes.

Your analysis is not quite correct.

  • If there's no aspect, ServiceImpl is not proxied at all.
  • With your original aspect pointcut and Spring Boot's default config, the service() bean was proxied with CGLIB on the JVM but with a JDK Dynamic Proxy in AOT mode (and hence in a native image). This is made evident by the fact that proxy-config.json was generated with an entry for org.example.demo.service.Service.
  • With Spring Boot's default config and having the service() method return ServiceImpl, the service() bean is proxied with CGLIB both on the JVM and in AOT mode. This is made evident by the fact that proxy-config.json is not generated with an entry for org.example.demo.service.Service, and there is instead a generated ServiceImpl$$SpringCGLIB$$0.class saved to disk.

In theory I guess, AOP processing could look at aspect and know which classes need to be advised in conjunction with bean definition processing?

That's actually what Spring AOP does when processing the ApplicationContext ahead of time (AOT). When the service() bean method declares that it returns a Service, the BeanDefinition says it's a Service, because it's impossible to know that the actual object returned from that @Bean method will be a ServiceImpl without invoking the method. And Spring's AOT support does not invoke the @Bean method during AOT processing.

See if that addresses the issue for you!

It does, thank you very much again! Native version is now up and running with aspect enabled. ;)

Awesome! I'm very glad to hear that.

Thanks for the feedback.

In light of the above, I am closing this issue.

Comment From: sbrannen

  • With your original aspect pointcut and Spring Boot's default config, the service() bean was proxied with CGLIB on the JVM but with a JDK Dynamic Proxy in AOT mode (and hence in a native image). This is made evident by the fact that proxy-config.json was generated with an entry for org.example.demo.service.Service.

  • With Spring Boot's default config and having the service() method return ServiceImpl, the service() bean is proxied with CGLIB both on the JVM and in AOT mode. This is made evident by the fact that proxy-config.json is not generated with an entry for org.example.demo.service.Service, and there is instead a generated ServiceImpl$$SpringCGLIB$$0.class saved to disk.

In order to analyze that myself, I had two modified versions of your original sample application, and I pushed them to a fork of your repository so that you (and others) can experiment/verify on your own.

  • CGLIB Proxies: https://github.com/sbrannen/sb-native-image-aspect-error/commit/52006da398cc5c8710892bc3b3cdc51042415b47
  • Dynamic Proxies: https://github.com/sbrannen/sb-native-image-aspect-error/commit/56aaea61714080e99648dca6a3938d755d01bd0b

In SmokeTests, you'll see that I verified what was proxied as well as how it was proxied, and those tests pass on the JVM as well as in a native image.

Regards,

Sam

Comment From: fenuks

Thank you again for a detailed explanation and updated example. Both are very appreciated, I've learned a lot!