[spring-projects/spring-boot]由于使用了错误的类加载器,Hazelcast 执行的异步反序列化可能会失败

2024-05-09 505 views
5

我在一次漫长的牦牛剃须过程中将应用程序迁移到 Boot 2.4.1 和 Hazelcast 4.0.3 时发现了这一点。

当您从 jar 运行它时会失败:

@SpringBootApplication
public class IssueApplication {
    public static void main(String[] args) {
        SpringApplication.run(IssueApplication.class, args);
    }
    @Bean
    CommandLineRunner runner(HazelcastInstance hazelcast) {
        IMap<String, Foo> sessions = hazelcast.getMap("foos");
        return args -> {
            sessions.set("foo", new Foo("foo"));
            System.err.println(sessions.get("foo"));
            sessions.getAsync("foo").whenComplete((u, t) -> System.err.println(u)).toCompletableFuture().get();
        };
    }
}
class Foo implements Serializable {
    private String value;
    public Foo() {
    }
    public Foo(String value) {
        this.value = value;
    }
    public String getValue() {
        return this.value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    @Override
    public String toString() {
        return "Foo [value=" + this.value + "]";
    }
}

当您在 IDE 中运行或使用java -classpath ....问题是 HazelcastThread.crrentThread().getContextClassLoader()默认使用 ,而在异步后台线程中,这JarLauncher不是AppClassLoader.

我用这个解决了这个问题

        @Bean
        Config hazelcastConfig() {
            Config config = new Config();
            config.setClassLoader(SpringApplication.class.getClassLoader());
            ...
            return config;
        }

但这个问题实际上更通用——可能任何使用 JDK fork-join 实用程序的库最终都会得到相同的类加载器。

回答

6

问题是 Hazelcast 默认使用 Thread.crrentThread().getContextClassLoader(),并且在异步后台线程中,这是 JarLauncher 而不是 AppClassLoader。

我想JarLauncher这里应该是LaunchedURLClassLoader。我想这也可能是错误的做法。在启动应用程序中,您希望 TCCL 成为LaunchedURLClassLoader类加载器,可以加载应用程序的类及其依赖项。只能AppClassLoader看到启动器和JDK。

不幸的是,JDK 的通用 fork-join 池使用应用程序类加载器作为其 TCCL。这使得它不适合在打包的 Boot 应用程序中使用,而且我怀疑,也不适合在任何具有自定义类加载器(例如 Servlet 容器)的环境中使用。我们以前见过这个问题(https://github.com/spring-projects/spring-boot/issues/15737),一般来说,我认为我们对此无能为力。

在这种特定情况下,看起来可以通过自动设置类加载器来改进自动配置(假设这是可能的,同时仍然尊重用户配置的其余部分)。

9

唔。我想也许 Hazelcast 中还有一些问题我们可以要求他们修复。

当 Hazelcast不在类路径上时,此应用程序可以正常运行(IDE 和 JAR) :

@SpringBootApplication
public class IssueApplication {

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

    @Bean
    CommandLineRunner runner() {
        return args -> {
            ForkJoinPool.commonPool().submit(() -> {
                System.err.println(ClassUtils.resolveClassName(Foo.class.getName(), null));
            }).get();
        };
    }

}
...

然后,如果您只是将 Hazelcast 放在类路径上但从不使用它,则 JAR 会失败:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-01-14 14:46:30.747 ERROR 1000765 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute CommandLineRunner
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:807) ~[spring-boot-2.4.2-SNAPSHOT.jar!/:2.4.2-SNAPSHOT]
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:788) ~[spring-boot-2.4.2-SNAPSHOT.jar!/:2.4.2-SNAPSHOT]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:333) ~[spring-boot-2.4.2-SNAPSHOT.jar!/:2.4.2-SNAPSHOT]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1311) ~[spring-boot-2.4.2-SNAPSHOT.jar!/:2.4.2-SNAPSHOT]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.4.2-SNAPSHOT.jar!/:2.4.2-SNAPSHOT]
    at com.example.IssueApplication.main(IssueApplication.java:16) ~[classes!/:0.0.1-SNAPSHOT]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[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:566) ~[na:na]
    at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) ~[hazelcast-issue-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
    at org.springframework.boot.loader.Launcher.launch(Launcher.java:107) ~[hazelcast-issue-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
    at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) ~[hazelcast-issue-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
    at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88) ~[hazelcast-issue-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
Caused by: java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: java.lang.IllegalArgumentException: Could not find class [com.example.Foo]
    at java.base/java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:1006) ~[na:na]
    at com.example.IssueApplication.lambda$runner$1(IssueApplication.java:24) ~[classes!/:0.0.1-SNAPSHOT]
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:804) ~[spring-boot-2.4.2-SNAPSHOT.jar!/:2.4.2-SNAPSHOT]
    ... 13 common frames omitted
