Java本机映像:可用性检查



不久之前,Oracle发布了GraalVM项目的第一个版本(https://www.graalvm.org/)。 该发行版立即分配了编号19.0.0,显然是为了说服该项目已经成熟并且可以在严重的应用程序中使用。 这个项目的一部分: Substrate VM是一个框架,它允许您将Java应用程序转换为本地可执行文件(以及可以在用C / C ++编写的应用程序中连接的本地库)。 到目前为止,此功能已宣布为实验性功能。 还值得注意的是,本机Java应用程序有一些限制:您必须列出用于将其包含在本机程序中的所有资源; 您需要列出将与反射和其他限制一起使用的所有类。 本机图像Java限制在此处提供了完整列表。 在研究了该列表之后,原则上可以理解的是,限制并不那么重要,以至于不可能开发出比地狱字更为复杂的应用程序。 我设定了这个目标:开发一个小型程序,该程序具有内置的Web服务器,使用数据库(通过ORM库)并编译为可以在未安装Java机器的系统上运行的本机二进制文件。

我将在Ubuntu 19.04(3.70GHz×4的Intel Core i3-6100 CPU)上进行实验。

安装GraalVM


使用SDKMAN方便地完成GraalVM的安装。 GraalVM安装命令:

sdk install java 19.0.0-grl 

将安装OpenJDK GraalVM CE 19.0.0,CE为Community Edition。 也有企业版(EE),但是该版本需要从Oracle技术网下载,链接在GraalVM下载页面上。

安装GraalVM之后,已经使用了GraalVM的gu组件更新管理器,我在本机二进制文件中安装了编译支持-

 gu install native-image 

一切就绪,工作工具就绪,现在您可以开始开发应用程序了。

简单的本机应用程序


作为构建系统,我使用Maven。 要创建本机二进制文件,有一个maven插件:

本机图像行家插件
 <build> <plugins> <plugin> <groupId>com.oracle.substratevm</groupId> <artifactId>native-image-maven-plugin</artifactId> <version>${graal.version}</version> <executions> <execution> <goals> <goal>native-image</goal> </goals> <phase>package</phase> </execution> </executions> <configuration> <imageName>nativej</imageName> <buildArgs> --no-server </buildArgs> </configuration> </plugin> </plugins> </build> 


仍然需要设置应用程序的主类。 既可以在native-image-maven-plugin中完成,也可以通过传统方式通过以下方式完成:

maven-jar-plugin
 <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.4</version> <configuration> <archive> <manifest> <mainClass>nativej.Startup</mainClass> </manifest> </archive> </configuration> </plugin> 


创建主类:

启动文件
 public class Startup { public static void main(String[] args) { System.out.println("Hello world!"); } } 


现在,您可以运行maven命令来构建应用程序:

 mvn clean package 

在我的机器上构建本机二进制文件需要35秒。 结果,在目标目录中获得了2.5 MB的二进制文件。 该程序不需要安装的Java机器,并且可以在缺少Java的机器上运行。

仓库链接: Github:native-java-helloworld-demo

JDBC Postgres驱动程序


这样,一个简单的应用程序就会显示“ Hello world”。 无需解决方案。 我将尝试上一个级别:我将连接Postgres JDBC驱动程序以从数据库请求数据。 GraalVM github上的问题遇到了与Postgres驱动程序有关的错误,但在GraalVM发行候选版本上存在问题。 所有这些都标记为固定。

我连接postgresql依赖项:

 <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency> 

我正在编写用于从数据库提取数据的代码(已创建了最简单的用户板):

启动文件
 public class Startup { public static void main(String[] args) SQLException { final PGSimpleDataSource ds = new PGSimpleDataSource(); ds.setUrl("jdbc:postgresql://localhost/demo_nativem"); ds.setUser("test"); ds.setPassword("test"); try ( Connection conn = ds.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM \"public\".\"user\""); ) { while(rs.next()){ System.out.print("ID: " + rs.getLong("id")); System.out.println(", Name: " + rs.getString("name")); } } } } 


我收集了本机二进制文件,并立即收到构建错误:

