The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

编写原生应用程序的提示

本指南包含各种提示和技巧,以解决在尝试以本地可执行文件形式运行Java应用程序时可能出现的问题。

请注意,我们区分了两种情况,适用的解决方案可能是不同的:

  • 在应用程序里面,你将通过 pom.xml 来调整 native-image 的配置

  • 在扩展里面,Quarkus提供了很多基础设施来简化这一切

请根据你的情况,参考适当的章节。

在你的应用程序中支持原生

由于GraalVM的一些限制,在构建生成本地可执行文件的时候,可能需要进行一些调整。

包含资源

默认情况下,在构建本地可执行文件时,GraalVM不会将classpath上的任何资源纳入其创建的本地可执行文件。要成为本地可执行文件一部分的资源,需要明确地进行配置。

Quarkus automatically includes the resources present in META-INF/resources (the web resources) but, outside this directory, you are on your own.

请注意,你在这里需要非常小心,因为 META-INF/resources 中的任何东西都会作为静态网络资源暴露出来。所以这个目录不是 "让我们自动把这些资源包含在本地可执行文件中 "的捷径,而应该只用于静态网络资源。

其他资源应明确声明。

要在本地可执行文件中包含更多的资源,最简单的方法是使用 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/ 目录及其子目录下的所有文本文件

If globs are not sufficiently precise for your use case and you need to rely on regular expressions, or if you prefer relying on the GraalVM infrastructure, you can also create a resources-config.json (the most common location is within src/main/resources) JSON file defining which resources should be included:

{
  "resources": [
    {
      "pattern": ".*\\.xml$"
    },
    {
      "pattern": ".*\\.json$"
    }
  ]
}

这些模式都是有效的Java正则表达式。在这里,我们将所有的XML文件和JSON文件纳入本地可执行文件。

你可以在 GraalVM文档中找到更多关于这个主题的信息。

最后一项工作是通过向 application.properties 添加适当的配置,使配置文件为 native-image 可执行文件所知:

quarkus.native.additional-build-args =-H:ResourceConfigurationFiles=resources-config.json

在前面的片段中,我们能够简单地使用 resources-config.json ,而不是指定文件的整个路径,只是因为它被添加到了 src/main/resources 。如果该文件被添加到了另一个目录,就必须手动指定适当的文件路径。

多个选项可以用逗号隔开。例如,可以使用:

quarkus.native.additional-build-args =\
    -H:ResourceConfigurationFiles=resources-config.json,\
    -H:ReflectionConfigurationFiles=reflection-config.json

以确保各种资源被包括在内,并注册了额外的反射。

如果由于某种原因,将上述配置添加到 application.properties ,是不可取的,可以通过配置构建工具来有效地执行相同的操作。

使用Maven时,我们可以使用以下配置:

<profiles>
    <profile>
        <id>native</id>
        <properties>
            <quarkus.package.type>native</quarkus.package.type>
            <quarkus.native.additional-build-args>-H:ResourceConfigurationFiles=resources-config.json</quarkus.native.additional-build-args>
        </properties>
    </profile>
</profiles>

注册反射

在构建本地可执行文件时,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: javax.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 {
}

请注意, MyClassRequiringReflectionMySecondClassRequiringReflection 将被注册为反射,但不包括 MyReflectionConfiguration

在运用使用对象映射功能的第三方库(如Jackson或GSON)时,这个功能很方便:

@RegisterForReflection(targets = {User.class, UserImpl.class})
public class MyReflectionConfiguration {

}

使用配置文件

你可以使用配置文件来为反射注册类。

作为一个例子,为了注册类 com.acme.MyClass 的所有方法进行反射,我们创建 reflection-config.json (最常见的位置是在 src/main/resources 内)

[
  {
    "name" : "com.acme.MyClass",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "allDeclaredFields" : true,
    "allPublicFields" : true
  }
]

关于该文件格式的更多细节,请参考 GraalVM文档

最后一项工作是通过向 application.properties 添加适当的配置,使配置文件为 native-image 可执行文件所知:

quarkus.native.additional-build-args =-H:ReflectionConfigurationFiles=reflection-config.json