Caused by: java.lang.IllegalArgumentException: java.lang.IllegalArgumentException: Could not find class [com.example.Foo]
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:600) ~[na:na]
    ... 16 common frames omitted
Caused by: java.lang.IllegalArgumentException: Could not find class [com.example.Foo]
    at org.springframework.util.ClassUtils.resolveClassName(ClassUtils.java:334) ~[spring-core-5.3.3.jar!/:5.3.3]
    at com.example.IssueApplication.lambda$runner$0(IssueApplication.java:23) ~[classes!/:0.0.1-SNAPSHOT]
    at java.base/java.util.concurrent.ForkJoinTask$AdaptedRunnableAction.exec(ForkJoinTask.java:1407) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177) ~[na:na]
Caused by: java.lang.ClassNotFoundException: com.example.Foo
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522) ~[na:na]
    at java.base/java.lang.Class.forName0(Native Method) ~[na:na]
    at java.base/java.lang.Class.forName(Class.java:398) ~[na:na]
    at org.springframework.util.ClassUtils.forName(ClassUtils.java:284) ~[spring-core-5.3.3.jar!/:5.3.3]
    at org.springframework.util.ClassUtils.resolveClassName(ClassUtils.java:324) ~[spring-core-5.3.3.jar!/:5.3.3]
    ... 7 common frames omitted

将 Hazelcast 添加到类路径(和一个空的hazelcast.xml)会导致创建我们的自动配置,这反过来似乎会与默认的ForkJoinPool.

8

这很奇怪。公共池使用DefaultForkJoinWorkerThreadFactory“使用系统类加载器作为线程上下文类加载器创建一个新的 ForkJoinWorkerThread”。我不知道 Hazelcast 怎么会搞砸这个。听起来需要一些挖掘。

0

这更奇怪了。如果您有一个 2 节点集群,则 JAR 不会失败。仅在一个节点上失败。

8

那是垃圾,抱歉。可能是脑放屁。由于您所说的原因,它总是从 JAR 中失败(系统类加载器是错误的)。

更新:有时会失败。尤其是 Java 8(与 11 不同,Java 11ForkJoinPool始终使用系统类加载器)。

5

那么也许我们需要Config在自动配置中设置 Hazelcast 中的类加载器?

2

是的,我想是的。现在我们需要弄清楚如何在我们支持的所有各种配置选项中做到这一点。

8

我认为它们最终都会以Config某种方式出现在一个物体中。 HC 中有一个实用程序,或者其他东西,用于加载 XML。而且Config是可变的,因此我们可以按照用户尝试建议的任何方式创建它并设置类加载器。或许?

9

是的,我想是的。现在我们需要弄清楚如何在我们支持的所有各种配置选项中做到这一点。

@wilkinsona 有机会以类似的方式修复475吗?

0

我不知道,因为我对 Feign 不熟悉。

5

Feign 没有可用于注入类加载器的自定义 API,因此无法提供与此类似的修复。 @OlgaMaciaszek 在引用的问题中建议了一种热切的初始化替代方案(并给出了另一个正在修复的类似问题的示例)。只需要有人去实施它。

4

谢谢@dsyer,我已经读过有关假客户端的急切加载,但它似乎没有解决问题

对我有用的有效解决方法/黑客是这个建议。但是,我无法判断这一点,因为我对 Springs ClassLoading 的了解非常有限。