Error: No instances are allowed in the image heap for a class that is initialized or reinitialized at image runtime: org.postgresql.Driver. Try marking this class for build-time initialization with --initialize-at-build-time=org.postgresql.Driver


事实是,本机应用程序构建器会在构建过程中初始化所有静态字段(除非另行指定),并且他通过检查类的依赖关系来完成此操作。 我的代码未引用org.postgresql.Driver,因此收集器不知道如何更好地对其进行初始化(在构建时或在应用程序启动时),并提供在构建时对其进行注册以进行初始化。 可以通过将其添加到native-image-maven-plugin插件的maven参数中来完成,如错误描述中所示。 添加驱动程序后,出现与org.postgresql.util.SharedTimer相关的相同错误。 同样,我收集并遇到这样的构建错误:

Error: Class initialization failed: org.postgresql.sspi.SSPIClient


没有修正建议。 但是,从类的源代码来看,很明显它与Windows下的代码执行有关。 在Linux上,其初始化(在组装过程中发生)失败,并显示错误。 有机会在应用程序启动时延迟其初始化:--initialize-at-run-time = org.postgresql.sspi.SSPIClient。 在Linux上不会进行初始化,并且我们将不再遇到与此类相关的错误。 构建参数:

 <buildArgs> --no-server --no-fallback --initialize-at-build-time=org.postgresql.Driver --initialize-at-build-time=org.postgresql.util.SharedTimer --initialize-at-run-time=org.postgresql.sspi.SSPIClient </buildArgs> 

组装开始需要1分20秒,文件膨胀到11 MB。 我添加了一个用于构建二进制文件的附加标志:--no-fallback禁止生成需要已安装Java机器的本地二进制文件。 如果收集器检测到使用了Substrate VM中不支持的语言功能或需要配置,但尚无配置,则会创建此类二进制文件。 以我为例,收集器发现了JDBC驱动程序中反射的潜在用途。 但这仅是潜在的用途,在我的程序中不是必需的,因此,不需要其他配置(稍后将显示操作方法)。 还有--static标志,它强制生成器静态链接libc。 但是,如果使用它,则当您尝试将网络名称解析为IP地址时,该程序将因分段错误而崩溃。 我寻找了解决该问题的方法,但没有找到合适的方法,因此我将程序依赖项留在了libc上。

我运行生成的二进制文件并得到以下错误:

Exception in thread "main" org.postgresql.util.PSQLException: Could not find a java cryptographic algorithm: TLS SSLContext not available.


经过一番研究后,确定了错误的原因:Postgres默认情况下使用椭圆曲线建立TLS连接。 SubstrateVM不包括针对TLS的此类算法的实现,这是相应的未解决问题-SubstrateVM的单二进制ECC(ECDSA / ECDHE)TLS支持 。 有几种解决方案:将来自GraalVM软件包的库:libsunec.so放在应用程序旁边,在Postgres服务器上配置算法列表,排除Elliptic Curve算法,或者只是在Postgres驱动程序中禁用TLS连接(已选择此选项):

 dataSource.setSslMode(SslMode.DISABLE.value); 

消除了与Postgres建立连接的错误之后,我启动了本机应用程序,它运行并显示数据库中的数据。

仓库链接: Github:native-java-postgres-demo

DI框架和嵌入式Web服务器


在开发复杂的Java应用程序时,他们通常使用某种框架,例如Spring Boot。 但是从GraalVM本机映像支持的这篇文章来看,仅在Spring Boot 5.3版本中才向我们保证Spring Boot在本机映像“开箱即用”中的工作。

但是有一个很棒的Micronaut框架声称可以在GraalVM本机映像中工作 。 通常,将Micronaut连接到将以二进制形式组装的应用程序不需要任何特殊设置或解决问题的方法。 的确,在Micronaut内部已经进行了许多用于为基材VM使用反射和连接资源的设置。 顺便说一句,可以将相同的设置放置在应用程序中的设置文件META-INF / native-image / $ {groupId} / $ {artifactId} /native-image.properties中(Substrate VM建议使用该设置文件的路径),这是典型的文件内容:

