Quarkus 打包native报错 随机数种子不符合预期

Quarkus 是一个目前非常火的 Java 应用开发框架,定位是轻量级的微服务框架。Quarkus 提供了优秀的容器化整合能力,相较于传统开发框架(Spring Boot)有着更快的启动速度、更小的内存消耗、更短的服务响应。

通过 GraalVM 将 Quarkus 项目打包为 native运行程序,即可以实现项目的快速启动,达到秒级甚至为毫秒级。

但在使用过程中也同样有着一定的约束,比如——随机数。在原来将项目打包成jar的方式时,类的加载和初始化会在程序运行时才进行执行,比如java.util.Random类中的随机种子的生成会在程序真正运行并进行初始化时才会生成。但GraalVM 将 Quarkus 项目打包为 native运行程序时,会直接将类中的静态属性直接进行初始化操作,而不是等待程序运行时。

假如你恰巧在类的静态属性中定义并创建了java.util.Random对象

1
private static final Random RANDOM = new Random();

那么你将会报如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected.  To see how this object got instantiated use --trace-object-instantiation=java.util.Random. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option --initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace:
at parsing org.apache.commons.lang3.RandomStringUtils.random(RandomStringUtils.java:297)
Call path from entry point to org.apache.commons.lang3.RandomStringUtils.random(int, int, int, boolean, boolean):
at org.apache.commons.lang3.RandomStringUtils.random(RandomStringUtils.java:297)
at org.apache.commons.lang3.RandomStringUtils.random(RandomStringUtils.java:277)
at org.apache.commons.lang3.RandomStringUtils.randomNumeric(RandomStringUtils.java:214)
at com.sheca.invoiceservice.utils.IdUtils.getIncreaseIdByNanoTime(IdUtils.java:19)
at com.sheca.invoiceservice.service.InvoiceService.fillInvoiceInfo(InvoiceService.java:281)
at com.sheca.invoiceservice.service.InvoiceService.makeInvoice(InvoiceService.java:224)
at com.sheca.invoiceservice.service.InvoiceService_Subclass.makeInvoice$$superaccessor1(InvoiceService_Subclass.zig:223)
at com.sheca.invoiceservice.service.InvoiceService_Subclass$$function$$1.apply(InvoiceService_Subclass$$function$$1.zig:41)
at sun.security.ec.XECParameters$1.get(XECParameters.java:183)
at com.oracle.svm.core.jdk.SystemPropertiesSupport.initializeLazyValue(SystemPropertiesSupport.java:216)
at com.oracle.svm.core.jdk.SystemPropertiesSupport.getProperty(SystemPropertiesSupport.java:169)
at com.oracle.svm.core.jdk.Target_java_lang_System.getProperty(JavaLangSubstitutions.java:291)
at com.oracle.svm.jni.JNIJavaCallWrappers.jniInvoke_VA_LIST:Ljava_lang_System_2_0002egetProperty_00028Ljava_lang_String_2_00029Ljava_lang_String_2(generated:0)

at com.oracle.svm.core.util.UserError.abort(UserError.java:82)
at com.oracle.svm.hosted.FallbackFeature.reportAsFallback(FallbackFeature.java:233)
at com.oracle.svm.hosted.NativeImageGenerator.runPointsToAnalysis(NativeImageGenerator.java:798)
at com.oracle.svm.hosted.NativeImageGenerator.doRun(NativeImageGenerator.java:582)
at com.oracle.svm.hosted.NativeImageGenerator.lambda$run$2(NativeImageGenerator.java:495)
at java.base/java.util.concurrent.ForkJoinTask$AdaptedRunnableAction.exec(ForkJoinTask.java:1407)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)

此时就是由于在GraalVM打包时会初始化Random对象和随机种子,同时又校验到不允许这样进行编码。

对于该问题的解决办法官方给了两种:
1、首先是他建议你修改代码逻辑,不要对类静态属性中的Random对象进行初始化。通过使用单例模式来实现对象的初始化和使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RandomUtil {

private static Random random;

public static Random get() {
if (random == null) {
synchronized (RandomUtil.class) {
if (random == null) {
random = new Random();
}
}
}
return random;
}
}

2、如果你不希望修改你的写法,或者出现问题的是第三方的jar包,你无法对源码进行修改,那么官方也提供了在运行时在进行初始化的方式。你只要指定需要在运行时才进行初始化的类,就可以解决这个问题。
在 application.properties 文件中增加配置:

1
2
# 延迟初始化org.apache.commons.lang3.RandomStringUtils类
quarkus.native.additional-build-args=--initialize-at-run-time=org.apache.commons.lang3.RandomStringUtils

注意,在这里写的是包含Random静态属性的类,而不是Random类。

其实Quarkus官方自身也遇到了这个问题,而他们是通过编码的方式解决的,而不是通过参数配置。
Merge pull request #16008 from zakkak/grpc-random-reinit
所以在实际使用中你也可以有第三种解决办法,就是像官方一样通过编码来解决问题。


最后,感谢你的阅读。愿你的人生了无Bug。

------ 本文结束------