spring-framework icon indicating copy to clipboard operation
spring-framework copied to clipboard

AOP support in native images, beyond simple method expressions

Open goafabric opened this issue 2 years ago • 3 comments

So this is a continuation of https://github.com/spring-projects-experimental/spring-native/issues/956 which is still an open issue with Boot 3 (RC2 + latest snapshot)

While simple methods expressions like this just work fine "execution(public * org.goafabric.calleeservice.aspect.TestComponent.*(..))"

More complex expressions, that e.g. leverage an custom anntotation, don't. To be more specific .. they work with console applications, but crash the moment wie have WEB on the classpath. Which somehow explains the error. Tried to set the jdk proxy interfaces that graalvm demands, but stopped after the 3rd roundtrip because this seems to be a neverending road ...

I've attached a simple example that works with non native, but crashes on bootstrap of the native image. What's also a little mind boggeling .. is that i have to register a simple hint for the Test Aspect. While the aot-smoke-tests don't ...

error.txt example.zip

goafabric avatar Nov 18 '22 12:11 goafabric

@goafabric thanks but that's not a Spring Boot concern. Moving to framework.

snicoll avatar Nov 18 '22 13:11 snicoll

  • Potentially a duplicate of #28711

sbrannen avatar Nov 18 '22 13:11 sbrannen

Thx for answering that fast .. insanely fast ...

https://github.com/spring-projects/spring-framework/issues/28711 is a little different

It at least explains why i had to set hints.reflection().registerType(TestAspect.class, MemberCategory.INVOKE_DECLARED_METHODS).

But with this the invocation works .. for all use cases as long as it is a simple console application.

Combine it with web and it crashes. So I guess this is a little different.

goafabric avatar Nov 18 '22 14:11 goafabric

That's an interesting one. We have:

@Component
@Aspect
public class TestAspect {

    @Around("@within(org.goafabric.calleeservice.aspect.TestAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            return joinPoint.proceed();
        } finally {
            final Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            System.err.println("aspects wrapped method : " + toString(method));
        }
    }
    // ...
}

The sample indeed fails with errors like:

Caused by: com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces [interface org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory, interface org.springframework.context.ResourceLoaderAware, interface org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory, interface org.springframework.boot.web.server.ConfigurableWebServerFactory] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:89) ~[na:na]
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.reflect.proxy.DynamicProxySupport.getProxyClass(DynamicProxySupport.java:171) ~[na:na]
        at [email protected]/java.lang.reflect.Proxy.getProxyConstructor(Proxy.java:47) ~[callee-service:na]
        at [email protected]/java.lang.reflect.Proxy.getProxyClass(Proxy.java:398) ~[callee-service:na]
        at org.springframework.util.ClassUtils.createCompositeInterface(ClassUtils.java:783) ~[na:na]
        at org.springframework.aop.aspectj.AspectJExpressionPointcut.getTargetShadowMatch(AspectJExpressionPointcut.java:432) ~[na:na]
        at org.springframework.aop.aspectj.AspectJExpressionPointcut.matches(AspectJExpressionPointcut.java:290) ~[na:na]
        at org.springframework.aop.support.AopUtils.canApply(AopUtils.java:251) ~[na:na]
        at org.springframework.aop.support.AopUtils.canApply(AopUtils.java:288) ~[na:na]
        at org.springframework.aop.support.AopUtils.findAdvisorsThatCanApply(AopUtils.java:320) ~[na:na]
        at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.findAdvisorsThatCanApply(AbstractAdvisorAutoProxyCreator.java:128) ~[callee-service:6.0.3]
        at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(AbstractAdvisorAutoProxyCreator.java:97) ~[callee-service:6.0.3]
        at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean(AbstractAdvisorAutoProxyCreator.java:78) ~[callee-service:6.0.3]
        at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:366) ~[callee-service:6.0.3]
        at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:318) ~[callee-service:6.0.3]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:435) ~[callee-service:6.0.3]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1754) ~[callee-service:6.0.3]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:599) ~[callee-service:6.0.3]
        ... 16 common frames omitted