native-image.properties
 Args = \ -H:+ReportUnsupportedElementsAtRuntime \ -H:ResourceConfigurationResources=${.}/resource-config.json \ -H:ReflectionConfigurationResources=${.}/reflect-config.json \ -H:DynamicProxyConfigurationResources=${.}/proxy-config.json \ --initialize-at-build-time=org.postgresql.Driver \ --initialize-at-build-time=org.postgresql.util.SharedTimer \ --initialize-at-run-time=org.postgresql.sspi.SSPIClient 


文件resource-config.json,reflect-config.json,proxy-config.json包含用于连接资源,反射和使用的代理的设置(Proxy.newProxyInstance)。 这些文件可以手动创建,也可以使用agentlib:native-image-agent检索。 在使用native-image-agent的情况下,您需要使用代理运行常规的jar(而非本地二进制文件):

 java -agentlib:native-image-agent=config-output-dir=output -jar my.jar 

其中output是上述文件所在的目录。 在这种情况下,该程序不仅需要运行,而且还需要执行程序中的脚本,因为在您使用反射,打开资源,创建代理时,会将设置写入文件中。 这些文件可以放在META-INF / native-image / $ {groupId} / $ {artifactId}中,并在native-image.properties中引用。

我决定使用logback连接日志记录:我向logback-classic库和logback.xml文件添加了一个依赖项。 之后,我编译了一个常规jar,并使用native-image-agent运行了它。 在程序末尾,必要的设置文件。 如果查看它们的内容,则可以看到该代理注册了使用logback.xml编译为二进制文件。 另外,reflection-config.json文件包含所有使用反射的情况:对于给定的类,元信息将进入二进制文件。

然后,我向micronaut-http-server-netty库添加了一个依赖项,以使用基于netty的嵌入式Web服务器并创建了一个控制器:

启动文件
 @Controller("/hello") public class HelloController { @Get("/{name}") @Produces(MediaType.TEXT_PLAIN) public HttpResponse<String> hello(String name) { return HttpResponse.ok("Hello " + name); } } 


和主类:

HelloController.java
 public class Startup { public static void main(String[] args) { Signal.handle(new Signal("INT"), sig -> System.exit(0)); Micronaut.run(Startup.class, args); } } 


现在,您可以尝试构建本机二进制文件。 我的大会花了4分钟。 如果运行它并转到地址http://本地主机:8080 / hello / user,则会发生错误:

 {"_links":{"self":{"href":"/hello/user","templated":false}},"message":"More than 1 route matched the incoming request. The following routes matched /hello/user: GET - /hello/user, GET - /hello/user"} 

坦率地说,目前尚不清楚为什么会发生这种情况,但是通过键入进行搜索后,我发现,如果从resource-config.json文件(由代理创建)中删除了以下几行,则错误消失了:

  {"pattern":"META-INF/services/com.fasterxml.jackson.databind.Module"}, {"pattern":"META-INF/services/io.micronaut.context.env.PropertySourceLoader"}, {"pattern":"META-INF/services/io.micronaut.http.HttpResponseFactory"}, {"pattern":"META-INF/services/io.micronaut.inject.BeanConfiguration"}, {"pattern":"META-INF/services/io.micronaut.inject.BeanDefinitionReference"}, 

Micronaut注册了这些资源,似乎重新注册导致控制器的双重注册和错误。 如果在纠正文件后重建二进制文件并运行它,则不会再有错误,文本“ Hello user”将显示在http:// localhost:8080 / hello / user

我想提请注意主类中以下行的使用:

 Signal.handle(new Signal("INT"), sig -> System.exit(0)); 

需要将其插入以使Micronaut正确完成。 尽管Micronaut挂了一个要关闭的钩子,但它在本机二进制文件中不起作用。 有一个相应的问题: Shutdownhook不使用native触发 。 它被标记为固定,但实际上,只有使用Signal类的一种解决方法。

仓库链接: Github:native-java-postgres-micronaut-demo

ORM连接


JDBC很好,但是重复的代码,无尽的SELECT和UPDATE令人厌倦。 我将尝试通过连接某种ORM来促进(或复杂化,具体取决于看哪一侧)我的生活。

冬眠


