
Not so long ago, Oracle released the first release of the GraalVM project (https://www.graalvm.org/). The release was immediately assigned the number 19.0.0, apparently in order to convince that the project is mature and ready for use in serious applications. One of the parts of this project:
Substrate VM is a framework that allows you to turn Java applications into native executable files (as well as native libraries that can be connected to applications written, for example, in C / C ++). This feature has been declared experimental. It is also worth noting that native Java applications have some limitations: it is necessary to list all the resources used in order to include them in the native program; You need to list all the classes that will be used with the help of reflection and other restrictions. The full list is listed here
Native Image Java Limitations . After studying this list, in principle, it is clear that the limitations are not so significant that it is impossible to develop more complex applications than the Hello Worlds. I have set such a goal: to develop a small program that has an embedded web server, uses a database (via the ORM library) and compiles into a native binary that can run on systems without a Java machine installed.
I will experiment on Ubuntu 19.04 (Intel Core i3-6100 CPU @ 3.70GHz Ă— 4).
Installing GraalVM
Installing GraalVM is conveniently done using
SDKMAN . GraalVM installation command:
')
sdk install java 19.0.0-grl
Installation of OpenJDK GraalVM CE 19.0.0 is completed, CE is Community Edition. There is also Enterprise Edition (EE), but this edition needs to be downloaded from the Oracle Technology Network, the link is on the
GraalVM Downloads page.
After installing GraalVM, already using the update manager for gu components from GraalVM, I installed support for compiling into the native binary -
gu install native-image
Everything, working tools are ready, now you can start developing applications.
Simple native application
I use Maven as a build system. To create native binaries there is a maven plugin:
native-image-maven-plugin <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>
You also need to set the main class of the application. This can be done both in the native-image-maven-plugin, and in the traditional way, through:
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>
Create the main class:
Startup.java public class Startup { public static void main(String[] args) { System.out.println("Hello world!"); } }
Now you can execute the maven command to build the application:
mvn clean package
Building the native binaries on my machine takes 35 seconds. As a result, a 2.5 MB binary file is obtained in the target directory. The program does not require the installed Java machine and runs on machines where Java is missing.
Repository link:
Github: native-java-helloworld-demo .
Jdbc driver postgres
And so, a simple application works, displays "Hello world". There were no solutions to any problems. I’ll try to go up one level: I’ll connect the Postgres JDBC driver to request data from the database The
Issues on the GraalVM githabab come across bugs related to the Postgres driver, but to the release candidates of GraalVM. All of them are marked as corrected.
I connect postgresql dependency:
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency>
I am writing code to extract data from the database (the simplest users tablet was created):
Startup.java 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")); } } } }
I collect the native binary and immediately get the build error:
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
The fact is that the native application builder initializes all static fields during the build process (unless indicated otherwise), and it does this by examining the dependencies of the classes. My code does not refer to org.postgresql.Driver, so the builder does not know how to initialize it (when building, or when the application starts) and offers to register it for initialization when building. This can be done by adding it to the maven arguments of the native-image-maven-plugin plugin, as specified in the error description. After adding Driver, I still get the same error related to org.postgresql.util.SharedTimer. Again I collect and encounter such an assembly error:
Error: Class initialization failed: org.postgresql.sspi.SSPIClient
There are no recommendations for corrections. But, looking at the source of the class, it is clear that it refers to the execution of code under Windows. On Linux, its initialization (which occurs during build) fails with an error. It is possible to postpone its initialization to start the application: --initialize-at-run-time = org.postgresql.sspi.SSPIClient. Initialization on Linux will not occur and errors associated with this class, we will not get. Build Arguments:
<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>
The build began to take 1 minute 20 seconds and the file swelled to 11 MB. I added an additional flag to build the binary: - no-fallback prohibits the generation of a native binary that requires an installed Java machine. Such a binary is created if the collector detects the use of language features that are either not supported in the Substrate VM or require configuration, but there is no configuration yet. In my case, the collector discovered the potential use of reflection in the JDBC driver. But this is only potential use, it is not required in my program and therefore no additional configuration is required (how to do it will be shown later). There is also the --static flag, which causes the generator to statically link libc. But if you use it, the program crashes with segmentation fault when trying to resolve a network name to an IP address. I looked for any solutions to this problem, but did not find anything suitable, so I left the program’s dependency on libc.
I run the resulting binary and get the following error:
Exception in thread "main" org.postgresql.util.PSQLException: Could not find a java cryptographic algorithm: TLS SSLContext not available.
After some research, the cause of the error was identified: Postgres establishes a default TLS connection using the Elliptic Curve. SubstrateVM does not include the implementation of such algorithms for TLS, this is the corresponding open issue -
Single-binary ECC (ECDSA / ECDHE) TLS support for SubstrateVM . There are several solutions: put the library from the delivery of GraalVM next to the application: libsunec.so, configure the list of algorithms on the Postgres server, excluding the Elliptic Curve algorithms, or simply disable the TLS connection setting in the Postgres driver (this option was chosen):
dataSource.setSslMode(SslMode.DISABLE.value);
Having eliminated the error of creating a connection with Postgres, I launch the native application, it runs and outputs data from the database.
Repository link:
Github: native-java-postgres-demo .
DI framework and embedded web server
When developing complex Java applications, they usually use some kind of framework, for example, Spring Boot. But judging by this article
GraalVM native image support , the work of Spring Boot in the native image “out of the box” is promised to us only in the version of Spring Boot 5.3.
But there is a wonderful
Micronaut framework, which
claims to work in GraalVM native image . In general, connecting Micronaut to an application that will be assembled into a binary does not require any special settings and problem solving. Indeed, many settings for the use of reflection and connection of resources for a Substrate VM have already been made inside Micronaut. By the way, the same settings can be placed inside your application in the settings file META-INF / native-image / $ {groupId} / $ {artifactId} /native-image.properties (this way for the settings file is recommended by Substrate VM), this is typical file content:
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
The resource-config.json, reflect-config.json, proxy-config.json files contain the resource connection settings, the reflection and the proxies used (Proxy.newProxyInstance). These files can be created manually or obtained using the agentlib: native-image-agent. In the case of using native-image-agent, you need to run a regular jar (and not a native binary) using the agent:
java -agentlib:native-image-agent=config-output-dir=output -jar my.jar
where output is the directory in which the files described above are located. At the same time, the program needs not only to be launched, but also to execute scripts in the program, because settings are written to the files as reflexive is used, resources are opened, and proxies are created. These files can be placed in META-INF / native-image / $ {groupId} / $ {artifactId} and referenced in native-image.properties.
I decided to connect logging with logback: I added a dependency on the logback-classic library and the logback.xml file. After that, I collected the usual jar and launched it using the native-image-agent. At the end of the program the desired configuration files. If you look at their contents, you can see that the agent has registered using logback.xml to compile into a binary. Also, all instances of using reflection are in the reflect-config.json file: for given classes, meta-information will fall into the binary.
Then I added a dependency on the micronaut-http-server-netty library to use the embedded web-server based on netty and created a controller:
Startup.java @Controller("/hello") public class HelloController { @Get("/{name}") @Produces(MediaType.TEXT_PLAIN) public HttpResponse<String> hello(String name) { return HttpResponse.ok("Hello " + name); } }
And main class:
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); } }
Now you can try to build a native binary. My build took 4 minutes. If you start it and go to
http: // localhost: 8080 / hello / user, then an error occurs:
{"_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"}
Honestly, it is not quite clear why this is happening, but after investigating with a spear method, I found that the error disappears if the following lines are removed from the resource-config.json file (which was created by the agent):
{"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"},
These resources are registered by Micronaut and it seems that re-registration leads to a double registration of my controller and an error. If, after correcting the file, the binary is reassembled and run, then there will be no errors, the text “Hello user” will be displayed at
http: // localhost: 8080 / hello / user .
I want to draw attention to the use of the following line in the main class:
Signal.handle(new Signal("INT"), sig -> System.exit(0));
It needs to be inserted for the correct completion of Micronaut. Despite the fact that Micronaut hangs up the hook to complete the work, it does not work in the native binary. There is a related issue:
Shutdownhook not firing with native . It is marked as fixed, but, in fact, it only has a workaround using the Signal class.
Repository link:
Github: native-java-postgres-micronaut-demo .
ORM connection
JDBC is fine, but tedious with repetitive code, endless SELECT and UPDATE. I will try to facilitate (or complicate, depending on which side to watch) my life by connecting some ORM.
Hibernate
At first I decided to try
Hibernate , as it is one of the most common ORM for Java. But I didn’t manage to build a native image using Hibernate due to a build error:
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) ...
There is a corresponding open issue:
[native-image] Micronaut + Hibernate results in Error encountered while analysing java.lang.reflect.Method.getDefaultValue () .
jOOQ
Then I decided to try
jOOQ . I managed to build a native binary, though I had to make a lot of settings: specifying which classes to initialize (buildtime, runtime), and messing around with reflexes. As a result, it all came down to the fact that when the application was started, jOOQ initialized the proxy org.jooq.impl.ParserImpl $ Ignore as a static member of the org.jooq.impl.Tools class. And this proxy uses MethodHandle, which Substrate VM
does not support yet . Here is a similar open issue: I
’m not a single call .
Apache cayenne
Apache Cayenne is less common, but looks quite functional. I'll try to connect it. I created XML database schema description files, they can be created either manually or using the CayenneModeler GUI tool, or based on an existing database. Using the cayenne-maven-plugin in the pom file, the code generation of classes that correspond to the database tables will be performed:
cayenne-maven-plugin <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>
Then I added the CayenneRuntimeFactory class to initialize the database context factory:
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 controller:
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(","))); } }
Then I ran the program as a regular jar, using the agentlib: native-image-agent agent, to collect information about the resources used and reflection.
I compiled a native binary, I launch it, go to
http: // localhost: 8080 / hello / user and get the error:
{"message":"Internal Server Error: Provider com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl not found"}
it turns out that agentlib: native-image-agent has not detected the use of this class in reflection.
Manually added it to the reflect-config.json file:
{ "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", "allDeclaredConstructors":true }
I collect the binary again, I launch, update the web-page and get another error:
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.
Everything is clear, I add the setting, as indicated in the proposed solution. Again I collect the binary (this is 5 minutes of time), again I run and again an error, another one:
No DataMap found, can't route query org.apache.cayenne.query.SelectQuery@2af96966[root=class name.voyachek.demos.nativemcp.db.User,name=]"}
I had to tinker with this error, after numerous tests, studying the sources, it became clear that the reason for the error lies in this line of the org.apache.cayenne.resource.URLResource class:
return new URLResource(new URL(url, relativePath));
As it turned out, the Substrate VM loads the resource by url, which is specified as the base, and not by the url, which should be based on the base and relativePath. What I registered the following issue:
Invalid resource content when using new URL (URL context, String spec) .
The error is determined, now you need to look for workarounds. Fortunately, Apache Cayenne was a pretty customizable thing. You had to register your own resource loader:
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();
Here is his code:
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); } } } }
It has a line
return new URLResource(new URL(url, relativePath));
replaced by:
String url = getURL().toString(); url = url.substring(0, url.lastIndexOf("/") + 1) + relativePath; return new URLResource(new URI(url).toURL());
I collect the binary (70 MB), I launch it, go to
http: // localhost: 8080 / hello / user and everything works, the page displays data from the database.
Repository link:
Github: native-micronaut-cayenne-demo .
findings
The goal has been achieved: a simple web application has been developed with database access using ORM. The application is compiled into a native binary and can be run on systems without a Java machine installed. Despite numerous problems, I found a combination of frameworks, settings, and workarounds, which allowed me to get a working program.
Yes, the ability to build regular binaries from Java source code is still in experimental status. This is evident from the abundance of problems, the need to find workarounds. But in the end all the same it turned out to achieve the desired result. What did I get?
- The only self-contained file (almost, there are dependencies on libraries such as libc) that can run on systems without a Java machine.
- The start time is on average 40 milliseconds versus 2 seconds when starting a regular jar.
Among the shortcomings, I would like to mention a great compile time for the native binary. It takes me an average of five minutes, and most likely will increase when writing code and connecting libraries. Therefore, it makes sense to create binaries already on the basis of fully debugged code. In addition, debugging information for native binaries is available only in the commercial edition of Graal VM - Enterprise Edition.