在前面的片段中,我们能够简单地使用 reflection-config.json ,而不是指定文件的整个路径,只是因为它被添加到了 src/main/resources 。如果该文件被添加到了另一个目录,就必须手动指定适当的文件路径。

多个选项可以用逗号隔开。例如,可以使用:

quarkus.native.additional-build-args =\
    -H:ResourceConfigurationFiles=resources-config.json,\
    -H:ReflectionConfigurationFiles=reflection-config.json

以确保各种资源被包括在内,并注册了额外的反射。

如果由于某种原因,将上述配置添加到 application.properties ,是不可取的,可以通过配置构建工具来有效地执行相同的操作。

使用Maven时,我们可以使用以下配置:

<profiles>
    <profile>
        <id>native</id>
        <properties>
            <quarkus.package.type>native</quarkus.package.type>
            <quarkus.native.additional-build-args>-H:ReflectionConfigurationFiles=reflection-config.json</quarkus.native.additional-build-args>
        </properties>
    </profile>
</profiles>

推迟类的初始化

默认情况下,Quarkus在构建时初始化所有类。

There are cases where the initialization of certain classes is done in a static block needs to be postponed to runtime. Typically, omitting such configuration would result in a runtime exception like the following:

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):

Another common source of errors is when the image heap taken by GraalVM contains a Random/SplittableRandom instance:

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.

Which is more often than not caused by Quarkus initializing at build time a class with a static Random/SplittableRandom field, causing this particular instance to be tentatively included in the image heap.

You can find detailed information about this Random/SplittableRandom issue in this blog post.

In these cases, delaying the infringing class initialization at runtime might be the solution and, to achieve that, you can use the --initialize-at-run-time=<package or class> configuration knob.

它应该使用 quarkus.native.additional-build-args 配置属性添加到 native-image 配置中,如上面的例子所示。

你可以在 GraalVM的文档中找到更多关于这一切的信息。

当需要通过 quarkus.native.additional-build-args 配置属性指定多个类或包时,需要对 , 符号进行转义。这方面的一个例子如下:

quarkus.native.additional-build-args=--initialize-at-run-time=com.example.SomeClass\\,org.acme.SomeOtherClass

以及在使用Maven配置而不是 application.properties :

<quarkus.native.additional-build-args>--initialize-at-run-time=com.example.SomeClass\,org.acme.SomeOtherClass</quarkus.native.additional-build-args>

管理代理类

在编写本地应用程序时,你需要在构建时通过指定它们的实现接口列表来定义代理类。

在这种情况下,你可能遇到的错误是:

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.

解决这个问题需要添加 -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> 参数,并提供一个动态代理配置文件。你可以在 GraalVM文档中找到关于这个文件格式的所有信息。

Modularity Benefits

During native executable build time GraalVM analyses the application’s call tree and generates a code-set that includes all the code it needs. Having a modular codebase is key to avoiding problems with unused or optional parts of your application, while at the same time reducing both native executable build times and size. In this section you will learn about the details behind the benefits of modularity for native applications.

When code is not modular enough, generated native executables can end up with more code than what the user needs. If a feature is not used and the code gets compiled into the native executable, this is a waste of native compilation time and memory usage, as well as native executable disk space and starting heap size. Even more problems arise when third party libraries or specialized API subsystems are used which cause native compilation or runtime errors, and their use is not modularised enough. A recent problem can be found in the JAXB library, which is capable of deserializing XML files containing images using Java’s AWT APIs. The vast majority of Quarkus XML users don’t need to deserialize images, so there shouldn’t be a need for users applications to include Java AWT code, unless they specifically configure Quarkus to add the JAXB AWT code to the native executable. However, because JAXB code that uses AWT is in the same jar as the rest of the XML parsing code, achieving this separation was rather complex and required the use of Java bytecode substitutions to get around it. These substitutions are hard to maintain and can easily break, hence they should be one’s last resort.

A modular codebase is the best way to avoid these kind of issues. Taking the JAXB/AWT problem above, if the JAXB code that dealt with images was in a separate module or jar (e.g. jaxb-images), then Quarkus could choose not to include that module unless the user specifically requested the need to serialize/deserialize XML files containing images at build time.