So I began to add more proxy hints, but at some point I stopped because even with that hint configuration, that's not enough to make it work.

 hints.proxies()
                    .registerJdkProxy(OrderedFilter.class, Filter.class, BeanNameAware.class, EnvironmentAware.class,
                            EnvironmentCapable.class, ServletContextAware.class, InitializingBean.class, DisposableBean.class)
                    .registerJdkProxy(WebServerFactoryCustomizer.class, Ordered.class)
                    .registerJdkProxy(ApplicationContextAware.class, EnvironmentCapable.class, EnvironmentAware.class,
                            Servlet.class, ServletConfig.class, Serializable.class)
                    .registerJdkProxy(DispatcherServletPath.class, ServletContextInitializer.class, Ordered.class)
                    .registerJdkProxy(ErrorPageRegistrar.class, Ordered.class)
                    .registerJdkProxy(ViewResolver.class, Ordered.class, ServletContextAware.class, ApplicationContextAware.class)
                    .registerJdkProxy(ErrorViewResolver.class, Ordered.class)
                    .registerJdkProxy(ErrorAttributes.class, Ordered.class)
                    .registerJdkProxy(ErrorAttributes.class, HandlerExceptionResolver.class, Ordered.class)
                    .registerJdkProxy(WebMvcConfigurer.class, ServletContextAware.class)
                    .registerJdkProxy(ResourceLoaderAware.class, ApplicationContextAware.class, ServletContextAware.class)
                    .registerJdkProxy(ConfigurableTomcatWebServerFactory.class, ResourceLoaderAware.class,
                            ConfigurableServletWebServerFactory.class, ConfigurableWebServerFactory.class);

@jhoeller It looks like AspectJExpressionPointcut#getTargetShadowMatch tentatively create proxies for every bean in this try catch block. I am wondering about the performance impact on the JVM, and on native that means we would need to create proxy hints for every bean, leading to massive compatibility headaches and increased footprint. Is this something we could refine from your point of view?

sdeleuze avatar Jan 04 '23 14:01 sdeleuze

That composite interface creation is only really needed for default methods in interfaces and similar scenarios where interface-based method implementations are inherited rather than implemented locally. I've added a corresponding condition to reduce the creation of composite interfaces to actual method references in the current pointcut, that is, only when the pointcut actually refers to a non-overridden default method or the like. This should eliminate all of the common cases above.

jhoeller avatar Jan 09 '23 19:01 jhoeller

I confirm the example is now working as expected with method hints (hints.reflection().registerType(TestAspect.class, MemberCategory.INVOKE_DECLARED_METHODS);) which will be inferred via #28711.

sdeleuze avatar Jan 10 '23 07:01 sdeleuze

@sdeleuze thank you for finally having this fixed is there any kind of way i can try it out myself already ?

boot 3.0.2-snapshot still seems to rely on framework-core 6.0.3 and including framework-core 6.0.4-SNAPSHOT did not solve the issue

or do i just have to wait for a release ? :)

thx

goafabric avatar Jan 11 '23 11:01 goafabric

Thanks should go to @jhoeller for the fix ;-)

You can override the Framework version in Boot, otherwise just wait a bit, Spring Framework 6.0.4 has been released today and will be picked up by Boot shortly.

sdeleuze avatar Jan 11 '23 17:01 sdeleuze

yes i saw it on twitter yesterday evening, thx 4 letting me know and of course also to @jhoeller

goafabric avatar Jan 12 '23 07:01 goafabric

@sdeleuze I can happily report that everything is working now with boot 3.0.2-SNAPSHOT Even more complex applications finally including CircuitBreaker Annotation So thx again to both of you.

As a sugar on top .. would it be possible to get rid of the "hints.reflection().registerType" ? Because basically everything annotated with Aspect should qualify already qualify for reflection ?

Or .. "at least" as a fallback, at least have a working variant of "RegisterForReflectionBinding"

Then it would also be nice to have spring cloud resilience4j working out of the box. I opened an issue a while ago, that was just closed https://github.com/spring-cloud/spring-cloud-circuitbreaker/issues/159

And this basically needs " hints.reflection().registerType(io.github.resilience4j.spring6.circuitbreaker.configure.CircuitBreakerAspect.class, builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_METHODS));"

goafabric avatar Jan 15 '23 08:01 goafabric

Please create a related issue with me mentioned with a minimal reproducer and a detailed description of the type hint inference you would expect.

sdeleuze avatar Jan 16 '23 19:01 sdeleuze