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

Issues with DummyInvocationUtils on Java 17

Open jochenberger opened this issue 3 years ago • 12 comments
trafficstars

I'm trying to move a project from Java 11 to Java 17 and I'm having issues with code that creates resource URLs.

linkTo(methodOn(EntityController.class)).getEntity(id).getHref()

The Controller method returns a CompletableFuture.

When I switch to Java 17, I get

java.lang.IllegalAccessException: module java.base does not open java.util.concurrent to unnamed module @6fdb1f78
	at java.base/java.lang.invoke.MethodHandles.privateLookupIn(MethodHandles.java:259) ~[na:na]
	at java.base/jdk.internal.reflect.GeneratedMethodAccessor51.invoke(Unknown Source) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:576) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:110) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:108) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54) ~[spring-core-5.3.21.jar!/:5.3.21]
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
	at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:134) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.proxy.Enhancer.createClass(Enhancer.java:419) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:57) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:206) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.hateoas.server.core.DummyInvocationUtils.getProxyWithInterceptor(DummyInvocationUtils.java:203) ~[spring-hateoas-1.5.1.jar!/:1.5.1]
	at org.springframework.hateoas.server.core.DummyInvocationUtils.access$000(DummyInvocationUtils.java:37) ~[spring-hateoas-1.5.1.jar!/:1.5.1]
	at org.springframework.hateoas.server.core.DummyInvocationUtils$InvocationRecordingMethodInterceptor.invoke(DummyInvocationUtils.java:96) ~[spring-hateoas-1.5.1.jar!/:1.5.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.example.EntityController$$EnhancerBySpringCGLIB$$8127ed24.getEntity(<generated>) ~[classes!/:na]

If I add --add-opens=java.base/java.util.concurrent=ALL-UNNAMED, I get

java.lang.IllegalArgumentException: $java.util.concurrent.CompletableFuture$$EnhancerBySpringCGLIB$$2c5f9ec4 not in same package as lookup class
	at java.base/java.lang.invoke.MethodHandleStatics.newIllegalArgumentException(MethodHandleStatics.java:167) ~[na:na]
	at java.base/java.lang.invoke.MethodHandles$Lookup$ClassFile.newInstance(MethodHandles.java:2283) ~[na:na]
	at java.base/java.lang.invoke.MethodHandles$Lookup.makeClassDefiner(MethodHandles.java:2318) ~[na:na]
	at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1843) ~[na:na]
	at java.base/jdk.internal.reflect.GeneratedMethodAccessor52.invoke(Unknown Source) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:577) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:110) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:108) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54) ~[spring-core-5.3.21.jar!/:5.3.21]
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
	at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:134) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.cglib.proxy.Enhancer.createClass(Enhancer.java:419) ~[spring-core-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:57) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:206) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.hateoas.server.core.DummyInvocationUtils.getProxyWithInterceptor(DummyInvocationUtils.java:203) ~[spring-hateoas-1.5.1.jar!/:1.5.1]
	at org.springframework.hateoas.server.core.DummyInvocationUtils.access$000(DummyInvocationUtils.java:37) ~[spring-hateoas-1.5.1.jar!/:1.5.1]
	at org.springframework.hateoas.server.core.DummyInvocationUtils$InvocationRecordingMethodInterceptor.invoke(DummyInvocationUtils.java:96) ~[spring-hateoas-1.5.1.jar!/:1.5.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.21.jar!/:5.3.21]
	at org.example.EntityController$$EnhancerBySpringCGLIB$$ee1b9f9d.getEntity(<generated>) ~[classes!/:na]
...

Please let me know if I can provide any more details.

jochenberger avatar Jun 28 '22 14:06 jochenberger

Might be related to #27490 / 28138

jochenberger avatar Jun 29 '22 10:06 jochenberger

Are you using Spring Framework / HATEOAS on the module path? Or does the error also appear in a plain classpath arrangement? A reproducer would be super helpful.

odrotbohm avatar Jun 29 '22 11:06 odrotbohm

I'm not sure what you mean. I'm using a Spring Boot REST application, I'm starting the fat jar with java -jar.

jochenberger avatar Jun 29 '22 11:06 jochenberger

Here you go: demo.zip

Start the project using ./gradlew bootRun with Java 17 and call curl localhost:8080/help.

Please let me know if you need anything else.

For reference, the controller code in the demo project looks like this:

public class DemoController {

    @GetMapping("/entity/{id}")
    public CompletableFuture<Object> getEntity(@PathVariable String id) {
        return CompletableFuture.completedFuture(Map.of("id", id));
    }