Another benefit of modular applications is that they can reduce the code-set that will need to get into the native executable. The smaller the code-set, the faster the native executable builds will be and the smaller the native executable produced.

The key takeaway point here is the following: Keeping optional features, particularly those that depend on third party libraries or API subsystems with a big footprint, in separate modules is the best solution.

How do I know if my application suffers from similar problems? Aside from a deep study of the application, finding usages of Maven optional dependencies is a clear indicator that your application might suffer from similar problems. These type of dependencies should be avoided, and instead code that interacts with optional dependencies should be moved into separate modules.

Enforcing Singletons

As already explained in the delay class initialization section, Quarkus marks all code to be initialized at build time by default. This means that, unless marked otherwise, static variables will be assigned at build time, and static blocks will be executed at build time too.

This can cause values in Java programs that would normally vary from one run to another, to always return a constant value. E.g. a static field that is assigned the value of System.currentTimeMillis() will always return the same value when executed as a Quarkus native executable.

Singletons that rely on static variable initialization will suffer similar problems. For example, imagine you have a singleton based around static initialization along with a REST endpoint to query it:

@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;
    }
}

When the singletons/static endpoint is queried, it will always return the same value, even after the application is restarted:

$ 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%

Singletons that rely on enum classes are also affected by the same issue:

@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;
    }
}

When the singletons/enum endpoint is queried, it will always return the same value, even after the application is restarted:

$ 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%

One way to fix it is to build singletons using CDI’s @Singleton annotation:

@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;
    }
}

After each restart, querying singletons/cdi will return a different value, just like it would in JVM mode:

$ 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%

An alternative way to enforce a singleton while relying static fields, or enums, is to delay its class initialization until run time. The nice advantage of CDI-based singletons is that your class initialization is not constrained, so you can freely decide whether it should be build-time or run-time initialized, depending on your use case.

Beware of common Java API overrides

Certain commonly used Java methods are overriden by user classes, e.g. toString, equals, hashCode…​etc. The majority of overrides do not cause problems, but if they use third party libraries (e.g. for additional formatting), or use dynamic language features (e.g. reflection or proxies), they can cause native image build to fail. Some of those failures might be solvable via configuration, but others can be more tricky to handle.

From a GraalVM points-to analysis perspective, what happens in these method overrides matters, even if the application does not explicitly call them. This is because these methods are used throughout the JDK, and all it takes is for one of those calls to be done on an unconstrained type, e.g. java.lang.Object, for the analysis to have to pull all implementations of that particular method.

在Quarkus扩展中支持原生

在Quarkus扩展中支持本机更容易,因为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中反射的更多信息可以 在这里 找到。

包含资源

在扩展里面,Quarkus通过允许扩展的作者指定一个 NativeImageResourceBuildItem 来消除对JSON配置文件的需求:

public class ResourcesProcessor {

    @BuildStep
    NativeImageResourceBuildItem nativeImageResourceBuildItem() {
        return new NativeImageResourceBuildItem("META-INF/extra.properties");
    }

}

关于本地可执行文件中GraalVM资源处理的更多信息,请参考 GraalVM文档

推迟类的初始化

Quarkus通过允许扩展作者简单地注册一个 RuntimeInitializedClassBuildItem 来简化操作。这样做的一个简单例子是:

public class S3Processor {

    @BuildStep
    RuntimeInitializedClassBuildItem cryptoConfiguration() {
        return new RuntimeInitializedClassBuildItem(CryptoConfiguration.class.getCanonicalName());
    }

}

使用这样的结构意味着 --initialize-at-run-time 参数将被自动添加到 native-image 命令行中。

关于 --initialize-at-run-time 的更多信息,请阅读 GraalVM文档

管理代理类

非常相似的是,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");
    }

}

使用这样的结构意味着 -H:DynamicProxyConfigurationResources 参数将被自动添加到 native-image 命令行中。

关于代理类的更多信息,你可以阅读 GraalVM文档

使用原生镜像日志

如果你的应用需要依赖日志组件(如Apache Commons logging或Log4j),并且在构建原生可执行文件时遇到了' ClassNotFoundException ',你可以通过排除日志库并添加相应的JBoss日志适配器来解决这个问题。

更多细节请参考 日志指南