I've encountered a use-case in which RuntimeHintsUtils.registerAnnotation isn't enough to work with the annotation in a native setting. I'm using this code:

RuntimeHintsUtils.registerAnnotation(hints, Endpoint.class);

where Endpoint is org.springframework.boot.actuate.endpoint.annotation.Endpoint:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective
public @interface Endpoint {

    String id() default "";

    boolean enableByDefault() default true;

}

There are no @AliasFor annotations used, so RuntimeHintsUtils.registerAnnotation won't register a proxy for Endpoint and SynthesizedAnnotation.

But if Endpoint is now used in this code:

String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext, Endpoint.class);

it fails with

Caused by: com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces [interface org.springframework.boot.actuate.endpoint.annotation.Endpoint, interface org.springframework.core.annotation.SynthesizedAnnotation] 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) ~[actuator-aot:na]
    at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:1037) ~[actuator-aot:na]
    at org.springframework.core.annotation.SynthesizedMergedAnnotationInvocationHandler.createProxy(SynthesizedMergedAnnotationInvocationHandler.java:305) ~[na:na]
    at org.springframework.core.annotation.TypeMappedAnnotation.createSynthesizedAnnotation(TypeMappedAnnotation.java:333) ~[na:na]
    at org.springframework.core.annotation.AbstractMergedAnnotation.synthesize(AbstractMergedAnnotation.java:210) ~[actuator-aot:6.0.0-SNAPSHOT]
    at org.springframework.core.annotation.AbstractMergedAnnotation.synthesize(AbstractMergedAnnotation.java:200) ~[actuator-aot:6.0.0-SNAPSHOT]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAnnotationOnBean(DefaultListableBeanFactory.java:733) ~[actuator-aot:6.0.0-SNAPSHOT]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAnnotationOnBean(DefaultListableBeanFactory.java:723) ~[actuator-aot:6.0.0-SNAPSHOT]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForAnnotation(DefaultListableBeanFactory.java:693) ~[actuator-aot:6.0.0-SNAPSHOT]
    at org.springframework.context.support.AbstractApplicationContext.getBeanNamesForAnnotation(AbstractApplicationContext.java:1301) ~[actuator-aot:6.0.0-SNAPSHOT]
    at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(BeanFactoryUtils.java:285) ~[na:na]
    at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.createEndpointBeans(EndpointDiscoverer.java:130) ~[actuator-aot:3.0.0-SNAPSHOT]
    at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.discoverEndpoints(EndpointDiscoverer.java:123) ~[actuator-aot:3.0.0-SNAPSHOT]
    at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.getEndpoints(EndpointDiscoverer.java:117) ~[actuator-aot:3.0.0-SNAPSHOT]
    at org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration.servletEndpointRegistrar(ServletEndpointManagementContextConfiguration.java:61) ~[actuator-aot:0.0.1-SNAPSHOT]
    at org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration_WebMvcServletEndpointManagementContextConfiguration__BeanDefinitions.lambda$getServletEndpointRegistrarInstance$0(ServletEndpointManagementContextConfiguration_WebMvcServletEndpointManagementContextConfiguration__BeanDefinitions.java:45) ~[na:na]
    at org.springframework.util.function.ThrowingFunction.apply(ThrowingFunction.java:63) ~[actuator-aot:6.0.0-SNAPSHOT]
    at org.springframework.util.function.ThrowingFunction.apply(ThrowingFunction.java:51) ~[actuator-aot:6.0.0-SNAPSHOT]
    at org.springframework.beans.factory.aot.AutowiredInstantiationArgumentsResolver.resolve(AutowiredInstantiationArgumentsResolver.java:156) ~[na:na]
    at org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration_WebMvcServletEndpointManagementContextConfiguration__BeanDefinitions.getServletEndpointRegistrarInstance(ServletEndpointManagementContextConfiguration_WebMvcServletEndpointManagementContextConfiguration__BeanDefinitions.java:45) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1224) ~[actuator-aot:6.0.0-SNAPSHOT]
    ... 50 common frames omitted

It seems like @AliasFor isn't the only thing which should trigger proxy creation.

At the moment I'm working around this with:

hints.proxies().registerJdkProxy(Endpoint.class, SynthesizedAnnotation.class);

Comment From: sbrannen

I've encountered a use-case in which RuntimeHintsUtils.registerAnnotation isn't enough to work with the annotation in a native setting.

That's to be expected. RuntimeHintsUtils.registerAnnotation can only reason about the annotation itself and meta-annotations in the meta-annotation hierarchy above the supplied annotation type.

If @Endpoint is used as a meta-annotation with attribute overrides in a composed annotation, you should actually invoke RuntimeHintsUtils.registerAnnotation with the composed annotation instead of Endpoint.class. Then RuntimeHintsUtils.registerAnnotation should automatically register the correct proxy class configuration for @Endpoint.

Comment From: sbrannen

One concrete example is @WebEndpoint whose attributes are annotated with @AliasFor(annotation = Endpoint.class), which in turn requires that @Endpoint by synthesizable (i.e., will need proxy configuration).


In summary, this effectively "works as designed", but I'll leave it open in order for the team to put more thought into it and potentially to improve the documentation to point out what I explained above.

Comment From: mhalbritter

Ah, I see. Thanks for the explanation.

Comment From: mhalbritter

If @Endpoint is used as a meta-annotation with attribute overrides in a composed annotation, you should actually invoke RuntimeHintsUtils.registerAnnotation with the composed annotation instead of Endpoint.class. Then RuntimeHintsUtils.registerAnnotation should automatically register the correct proxy class configuration for @Endpoint

When I register WebEndpoint, there will be no proxy hints generated for Endpoint, only for WebEndpoint.

Comment From: sbrannen

When I register WebEndpoint, there will be no proxy hints generated for Endpoint, only for WebEndpoint.

Ahhh... then that's likely a bug.

We'll look into it.