Assuming ExampleApi.class is an interface that has HttpExchange methods, only in a forkjoin thread when running the app with java -jar the following code:
HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(webClient))
.build()
.createClient(ExampleApi.class);
raises the following exception:
2023-01-26T09:13:50.045+01:00 ERROR 1908 --- [Pool-1-worker-1] com.example.demo.DemoRunner : Failed to create client
java.lang.IllegalArgumentException: com.example.demo.ExampleApi referenced from a method is not visible from class loader
at java.base/java.lang.reflect.Proxy$ProxyBuilder.ensureVisible(Proxy.java:883) ~[na:na]
at java.base/java.lang.reflect.Proxy$ProxyBuilder.validateProxyInterfaces(Proxy.java:721) ~[na:na]
at java.base/java.lang.reflect.Proxy$ProxyBuilder.<init>(Proxy.java:648) ~[na:na]
at java.base/java.lang.reflect.Proxy.lambda$getProxyConstructor$1(Proxy.java:440) ~[na:na]
at java.base/jdk.internal.loader.AbstractClassLoaderValue$Memoizer.get(AbstractClassLoaderValue.java:329) ~[na:na]
at java.base/jdk.internal.loader.AbstractClassLoaderValue.computeIfAbsent(AbstractClassLoaderValue.java:205) ~[na:na]
at java.base/java.lang.reflect.Proxy.getProxyConstructor(Proxy.java:438) ~[na:na]
at java.base/java.lang.reflect.Proxy.newProxyInstance(Proxy.java:1037) ~[na:na]
at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:123) ~[spring-aop-6.0.4.jar!/:6.0.4]
at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:115) ~[spring-aop-6.0.4.jar!/:6.0.4]
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:97) ~[spring-aop-6.0.4.jar!/:6.0.4]
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:137) ~[spring-aop-6.0.4.jar!/:6.0.4]
at org.springframework.web.service.invoker.HttpServiceProxyFactory.createClient(HttpServiceProxyFactory.java:95) ~[spring-web-6.0.4.jar!/:6.0.4]
at com.example.demo.DemoRunner.lambda$run$0(DemoRunner.java:42) ~[classes!/:0.0.1-SNAPSHOT]
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1804) ~[na:na]
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1796) ~[na:na]
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[na:na]
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[na:na]
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[na:na]
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[na:na]
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[na:na]
The same code run with ./mvnw spring-boot:run doesn't raise an exception. Also the same code when using a FixedThreadPool instead of a ForkJoinPool doesn't raise an exception.
Tested with Spring boot 3.0.2, maven 3.8.6, java temurin 17.0.5, I've uploaded a reproducer here.
Comment From: wilkinsona
Thanks for the report. This is a duplicate of https://github.com/spring-projects/spring-boot/issues/19427. When run with java -jar the thread context class loader is Boot's LaunchedURLClassLoader. In Java 8, ForkJoinPool would continue to use this TCCL by default. In Java 9 it changed and uses, IIRC, the app class loader by default. You'll need to update your code where you're using the ForkJoinPool to ensure that the threads that it create use the class loader that loaded ExampleApi as their thread context class loader.