Skip to content

Native image is missing CGLIB proxy when @Aspect is used #33491

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

Closed
fenuks opened this issue Sep 5, 2024 · 10 comments
Closed

Native image is missing CGLIB proxy when @Aspect is used #33491

fenuks opened this issue Sep 5, 2024 · 10 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: invalid An issue that we don't feel is valid theme: aot An issue related to Ahead-of-time processing

Comments

@fenuks
Copy link

fenuks commented Sep 5, 2024

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Sep 5, 2024
@snicoll snicoll transferred this issue from spring-projects/spring-boot Sep 5, 2024
@sbrannen
Copy link
Member

sbrannen commented Sep 5, 2024

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.

@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label Sep 5, 2024
@fenuks
Copy link
Author

fenuks commented Sep 5, 2024

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?

@sbrannen sbrannen self-assigned this Sep 6, 2024
@sbrannen
Copy link
Member

sbrannen commented Sep 6, 2024

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.

@sbrannen sbrannen added the status: waiting-for-feedback We need additional information before we can continue label Sep 6, 2024
@fenuks
Copy link
Author

fenuks commented Sep 6, 2024

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.

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!

@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 6, 2024
@sbrannen sbrannen added the theme: aot An issue related to Ahead-of-time processing label Sep 6, 2024
@sbrannen
Copy link
Member

sbrannen commented Sep 6, 2024

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.

@sbrannen
Copy link
Member

sbrannen commented Sep 6, 2024

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!

@fenuks
Copy link
Author

fenuks commented Sep 6, 2024

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

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

@sbrannen
Copy link
Member

sbrannen commented Sep 7, 2024

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.

@sbrannen sbrannen closed this as not planned Won't fix, can't repro, duplicate, stale Sep 7, 2024
@sbrannen sbrannen added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Sep 7, 2024
@sbrannen sbrannen changed the title Native image is missing CGLIB-enhanced proxy when aspect is used Native image is missing CGLIB proxy when @Aspect is used Sep 7, 2024
@sbrannen
Copy link
Member

sbrannen commented Sep 7, 2024

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

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

@fenuks
Copy link
Author

fenuks commented Sep 9, 2024

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

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) status: invalid An issue that we don't feel is valid theme: aot An issue related to Ahead-of-time processing
Projects
None yet
Development

No branches or pull requests

3 participants