最初,我决定尝试Hibernate ,因为它是Java最常见的ORM之一。 但是由于构建错误,我无法使用Hibernate构建本机映像:

 Error: Field java.lang.reflect.Method.defaultValue is not present on type java.lang.reflect.Constructor. Error encountered while analysing java.lang.reflect.Method.getDefaultValue() Parsing context: parsing org.hibernate.annotations.common.annotationfactory.AnnotationProxy.getAnnotationValues(AnnotationProxy.java:63) parsing org.hibernate.annotations.common.annotationfactory.AnnotationProxy(AnnotationProxy.java:52) ... 

有一个相应的未解决问题: [native-image] Micronaut + Hibernate导致在分析java.lang.reflect.Method.getDefaultValue()时遇到错误

OO


然后我决定尝试jOOQ 。 尽管必须做很多设置,我还是设法构建了一个本机二进制文件:指定要初始化的类(构建时间,运行时)和反射类。 最后,全部归结为以下事实:应用程序启动时,jOOQ将代理org.jooq.impl.ParserImpl $初始化为org.jooq.impl.Tools类的静态成员。 并且此代理使用MethodHandle,Substrate VM 尚不支持 。 这是一个类似的未解决问题: [native-image] Micronaut + Kafka无法使用MethodHandle参数构建本地图像,最多只能将其减少为一个调用

Apache卡宴


Apache Cayenne不太常见,但看起来很实用。 我会尝试连接它。 我创建了用于描述数据库架构的XML文件,可以手动创建它们,也可以使用CayenneModeler GUI工具创建它们,也可以基于现有数据库创建它们。 使用pom文件中的cayenne-maven-plugin,将执行与数据库表相对应的类的代码生成:

卡宴Maven插件
 <plugin> <groupId>org.apache.cayenne.plugins</groupId> <artifactId>cayenne-maven-plugin</artifactId> <version>${cayenne.version}</version> <configuration> <map>src/main/resources/db/datamap.map.xml</map> <destDir>${project.build.directory}/generated-sources/cayenne</destDir> </configuration> <executions> <execution> <goals> <goal>cgen</goal> </goals> </execution> </executions> </plugin> 


然后,我添加了CayenneRuntimeFactory类来初始化数据库上下文工厂:

CayenneRuntimeFactory.java
 @Factory public class CayenneRuntimeFactory { private final DataSource dataSource; public CayenneRuntimeFactory(DataSource dataSource) { this.dataSource = dataSource; } @Bean @Singleton public ServerRuntime cayenneRuntime() { return ServerRuntime.builder() .dataSource(dataSource) .addConfig("db/cayenne-test.xml") .build(); } } 


HelloController控制器:

HelloController.java
 @Controller("/hello") public class HelloController { private final ServerRuntime cayenneRuntime; public HelloController(ServerRuntime cayenneRuntime) { this.cayenneRuntime = cayenneRuntime; } @Get("/{name}") @Produces(MediaType.TEXT_PLAIN) public HttpResponse<String> hello(String name) { final ObjectContext context = cayenneRuntime.newContext(); final List<User> result = ObjectSelect.query(User.class).select(context); if (result.size() > 0) { result.get(0).setName(name); } context.commitChanges(); return HttpResponse.ok(result.stream() .map(x -> MessageFormat.format("{0}.{1}", x.getObjectId(), x.getName())) .collect(Collectors.joining(","))); } } 


然后,他使用agentlib:native-image-agent作为常规jar启动该程序,以收集有关所用资源和反射的信息。

我收集了本机二进制文件,运行它,转到地址http://本地主机:8080 / hello / user并收到错误:

 {"message":"Internal Server Error: Provider com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl not found"} 

事实证明agentlib:native-image-agent在反射中未检测到此类的使用。

手动将其添加到reflect-config.json文件:

 { "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", "allDeclaredConstructors":true } 

再次,我收集二进制文件,启动,更新网页,并得到另一个错误:

 Caused by: java.util.MissingResourceException: Resource bundle not found org.apache.cayenne.cayenne-strings. Register the resource bundle using the option -H:IncludeResourceBundles=org.apache.cayenne.cayenne-strings. 

