Tips for writing native applications
本指南包含各种技巧,帮助你解决在尝试以本机可执行文件运行 Java 应用程序时可能遇到的问题。 请注意,我们区分两种上下文,在这些上下文中应用的解决方案可能不同:
-
在应用程序的上下文中,你将依靠配置
native-image
配置来调整pom.xml
; -
在扩展的上下文中,Quarkus 提供大量基础设施来简化所有这些。
根据你的语境,请参阅相应部分。
Supporting native in your application
GraalVM 会施加许多限制,而将你的应用程序变成可执行的原生应用程序可能需要进行一些微调。
Including resources
默认情况下,在构建原生可执行文件时,GraalVM 不会将 classpath 中的任何资源包含在它创建的可执行的原生文件中。要作为原生可执行文件的一部分的资源需要作明确配置。
Quarkus 自动包含 META-INF/resources
中存在的资源(Web 资源),但在该目录之外,你需要自行处理。
需要注意的是,你必须在这里极其谨慎,因为 META-INF/resources
中的任何内容都会作为静态 Web 资源公开。因此,这个目录不是“让我们自动将这些资源包含在可执行的原生文件中”的捷径,并且应该仅用于静态 Web 资源。
其他资源应作明确声明。
Using the quarkus.native.resources.includes
configuration property
要将更多资源包含在可执行的原生文件中,最简单的方法是使用 quarkus.native.resources.includes
配置属性,以及用于排除资源的其对应属性 quarkus.native.resources.excludes
。
这两个配置属性都支持全局模式。
例如,在你的 application.properties
中有以下属性:
quarkus.native.resources.includes=foo/**,bar/**/*.txt
quarkus.native.resources.excludes=foo/private/**
将会包括:
-
foo/
目录及其子目录中的所有文件,但foo/private/
及其子目录中的文件除外, -
bar/
目录及其子目录中的所有文本文件。
Using a configuration file
如果全局模式对于你的用例来说不够精确,并且你需要使用正则表达式,或者如果你更愿意使用 GraalVM 基础设施,你还可以创建一个 resource-config.json
JSON 文件来定义应包括哪些资源。该文件和其他本机镜像配置文件应放在 src/main/resources/META-INF/native-image/<group-id>/<artifact-id>
文件夹下。这样,它们将由本机构建自动解析,而无需其它配置。
使用 GraalVM 基础设施意味着你需要负责在发布新的 Mandrel 和 GraalVM 版本时更新配置文件。
另请注意,如果 resource-config.json
文件直接放在 src/main/resources/META-INF/native-image/
下,会被 Quarkus 覆盖,因为 Quarkus 在该目录中生成了自己的配置文件。
此类文件的示例如下:
{
"resources": [
{
"pattern": ".*\\.xml$"
},
{
"pattern": ".*\\.json$"
}
]
}
模式是有效的 Java 正则表达式。此处我们将在可执行的原生文件中包含所有 XML 文件和 JSON 文件。
有关此主题的更多信息,请参阅 GraalVM Accessing Resources in Native Image指南。 |
Registering for reflection
在构建本地可执行文件时,GraalVM 在封闭世界假设下运行。它分析调用树,并删除所有没有直接使用的类/方法/字段。
通过反射使用的元素不属于调用树中的一部分,因此它们是无效代码(如果在其他情况下没有直接调用)。要将这些元素包含在本地可执行文件中,您需要显式地为反射注册它们。
这是一个非常常见的案例,因为 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();
}
}
如果使用以上代码,使用本地可执行文件时我们将会得到类似以下的异常:
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 结果是完全空的。
有两种不同的方法可以修复此类问题。
Using the @RegisterForReflection annotation
最简单的注册类以供反射使用的方法是使用 `@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
。
Using a configuration file
如果您更愿意依赖 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 Reflection in Native Image指南。 |
最后的工作是让 `native-image`可执行文件了解配置文件。要做到这一点,请将配置文件放在 `src/main/resources/META-INF/native-image/<group-id>/<artifact-id>`文件夹下。这样,它们将被本地版本自动解析,无需额外配置。
Registering for proxy
与 `@RegisterForReflection`类似,您可以使用 `@RegisterForProxy`注册接口以供动态代理使用:
@RegisterForProxy
public interface MyInterface extends MySecondInterface {
}
请注意, `MyInterface`及其所有超接口都将被注册。
另外,如果界面位于第三方 jar 中,你可以通过使用一个将为其托管 @RegisterForProxy
的空类来实现它。
@RegisterForProxy(targets={MyInterface.class, MySecondInterface.class})
public class MyReflectionConfiguration {
}
请注意,指定代理接口的顺序很重要。有关更多信息,请参见 Proxy javadoc。
Delaying class initialization
默认情况下,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
字段的类造成的,从而导致将此特定实例临时包含在映像堆中。
你可以在 this blog post 中找到有关此 |
在这些情况下,延迟执行违规类的运行时初始化可能是解决方案,为此,你可以使用 --initialize-at-run-time=<package or class>
配置旋钮。
应该使用 native-image
配置,如上述示例中所示,将其添加到该配置中。
有关更多信息,请参阅 GraalVM Class Initialization in Native Image 指南。 |
当需要通过
如果使用 Maven 配置而不是
|
Managing Proxy Classes
在编写本机应用程序时,你需要通过指定它们实现的接口列表,在图像构建时定义代理类。
在这种情况下,你可能会遇到以下错误:
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
文件。这样,该配置将由本机构建自动解析,无需额外配置。有关此文件格式的更多信息,请参阅 Dynamic Proxy Metadata in JSON 文档。
Modularity Benefits
在构建本机可执行文件期间,GraalVM 会分析应用程序的调用树,并生成一个代码集,其中包含它所需的所有代码。拥有一个模块化代码库是避免应用程序中未使用或可选部分出现问题并且同时减少本机可执行文件构建时间和大小的关键。在本节中,你将了解本地应用程序模块化的优势背后的详细信息。
当代码不够模块化时,生成的本机可执行文件最终可能包含比用户需要的更多代码。如果某个功能没有使用,并且该代码已编译到本机可执行文件中,那么这就是本机编译时间和内存使用量,以及本机可执行文件磁盘空间和初始堆大小的浪费。当使用第三方库或专用 API 子系统并且它们的使用不够模块化时,会出现更多问题,从而导致本机编译或运行时错误。在 JAXB 库中发现了最近出现的问题,它能够使用 Java 的 AWT API 反序列化包含图像的 XML 文件。绝大多数 Quarkus XML 用户不需要反序列化图像,因此用户的应用程序无需包含 Java AWT 代码,除非他们专门配置 Quarkus 将 JAXB AWT 代码添加到本机可执行文件中。然而,由于使用 AWT 的 JAXB 代码与其他 XML 解析代码位于同一个 jar 中,因此实现这种分离相当复杂,并且需要使用 Java 字节码替换才能解决。这些替换很难维护,并且很容易损坏,因此它们应该作为最后的办法。
模块化代码库是避免此类问题的最佳方法。以上面的 JAXB/AWT 问题为例,如果处理图像的 JAXB 代码位于单独的模块或 jar(例如 jaxb-images
)中,那么 Quarkus 可以选择不包含该模块,除非用户明确要求在构建时序列化/反序列化包含图像的 XML 文件。
模块化应用程序的另一个好处是,它们能够减少需要放入原生可执行文件中的代码集。代码集越小,原生可执行文件的构建速度就越快,生成的文件也越小。
这里关键的要点是:将可选功能(特别是那些依赖于具有较大空间占用量的第三方库或 API 子系统)放在单独的模块中是最佳解决方案。 |
我如何知道我的应用程序是否遇到了类似的问题?除了对应用程序进行深入研究之外,找到 Maven optional dependencies的用法是应用程序可能遇到类似问题的明确指示。应避免使用此类型的依赖关系,而应将与可选依赖关系交互的代码移入单独的模块中。
Enforcing Singletons
如delay class initialization部分中已说明的那样,Quarkus 默认情况下标记所有代码在构建时进行初始化。这意味着,除非另有标记,否则静态变量将在构建时分配,并且静态块也将同时在构建时执行。
这会导致 Java 程序中的值通常在每次运行时都会发生变化,但始终返回常数值。例如,分配了`System.currentTimeMillis()`值的静态字段在作为 Quarkus 原生可执行文件执行时,始终会返回相同的值。
依赖于静态变量初始化的单例将遇到类似的问题。例如,假设您有一个基于静态初始化的单例,以及用于查询它的 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 http://localhost:8080/singletons/static
1656509254532%
$ curl http://localhost:8080/singletons/static
1656509254532%
### Restart the native application ###
$ curl http://localhost: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 http://localhost:8080/singletons/enum
1656509254601%
$ curl http://localhost:8080/singletons/enum
1656509254601%
### Restart the native application ###
$ curl http://localhost: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 http://localhost:8080/singletons/cdi
1656510218554%
$ curl http://localhost:8080/singletons/cdi
1656510218554%
### Restart the native application ###
$ curl http://localhost:8080/singletons/cdi
1656510714689%
在依赖于静态字段或枚举时,强制单例的另一种方法是delay its class initialization until run time。基于 CDI 的单例的一个好处是,您的类初始化不受约束,因此您可以根据自己的使用情况自由决定是否应在构建时还是在运行时进行初始化。
Beware of common Java API overrides
某些常用的 Java 方法会被用户类覆盖,例如:toString
、equals
、hashCode
…等。大多数覆盖不会导致问题,但如果它们使用第三方库(例如用于其他格式化)或使用动态语言功能(例如反射或代理),它们可能会导致原生映像构建失败。其中一些故障可以通过配置解决,但其他故障在处理上可能更棘手。
从 GraalVM 指向分析的角度来看,即使应用程序没有显式调用这些方法覆盖,这些方法覆盖中发生的内容也很重要。这是因为这些方法在整个 JDK 中使用,只需在不受约束的类型(例如`java.lang.Object`)上执行其中一个调用即可,即可分析出该特定方法的所有实现。
Supporting native in a Quarkus extension
在 Quarkus 扩展中支持原生版本甚至更容易,因为 Quarkus 提供了许多工具简化了所有这些操作。
此处描述的所有内容仅在 Quarkus 扩展的上下文中有效,在应用程序中无效。
Register reflection
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 Reflection in Native Image指南。 |
Including resources
在扩展的上下文中,Quarkus 允许扩展作者指定一个 NativeImageResourceBuildItem
来消除对 JSON 配置文件的需求:
public class ResourcesProcessor {
@BuildStep
NativeImageResourceBuildItem nativeImageResourceBuildItem() {
return new NativeImageResourceBuildItem("META-INF/extra.properties");
}
}
有关本机可执行文件中 GraalVM 资源处理的更多信息,请参阅 GraalVM Accessing Resources in Native Image 指南。 |
Delay class initialization
Quarkus 通过允许扩展作者简单地注册一个 RuntimeInitializedClassBuildItem
来简化事情。以下是一个简单的示例:
public class S3Processor {
@BuildStep
RuntimeInitializedClassBuildItem cryptoConfiguration() {
return new RuntimeInitializedClassBuildItem(CryptoConfiguration.class.getCanonicalName());
}
}
使用此结构意味着将自动向 native-image
命令行添加一个 --initialize-at-run-time
选项。
有关 |
Managing Proxy Classes
非常类似地,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");
}
}
使用此结构意味着将自动向 native-image
命令行添加一个 -H:DynamicProxyConfigurationResources
选项。
有关代理类别的更多信息,请参阅 GraalVM Configure Dynamic Proxies Manually 指南。 |
Logging with Native Image
如果您使用需要日志组件(例如 Apache Commons Logging 或 Log4j)的依赖项,并且在构建本机可执行文件时遇到 ClassNotFoundException
,则可以通过排除日志库并添加相应的 JBoss Logging 适配器来解决此问题。
有关更多详细信息,请参阅 Logging guide。