spring-projects / spring-projects/spring-framework

Default method invocation fails in native image with `HttpServiceProxyFactory` and `RSocketServiceProxyFactory`

Open
#37,166 1 comment 0 reactions 1 assignee View on GitHub

@sbrannen is already working on this.

Since Sep 4, 2026.

status: waiting-for-triage theme: aot
Dominant language
Java
Stars
60.2k
Forks
38.8k
Avg merge
5d 2h
Merged PRs (30d)
27

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.