编写 Native 应用程序的技巧
本指南包含各种技巧和窍门,用于解决尝试将 Java 应用程序作为 Native 可执行文件运行时可能出现的问题。
请注意,我们区分了两种上下文,所应用的解决方案可能不同
-
在应用程序的上下文中,您将依赖于通过调整您的
pom.xml
来配置native-image
配置; -
在扩展的上下文中,Quarkus 提供了大量基础设施来简化所有这些。
请根据您的上下文参考相应的章节。
支持应用中的 Native
GraalVM 施加了许多约束,将您的应用程序变为 Native 可执行文件可能需要进行一些调整。
包含资源
默认情况下,在构建 Native 可执行文件时,GraalVM 不会将类路径上的任何资源包含到它创建的 Native 可执行文件中。需要显式配置要作为 Native 可执行文件一部分的资源。
Quarkus 会自动包含 META-INF/resources
(Web 资源) 中的资源,但在此目录之外,您需要自己处理。
请注意,您需要非常小心,因为 其他资源应该显式声明。 |
使用 quarkus.native.resources.includes
配置属性
要在 Native 可执行文件中包含更多资源,最简单的方法是使用 quarkus.native.resources.includes
配置属性及其用于排除资源的对应属性 quarkus.native.resources.excludes
。
两种配置属性都支持 glob 模式。
例如,在您的 application.properties
中拥有以下属性
quarkus.native.resources.includes=foo/**,bar/**/*.txt
quarkus.native.resources.excludes=foo/private/**
将会包含
-
foo/
目录及其子目录中的所有文件,除了foo/private/
及其子目录中的文件, -
bar/
目录及其子目录中的所有文本文件。
使用配置文件
如果 glob 对于您的用例来说不够精确,并且您需要依赖正则表达式,或者如果您更喜欢依赖 GraalVM 基础设施,您还可以创建一个 resource-config.json
JSON 文件来定义应包含哪些资源。此文件和其他 Native 镜像配置文件应放置在 src/main/resources/META-INF/native-image/<group-id>/<artifact-id>
文件夹下。这样,它们将被 Native 构建自动解析,无需额外配置。
依赖 GraalVM 基础设施意味着您有责任在新的 Mandrel 和 GraalVM 版本发布时保持配置文件更新。 另请注意,如果直接放置在 |
以下是此类文件的示例
{
"resources": [
{
"pattern": ".*\\.xml$"
},
{
"pattern": ".*\\.json$"
}
]
}
这些模式是有效的 Java 正则表达式。在这里,我们将所有 XML 文件和 JSON 文件包含到 Native 可执行文件中。
有关此主题的更多信息,请参阅 GraalVM Native 镜像中访问资源 指南。 |
注册以进行反射
构建 Native 可执行文件时,GraalVM 以封闭世界假设运行。它分析调用树并删除所有未直接使用的类/方法/字段。
通过反射使用的元素不属于调用树,因此它们会被死代码消除(如果在其他情况下未直接调用)。要将这些元素包含在您的 Native 可执行文件中,您需要显式地为它们注册反射。
这是一个非常常见的情况,因为 JSON 库通常使用反射将对象序列化为 JSON
public class Person {
private String first;
private String last;
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getLast() {
return last;
}
public void setValue(String last) {
this.last = last;
}
}
@Path("/person")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PersonResource {
private final Jsonb jsonb;
public PersonResource() {
jsonb = JsonbBuilder.create(new JsonbConfig());
}
@GET
public Response list() {
return Response.ok(jsonb.fromJson("{\"first\": \"foo\", \"last\": \"bar\"}", Person.class)).build();
}
}
如果我们使用上面的代码,在使用 Native 可执行文件时会收到如下异常
Exception handling request to /person: org.jboss.resteasy.spi.UnhandledException: jakarta.json.bind.JsonbException: Can't create instance of a class: class org.acme.jsonb.Person, No default constructor found
或者如果您使用 Jackson
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.acme.jsonb.Person and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
一个更糟糕的结果可能是不抛出任何异常,而是 JSON 结果完全为空。
有两种不同的方法可以解决此类问题。
使用 @RegisterForReflection 注解
注册类以进行反射的最简单方法是使用 @RegisterForReflection
注解
@RegisterForReflection
public class MyClass {
}
如果您的类位于第三方 jar 中,您可以通过使用一个空的类来托管它的 @RegisterForReflection
来实现。
@RegisterForReflection(targets={ MyClassRequiringReflection.class, MySecondClassRequiringReflection.class})
public class MyReflectionConfiguration {
}
请注意,MyClassRequiringReflection
和 MySecondClassRequiringReflection
将注册以进行反射,但 MyReflectionConfiguration
不会。
当使用使用对象映射功能的第三方库 (例如 Jackson 或 GSON) 时,此功能非常方便
@RegisterForReflection(targets = {User.class, UserImpl.class})
public class MyReflectionConfiguration {
}
注意:默认情况下,@RegisterForReflection
注解还将注册任何潜在的嵌套类以进行反射。如果您想避免此行为,可以将 ignoreNested
属性设置为 true
。
使用配置文件
如果您更喜欢依赖 GraalVM 基础设施,您还可以使用配置文件注册类以进行反射。
依赖 GraalVM 基础设施意味着您有责任在新的 Mandrel 和 GraalVM 版本发布时保持配置文件更新。 |
例如,为了注册类 com.acme.MyClass
的所有方法以进行反射,我们创建 reflect-config.json
(最常见的位置是在 src/main/resources
中)
[
{
"name" : "com.acme.MyClass",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"allDeclaredFields" : true,
"allPublicFields" : true
}
]
有关此文件格式的更多信息,请参阅 GraalVM Native 镜像中的反射 指南。 |
最后一件事是将配置文件告知 native-image
可执行文件。为此,将配置文件放置在 src/main/resources/META-INF/native-image/<group-id>/<artifact-id>
文件夹下。这样,它们将被 Native 构建自动解析,无需额外配置。
注册以进行代理
类似于 @RegisterForReflection
,您可以使用 @RegisterForProxy
注册接口以进行动态代理
@RegisterForProxy
public interface MyInterface extends MySecondInterface {
}
请注意,MyInterface
及其所有父接口都将被注册。
此外,如果接口位于第三方 jar 中,您可以通过使用一个空的类来托管它的 @RegisterForProxy
来实现。
@RegisterForProxy(targets={MyInterface.class, MySecondInterface.class})
public class MyReflectionConfiguration {
}
请注意,指定的代理接口的顺序很重要。有关更多信息,请参阅 Proxy javadoc。 |
延迟类初始化
默认情况下,Quarkus 在构建时初始化所有类。
在某些情况下,某些类在静态块中的初始化需要推迟到运行时。通常,省略此类配置将导致如下运行时异常
Error: No instances are allowed in the image heap for a class that is initialized or reinitialized at image runtime: sun.security.provider.NativePRNG
Trace: object java.security.SecureRandom
method com.amazonaws.services.s3.model.CryptoConfiguration.<init>(CryptoMode)
Call path from entry point to com.amazonaws.services.s3.model.CryptoConfiguration.<init>(CryptoMode):
另一个常见的错误来源是 GraalVM 获取的镜像堆包含 Random
/SplittableRandom
实例
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.
这通常是由于 Quarkus 在构建时初始化一个具有静态 Random
/SplittableRandom
字段的类引起的,从而导致此特定实例被尝试包含在镜像堆中。
您可以在 这篇博客文章 中找到有关此 |
在这些情况下,在运行时延迟侵权类初始化可能是解决方案,为了实现这一点,您可以使用 --initialize-at-run-time=<package or class>
配置旋钮。
它应该使用 quarkus.native.additional-build-args
配置属性添加到 native-image
配置中,如上面的示例所示。
有关更多信息,请参阅 GraalVM Native 镜像中的类初始化 指南。 |
当需要通过
在使用 Maven 配置而不是
|
管理代理类
在编写 Native 应用程序时,您需要在镜像构建时通过指定它们实现的接口列表来定义代理类。
在这种情况下,您可能遇到的错误是
com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces [interface org.apache.http.conn.HttpClientConnectionManager, interface org.apache.http.pool.ConnPoolControl, interface com.amazonaws.http.conn.Wrapped] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.
要解决此问题,您可以在 src/main/resources/META-INF/native-image/<group-id>/<artifact-id>
文件夹下创建一个 proxy-config.json
文件。有关 proxy-config.json
格式的更多信息,请参阅 JSON 中的动态代理元数据 文档。
或者,您可以创建一个 quarkus 扩展并注册代理类,如 管理代理类 中所述。
模块化优势
在 Native 可执行文件构建期间,GraalVM 分析应用程序的调用树并生成一个代码集,其中包含它需要的所有代码。拥有模块化的代码库是避免应用程序中未使用或可选部分问题的关键,同时还可以减少 Native 可执行文件的构建时间和大小。在本节中,您将了解模块化对于 Native 应用程序的好处背后的详细信息。
当代码不够模块化时,生成的 Native 可执行文件最终可能包含比用户需要的更多的代码。如果未使用某个功能,并且代码被编译到 Native 可执行文件中,那么这是对 Native 编译时间和内存使用量的浪费,以及 Native 可执行文件的磁盘空间和启动堆大小的浪费。当使用第三方库或专门的 API 子系统时,会出现更多问题,这会导致 Native 编译或运行时错误,并且它们的用例不够模块化。最近的一个问题可以在 JAXB 库中找到,该库能够使用 Java 的 AWT API 反序列化包含图像的 XML 文件。绝大多数 Quarkus XML 用户不需要反序列化图像,因此除非用户专门配置 Quarkus 以将 JAXB AWT 代码添加到 Native 可执行文件中,否则用户应用程序不应该需要包含 Java AWT 代码。但是,由于使用 AWT 的 JAXB 代码与其余 XML 解析代码位于同一个 jar 中,因此实现这种分离相当复杂,并且需要使用 Java 字节码替换来解决此问题。这些替换很难维护,并且很容易损坏,因此它们应该是最后的手段。
模块化的代码库是避免这些问题的最佳方法。以上面的 JAXB/AWT 问题为例,如果处理图像的 JAXB 代码位于单独的模块或 jar (例如 jaxb-images
) 中,那么除非用户专门请求需要在构建时序列化/反序列化包含图像的 XML 文件,否则 Quarkus 可以选择不包含该模块。
模块化应用程序的另一个好处是它们可以减少需要进入 Native 可执行文件的代码集。代码集越小,Native 可执行文件的构建速度就越快,生成的 Native 可执行文件就越小。
这里的关键要点是:将可选功能,尤其是那些依赖于具有较大占用空间的第三方库或 API 子系统的功能,保存在单独的模块中是最佳解决方案。 |
我如何知道我的应用程序是否存在类似问题?除了对应用程序进行深入研究之外,查找 Maven 可选依赖项 的用法清楚地表明您的应用程序可能存在类似问题。应该避免这些类型的依赖项,而应将与可选依赖项交互的代码移动到单独的模块中。
强制单例
如 延迟类初始化 部分中所述,Quarkus 默认将所有代码标记为在构建时初始化。这意味着,除非另有标记,否则静态变量将在构建时分配,并且静态块也将在构建时执行。
这可能会导致 Java 程序中通常从一次运行到另一次运行而变化的值始终返回一个常量值。例如,当作为 Quarkus Native 可执行文件执行时,分配了 System.currentTimeMillis()
值的静态字段将始终返回相同的值。
依赖于静态变量初始化的单例将遇到类似的问题。例如,假设您有一个基于静态初始化的单例以及一个查询它的 REST 端点
@Path("/singletons")
public class Singletons {
@GET
@Path("/static")
public long withStatic() {
return StaticSingleton.startTime();
}
}
class StaticSingleton {
static final long START_TIME = System.currentTimeMillis();
static long startTime() {
return START_TIME;
}
}
当查询 singletons/static
端点时,即使在应用程序重新启动后,它也将始终返回相同的值
$ curl https://:8080/singletons/static
1656509254532%
$ curl https://:8080/singletons/static
1656509254532%
### Restart the native application ###
$ curl https://:8080/singletons/static
1656509254532%
依赖于 enum
类的单例也受到相同问题的影响
@Path("/singletons")
public class Singletons {
@GET
@Path("/enum")
public long withEnum() {
return EnumSingleton.INSTANCE.startTime();
}
}
enum EnumSingleton {
INSTANCE(System.currentTimeMillis());
private final long startTime;
private EnumSingleton(long startTime) {
this.startTime = startTime;
}
long startTime() {
return startTime;
}
}
当查询 singletons/enum
端点时,即使在应用程序重新启动后,它也将始终返回相同的值
$ curl https://:8080/singletons/enum
1656509254601%
$ curl https://:8080/singletons/enum
1656509254601%
### Restart the native application ###
$ curl https://:8080/singletons/enum
1656509254601%
一种解决方法是使用 CDI 的 @Singleton
注解构建单例
@Path("/singletons")
public class Singletons {
@Inject
CdiSingleton cdiSingleton;
@GET
@Path("/cdi")
public long withCdi() {
return cdiSingleton.startTime();
}
}
@Singleton
class CdiSingleton {
// Note that the field is not static
final long startTime = System.currentTimeMillis();
long startTime() {
return startTime;
}
}
每次重新启动后,查询 singletons/cdi
将返回不同的值,就像在 JVM 模式下一样
$ curl https://:8080/singletons/cdi
1656510218554%
$ curl https://:8080/singletons/cdi
1656510218554%
### Restart the native application ###
$ curl https://:8080/singletons/cdi
1656510714689%
在依赖静态字段或枚举的情况下强制单例的另一种方法是 延迟其类初始化直到运行时。基于 CDI 的单例的一个好处是您的类初始化不受约束,因此您可以根据您的用例自由决定它应该是在构建时还是运行时初始化。
注意常见的 Java API 覆盖
某些常用的 Java 方法被用户类覆盖,例如 toString
、equals
、hashCode
...等。大多数覆盖不会引起问题,但如果它们使用第三方库(例如用于额外格式化)或使用动态语言功能(例如反射或代理),它们可能会导致 Native 镜像构建失败。其中一些失败可以通过配置解决,但另一些可能更难处理。
从 GraalVM 指针分析的角度来看,这些方法覆盖中发生的事情很重要,即使应用程序没有显式调用它们。这是因为这些方法在整个 JDK 中使用,并且只需要在一个不受约束的类型上(例如 java.lang.Object
)完成其中一个调用,分析就必须提取该特定方法的所有实现。
在 Quarkus 扩展中支持 Native
在 Quarkus 扩展中支持 Native 更加容易,因为 Quarkus 提供了许多工具来简化所有这些。
此处描述的所有内容仅在 Quarkus 扩展的上下文中有效,在应用程序中无效。 |
注册反射
Quarkus 通过使用 ReflectiveClassBuildItem
使扩展中的反射注册变得轻而易举,从而消除了对 JSON 配置文件的需求。
要注册一个类以进行反射,需要创建一个 Quarkus 处理器类并添加一个注册反射的构建步骤
public class SaxParserProcessor {
@BuildStep
ReflectiveClassBuildItem reflection() {
// since we only need reflection to the constructor of the class, we can specify `false` for both the methods and the fields arguments.
return new ReflectiveClassBuildItem(false, false, "com.sun.org.apache.xerces.internal.parsers.SAXParser");
}
}
有关 GraalVM 中反射的更多信息,请参阅 GraalVM Native 镜像中的反射 指南。 |
包含资源
在扩展的上下文中,Quarkus 通过允许扩展作者指定 NativeImageResourceBuildItem
来消除对 JSON 配置文件的需求
public class ResourcesProcessor {
@BuildStep
NativeImageResourceBuildItem nativeImageResourceBuildItem() {
return new NativeImageResourceBuildItem("META-INF/extra.properties");
}
}
有关 Native 可执行文件中 GraalVM 资源处理的更多信息,请参阅 GraalVM Native 镜像中访问资源 指南。 |
延迟类初始化
Quarkus 通过允许扩展作者简单地注册 RuntimeInitializedClassBuildItem
来简化操作。一个简单的示例是
public class S3Processor {
@BuildStep
RuntimeInitializedClassBuildItem cryptoConfiguration() {
return new RuntimeInitializedClassBuildItem(CryptoConfiguration.class.getCanonicalName());
}
}
使用这样的构造意味着 --initialize-at-run-time
选项将自动添加到 native-image
命令行。
有关 |
管理代理类
类似地,Quarkus 允许扩展作者注册 NativeImageProxyDefinitionBuildItem
。一个示例是
public class S3Processor {
@BuildStep
NativeImageProxyDefinitionBuildItem httpProxies() {
return new NativeImageProxyDefinitionBuildItem("org.apache.http.conn.HttpClientConnectionManager",
"org.apache.http.pool.ConnPoolControl", "com.amazonaws.http.conn.Wrapped");
}
}
这将允许 Quarkus 生成处理代理类所需的配置。
或者,您可以创建一个 proxy-config.json
,如 管理代理类 中所述。
在这两种情况下,配置都将被 Native 构建自动解析,无需额外配置。 有关在 Native 可执行文件中使用代理类的更多信息,请参阅 Native 镜像中的动态代理 和 GraalVM 手动配置动态代理。 |
使用 Native 镜像进行日志记录
如果您使用的依赖项需要诸如 Apache Commons Logging 或 Log4j 之类的日志记录组件,并且在构建 Native 可执行文件时遇到 ClassNotFoundException
,则可以通过排除日志记录库并添加相应的 JBoss Logging 适配器来解决此问题。
有关更多详细信息,请参阅 日志记录指南。