    @GetMapping("/help")
    public Object getHelp() {
        return Map.of("urls",
                Map.of("entity", WebMvcLinkBuilder
                        // this is where the exception occurrs
                        .linkTo(WebMvcLinkBuilder.methodOn(DemoController.class).getEntity(null))
                        .withSelfRel().getHref()));
    }
}

The stacktrace is

2022-06-29 14:34:38.143 ERROR 42435 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class java.util.concurrent.CompletableFuture: Common causes of this problem include using a final class or a non-visible class; nested exception is org.springframework.cglib.core.CodeGenerationException: java.lang.IllegalAccessException-->module java.base does not open java.util.concurrent to unnamed module @4cfaf581] with root cause

java.lang.IllegalAccessException: module java.base does not open java.util.concurrent to unnamed module @4cfaf581
        at java.base/java.lang.invoke.MethodHandles.privateLookupIn(MethodHandles.java:259) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:576) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:110) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:108) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54) ~[spring-core-5.3.21.jar:5.3.21]
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
        at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:134) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.cglib.proxy.Enhancer.createClass(Enhancer.java:419) ~[spring-core-5.3.21.jar:5.3.21]
        at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:57) ~[spring-aop-5.3.21.jar:5.3.21]
        at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:206) ~[spring-aop-5.3.21.jar:5.3.21]
        at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[spring-aop-5.3.21.jar:5.3.21]
        at org.springframework.hateoas.server.core.DummyInvocationUtils.getProxyWithInterceptor(DummyInvocationUtils.java:203) ~[spring-hateoas-1.5.1.jar:1.5.1]
        at org.springframework.hateoas.server.core.DummyInvocationUtils.access$000(DummyInvocationUtils.java:37) ~[spring-hateoas-1.5.1.jar:1.5.1]
        at org.springframework.hateoas.server.core.DummyInvocationUtils$InvocationRecordingMethodInterceptor.invoke(DummyInvocationUtils.java:96) ~[spring-hateoas-1.5.1.jar:1.5.1]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.21.jar:5.3.21]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.21.jar:5.3.21]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.21.jar:5.3.21]
        at com.example.demo.DemoController$$EnhancerBySpringCGLIB$$d4c33d56.getEntity(<generated>) ~[main/:na]
        at com.example.demo.DemoController.getHelp(DemoController.java:25) ~[main/:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
...

jochenberger avatar Jun 29 '22 12:06 jochenberger

I'm not sure what you mean. I'm using a Spring Boot REST application, I'm starting the fat jar with java -jar.

That's helpful, thanks!

Here you go: demo.zip

Start the project using ./gradlew bootRun with Java 17 and call curl localhost:8080/help.

Please let me know if you need anything else.

That, too! I'll have a look ASAP.

odrotbohm avatar Jun 29 '22 12:06 odrotbohm

If I omit the CompletableFuture and return Map directly, it works fine. Curiously, it also seems to work if I use Future instead of CompletableFuture in the method signature. :man_shrugging:

jochenberger avatar Jun 29 '22 12:06 jochenberger

For interfaces, JDK proxies are generated. For concrete types, we need to create a class-based (CGLib) proxy, which fails as the JDK rejects a reflective reference to the core JDK type. We're currently investigating the issue internally. Can you switch to Future as the return type of the controller method as a workaround? Usually, those methods are not really called directly anyway.

odrotbohm avatar Jun 29 '22 12:06 odrotbohm

I'll try that. I only just found that workaround myself. :wink:

jochenberger avatar Jun 29 '22 12:06 jochenberger

Looks promising. I think I'll be able to work around the issue for us thanks to your quick and helpful response. Thanks! Feel free to close the issue.

jochenberger avatar Jun 29 '22 14:06 jochenberger

Glad to hear that! I'll keep it around until we come to an official conclusion. Could be a fix or just some tweak to the documentation outlining the limitation or configuration flags to set when running the app to re-enable those types being proxied.

odrotbohm avatar Jun 29 '22 14:06 odrotbohm

Hi @jochenberger,

If I add --add-opens=java.base/java.util.concurrent=ALL-UNNAMED, I get

@jhoeller suggested that you instead use --add-opens=java.base/java.lang=ALL-UNNAMED to open up the protected ClassLoader.defineClass(...) method used by Spring's CGLIB fork.

What happens if you instead do that?

sbrannen avatar Jul 02 '22 11:07 sbrannen

@sbrannen, yes, that does seem to help. At least with the demo project. I didn't try the other one yet.

jochenberger avatar Jul 04 '22 08:07 jochenberger