Skip to content

Default method invocation fails in native image with HttpServiceProxyFactory and RSocketServiceProxyFactory #37166

Description

@mingchiuli

Description

A default method declared on an HTTP Service interface works on the JVM but fails at runtime in a GraalVM native image.

HttpServiceProxyFactory recognizes the default method and delegates to InvocationHandler.invokeDefault(...). In the native image, that invocation attempts to reflectively invoke the generated proxy class's private proxyClassLookup(MethodHandles.Lookup) method, which is not registered in the generated reachability metadata.

The native image builds successfully. The error occurs only when the default method is invoked at runtime.

Versions

  • Spring Boot: 4.1.0
  • Spring Framework: 7.0.8
  • GraalVM Native Build Tools: 1.1.9
  • Oracle GraalVM: 25.2.4+7.1
  • Java: 25.0.4
  • Gradle: 9.7
  • macOS ARM64

Relevant dependencies:

implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.boot:spring-boot-http-client")

Minimal example

package example;

import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.service.annotation.GetExchange;

public interface TestHttpService {

    @GetExchange("/echo")
    String exchange(
            @RequestHeader(value = "X-Test", required = false)
                    String optionalHeader);

    default String convenience() {
        return exchange(null);
    }
}
package example;

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.ImportHttpServices;

@SpringBootApplication(proxyBeanMethods = false)
@ImportHttpServices(group = "test", types = TestHttpService.class)
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    RestClientHttpServiceGroupConfigurer httpServiceGroups() {
        return groups ->
                groups.filterByName("test")
                        .forEachClient(
                                (group, builder) ->
                                        builder.baseUrl("http://127.0.0.1:8080"));
    }

    @Bean
    ApplicationRunner invokeDefaultMethod(TestHttpService service) {
        return args -> System.out.println(service.convenience());
    }

    @RestController
    static class EchoController {

        @GetMapping("/echo")
        String echo() {
            return "ok";
        }
    }
}

Steps to reproduce

Run on the JVM:

./gradlew bootRun

The default method completes successfully and prints:

ok

Build and run the native image:

./gradlew nativeCompile
./build/native/nativeCompile/demo

The native image builds successfully, but invoking service.convenience() fails.

Actual behavior

The relevant part of the exception is:

org.graalvm.nativeimage.MissingReflectionRegistrationError:
Cannot reflectively invoke method
'private static java.lang.invoke.MethodHandles$Lookup
jdk.proxy4.$Proxy...proxyClassLookup(java.lang.invoke.MethodHandles$Lookup)'

    at java.lang.reflect.Proxy.defaultMethodHandle(Proxy.java)
    at java.lang.reflect.Proxy.invokeDefault(Proxy.java)
    at java.lang.reflect.InvocationHandler.invokeDefault(InvocationHandler.java)
    at org.springframework.web.service.invoker.HttpServiceProxyFactory
        $HttpServiceMethodInterceptor.invoke(HttpServiceProxyFactory.java)

Spring AOT generates proxy metadata containing the expected interface combination:

{
  "type": {
    "proxy": [
      "example.TestHttpService",
      "org.springframework.aop.SpringProxy",
      "org.springframework.aop.framework.Advised",
      "org.springframework.core.DecoratingProxy"
    ]
  }
}

However, invocation of the generated proxy's proxyClassLookup method is not registered.

Expected Behavior

Default methods supported by HttpServiceProxyFactory should behave consistently on the JVM and in a native image.

Alternatively, if default methods cannot currently be supported in native images, this limitation should be detected during AOT processing or documented.

Workaround

Calling the annotated exchange method directly works:

service.exchange(null);

Removing the convenience default method avoids the InvocationHandler.invokeDefault(...) branch and the native image works correctly.

Registering the generated proxy's private proxyClassLookup method manually does not appear to be a desirable application-level workaround, because the proxy class and its interface composition are framework/JDK implementation details.

Question

Should the HTTP Service proxy AOT processing register the metadata required by InvocationHandler.invokeDefault(...), or is this expected to be handled elsewhere?

Related Issues

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

status: waiting-for-triageAn issue we've not yet triaged or decided ontheme: aotAn issue related to Ahead-of-time processing

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions