After a discussion with @snicoll and @jhoeller insights, we found that it is possible to infer automatically JDK dynamic proxies required by Spring beans during the AOT processing.

Comment From: sdeleuze

@snicoll @jhoeller A draft implementation is available on gh-28980.

While transactional and cache-simple-jdk-proxy AOT smoke tests sample work without specific hints, validation one fails with:

Caused by: com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces [interface jakarta.validation.Validator, interface org.springframework.aop.SpringProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.core.DecoratingProxy] 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 com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:89) ~[na:na]
    at com.oracle.svm.reflect.proxy.DynamicProxySupport.getProxyClass(DynamicProxySupport.java:158) ~[na:na]
    at java.lang.reflect.Proxy.getProxyConstructor(Proxy.java:48) ~[validation:na]
    at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:1037) ~[validation:na]
    at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:126) ~[na:na]
    at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[na:na]
    at org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.buildLazyResolutionProxy(ContextAnnotationAutowireCandidateResolver.java:130) ~[na:na]
    at org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.getLazyResolutionProxyIfNecessary(ContextAnnotationAutowireCandidateResolver.java:54) ~[na:na]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1292) ~[validation:6.0.0-SNAPSHOT]
    at org.springframework.beans.factory.aot.BeanInstanceSupplier.resolveArgument(BeanInstanceSupplier.java:332) ~[na:na]
    at org.springframework.beans.factory.aot.BeanInstanceSupplier.resolveArguments(BeanInstanceSupplier.java:265) ~[na:na]
    at org.springframework.beans.factory.aot.BeanInstanceSupplier.get(BeanInstanceSupplier.java:208) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1225) ~[validation:6.0.0-SNAPSHOT]

Not sure yet why this one is not detected.

Comment From: sdeleuze

As discussed with Juergen, this is due to @Lazy used there and will likely require a refinement to be potentially supported.

Comment From: jhoeller

I've addressed the specific MethodValidationPostProcessor scenario through a setValidatorProvider(ObjectProvider<Validator>) method, with the suggestion to adapt the injection point declaration in Boot's ValidationAutoConfiguration accordingly.

Comment From: jhoeller

@sdeleuze for @Lazy detection on injection points, we are not going to get a complete picture through the bean type determination in refreshForAotProcessing. We'd have to fully initialize all beans for this since otherwise we won't hit all the - potentially nested - injection points. The key difference is that such lazy proxies are not created for managed bean instances themselves, they are rather locally created for adapting individual injection points in dependent beans.

Maybe we could cover the most common scenarios in our AOT configuration class processing, outside of the core container. As we are transforming each bean declaration into a programmatic bean definition, we could introspect the signatures of the factory methods, constructors and also setter methods that we're generating calls for - and automatically register a JDK proxy hint (or define a corresponding CGLIB subclass) for an @Lazy injection point type. Alternatively, we could also ask people to change every such injection point to ObjectProvider instead - which is generally the more efficient lazy access solution and therefore recommended in infrastructure code, but possibly not seen as convenient enough in application code.

Comment From: jhoeller

I've introduced a getLazyResolutionProxyClass mechanism in the AutowireCandidateResolver SPI, to be invoked for every parameter/field that we are triggering behind a specific bean definition. This works fine in a local test for both JDK proxies and CGLIB proxies. Hopefully it is straightforward to integrate into our AOT processing, @snicoll. For every Class returned, we should do something along the lines of the recent addition to refreshForAotProcessing:

    if (proxyType != null && Proxy.isProxyClass(proxyType)) {
        runtimeHints.proxies().registerJdkProxy(proxyType.getInterfaces());
    }

For CGLIB proxies, it should be sufficient to have triggered getLazyResolutionProxyClass to begin with, capturing the generated class underneath the covers.

Comment From: snicoll

getLazyResolutionProxyClass is integrated for @Autowired fields and methods, as well as on the constructor or factory method of any bean in the context.