在这里一切都清楚了,我添加了设置,如建议的解决方案所示。 我再次收集二进制文件(这是5分钟的时间),我一次又一次地启动它,并返回一个错误,另一个错误:

 No DataMap found, can't route query org.apache.cayenne.query.SelectQuery@2af96966[root=class name.voyachek.demos.nativemcp.db.User,name=]"} 

经过大量测试,然后研究了源代码,我不得不对这个错误进行修改,很明显,错误的原因在于来自org.apache.cayenne.resource.URLResource类的以下行:

 return new URLResource(new URL(url, relativePath)); 

事实证明,Substrate VM通过url(表示为基础)而不是url(应基于base和relativePath形成)来加载资源。 我注册了以下有关什么问题: 使用新URL(URL上下文,字符串规范)时资源内容无效

错误已确定,现在您需要寻找解决方法。 幸运的是,Apache Cayenne原来是可定制的东西。 您必须注册自己的资源加载器:

 ServerRuntime.builder() .dataSource(dataSource) .addConfig("db/cayenne-test.xml") .addModule(binder -> { binder.bind(ResourceLocator.class).to(ClassLoaderResourceLocatorFix.class); binder.bind(Key.get(ResourceLocator.class, Constants.SERVER_RESOURCE_LOCATOR)).to(ClassLoaderResourceLocatorFix.class); }) .build(); 

这是他的代码:

ClassLoaderResourceLocatorFix.java
 public class ClassLoaderResourceLocatorFix implements ResourceLocator { private ClassLoaderManager classLoaderManager; public ClassLoaderResourceLocatorFix(@Inject ClassLoaderManager classLoaderManager) { this.classLoaderManager = classLoaderManager; } @Override public Collection<Resource> findResources(String name) { final Collection<Resource> resources = new ArrayList<>(3); final Enumeration<URL> urls; try { urls = classLoaderManager.getClassLoader(name).getResources(name); } catch (IOException e) { throw new ConfigurationException("Error getting resources for "); } while (urls.hasMoreElements()) { resources.add(new URLResourceFix(urls.nextElement())); } return resources; } private class URLResourceFix extends URLResource { URLResourceFix(URL url) { super(url); } @Override public Resource getRelativeResource(String relativePath) { try { String url = getURL().toString(); url = url.substring(0, url.lastIndexOf("/") + 1) + relativePath; return new URLResource(new URI(url).toURL()); } catch (MalformedURLException | URISyntaxException e) { throw new CayenneRuntimeException( "Error creating relative resource '%s' : '%s'", e, getURL(), relativePath); } } } } 


它有一条线

 return new URLResource(new URL(url, relativePath)); 

替换为:

 String url = getURL().toString(); url = url.substring(0, url.lastIndexOf("/") + 1) + relativePath; return new URLResource(new URI(url).toURL()); 

我收集二进制文件(70 MB),启动它,然后转到http:// localhost:8080 / hello / user ,一切正常,数据库中的数据显示在页面上。

仓库链接: Github:native-micronaut-cayenne-demo

结论


目标已经实现:已经开发了一个简单的Web应用程序,该应用程序可以使用ORM访问数据库。 该应用程序被编译为本地二进制文件,并且可以在未安装Java机器的系统上运行。 尽管存在许多问题,但我发现框架,设置和变通方法相结合,使我可以获得有效的程序。

是的,从Java源代码构建常规二进制文件的功能仍处于试验状态。 大量的问题和寻找变通办法的必要性证明了这一点。 但是最后,结果仍然达到了预期的效果。 我得到了什么?

  • 唯一可以在没有Java机器的系统上运行的独立文件(几乎与libc之类的库有关)。
  • 启动时间平均为40毫秒,而启动常规jar则为2秒。

在缺点中,我想指出本地二进制文件的编译时间长。 我平均要花5分钟,最有可能在编写代码和连接库时增加。 因此,基于完全调试的代码创建二进制文件是有意义的。 此外,仅在商业版Graal VM-Enterprise Edition中提供本机二进制文件的调试信息。

Source: https://habr.com/ru/post/zh-CN454790/


All Articles