
Il n'y a pas si longtemps, Oracle a publié la première version du projet GraalVM (https://www.graalvm.org/). Le numéro 19.0.0 a été immédiatement attribué à la version, apparemment afin de convaincre que le projet est mature et prêt à être utilisé dans des applications sérieuses. L'une des parties de ce projet:
Substrate VM est un framework qui vous permet de transformer des applications Java en fichiers exécutables natifs (ainsi que des bibliothèques natives pouvant être connectées dans des applications écrites, par exemple, en C / C ++). Cette fonctionnalité a jusqu'à présent été déclarée expérimentale. Il convient également de noter que les applications Java natives ont certaines limites: vous devez répertorier toutes les ressources utilisées pour les inclure dans le programme natif; vous devez lister toutes les classes qui seront utilisées avec réflexion et autres restrictions. Une liste complète est donnée ici par
Native Image Java Limitations . Après avoir étudié cette liste, il est compréhensible en principe que les restrictions ne soient pas si importantes qu'il serait impossible de développer des applications plus complexes que les mots infernaux. J'ai fixé cet objectif: développer un petit programme qui a un serveur web intégré, utilise une base de données (via la bibliothèque ORM) et se compile en un binaire natif qui peut fonctionner sur des systèmes sans machine Java installée.
J'expérimenterai sur Ubuntu 19.04 (processeur Intel Core i3-6100 à 3,70 GHz × 4).
Installer GraalVM
L'installation de GraalVM s'effectue facilement à l'aide de
SDKMAN . Commande d'installation de GraalVM:
sdk install java 19.0.0-grl
OpenJDK GraalVM CE 19.0.0 sera installé, CE est Community Edition. Il existe également Enterprise Edition (EE), mais cette édition doit être téléchargée à partir d'Oracle Technology Network, le lien se trouve sur la page
Téléchargements GraalVM .
Après avoir installé GraalVM, déjà en utilisant le gestionnaire de mise à jour des composants gu de GraalVM, j'ai installé le support de compilation dans le binaire natif -
gu install native-image
Tout, les outils de travail sont prêts, vous pouvez maintenant commencer à développer des applications.
Application native simple
En tant que système de build, j'utilise Maven. Pour créer des binaires natifs, il existe un plugin maven:
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>
Il est toujours nécessaire de définir la classe principale de l'application. Cela peut être fait à la fois dans native-image-maven-plugin, et de manière traditionnelle, via:
plugin maven-jar <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>
Créez la classe principale:
Startup.java public class Startup { public static void main(String[] args) { System.out.println("Hello world!"); } }
Vous pouvez maintenant exécuter la commande maven pour créer l'application:
mvn clean package
La création d'un binaire natif sur ma machine prend 35 secondes. En conséquence, dans le répertoire cible, un fichier binaire de taille 2,5 Mo est obtenu. Le programme ne nécessite pas la machine Java installée et s'exécute sur les machines où Java est manquant.
Lien vers le référentiel:
Github: native-java-helloworld-demo .
Pilote JDBC Postgres
Et donc, une application simple fonctionne, affiche «Bonjour tout le monde». Aucune solution n'était nécessaire. J'essaierai d'aller un niveau plus haut: je connecterai le pilote Postgres JDBC pour demander des données à la base de données.
Les problèmes sur le github GraalVM rencontrent des bogues liés au pilote Postgres, mais sur les versions candidates de GraalVM. Tous sont marqués comme fixes.
Je connecte la dépendance postgresql:
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency>
J'écris le code pour extraire les données de la base de données (la plaque utilisateur la plus simple a été créée):
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")); } } } }
Je récupère le binaire natif et reçois immédiatement une erreur de build:
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
Le fait est que le générateur d'application natif initialise tous les champs statiques pendant le processus de génération (sauf indication contraire), et il le fait en examinant les dépendances des classes. Mon code ne fait pas référence à org.postgresql.Driver, donc le collecteur ne sait pas comment l'initialiser mieux (lors de la construction ou au démarrage de l'application) et propose de l'enregistrer pour l'initialisation lors de la génération. Cela peut être fait en l'ajoutant aux arguments maven du plugin native-image-maven-plugin, comme indiqué dans la description de l'erreur. Après avoir ajouté le pilote, j'obtiens la même erreur liée à org.postgresql.util.SharedTimer. Encore une fois, je collecte et rencontre une telle erreur de construction:
Error: Class initialization failed: org.postgresql.sspi.SSPIClient
Il n'y a aucune recommandation de correction. Mais, en regardant la source de la classe, il est clair qu'elle se rapporte à l'exécution de code sous Windows. Sous Linux, son initialisation (qui se produit lors de l'assemblage) échoue avec une erreur. Il est possible de retarder son initialisation au démarrage de l'application: --initialize-at-run-time = org.postgresql.sspi.SSPIClient. L'initialisation sous Linux ne se produira pas et nous n'obtiendrons plus d'erreurs liées à cette classe. Construire des 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>
L'assemblage a commencé à prendre 1 minute 20 secondes et le fichier a gonflé jusqu'à 11 Mo. J'ai ajouté un indicateur supplémentaire pour la construction du binaire: --no-fallback interdit de générer un binaire natif qui nécessite une machine Java installée. Un tel binaire est créé si le collecteur détecte l'utilisation de fonctionnalités de langage qui ne sont pas prises en charge dans la machine virtuelle de substrat ou nécessitent une configuration, mais il n'y a pas encore de configuration. Dans mon cas, le collectionneur a découvert l'utilisation potentielle de la réflexion dans le pilote JDBC. Mais ce n'est qu'une utilisation potentielle, ce n'est pas requis dans mon programme, et donc, une configuration supplémentaire n'est pas requise (comment le faire sera montré plus tard). Il y a aussi le drapeau --static, qui force le générateur à lier statiquement libc. Mais si vous l'utilisez, le programme se bloque avec une erreur de segmentation lorsque vous essayez de résoudre le nom du réseau en une adresse IP. J'ai cherché des solutions à ce problème, mais je n'ai rien trouvé de convenable, j'ai donc laissé la dépendance du programme sur libc.
J'exécute le binaire résultant et j'obtiens l'erreur suivante:
Exception in thread "main" org.postgresql.util.PSQLException: Could not find a java cryptographic algorithm: TLS SSLContext not available.
Après quelques recherches, la cause de l'erreur a été identifiée: Postgres, par défaut, établit une connexion TLS à l'aide de la courbe elliptique. SubstrateVM n'inclut pas la mise en œuvre de tels algorithmes pour TLS, voici le problème ouvert correspondant -
Prise en charge TLS ECC simple-binaire (ECDSA / ECDHE) pour SubstrateVM . Il existe plusieurs solutions: placer la bibliothèque du package GraalVM: libsunec.so à côté de l'application, configurer la liste des algorithmes sur le serveur Postgres, exclure les algorithmes Elliptic Curve ou tout simplement désactiver la connexion TLS dans le pilote Postgres (cette option a été choisie):
dataSource.setSslMode(SslMode.DISABLE.value);
Ayant éliminé l'erreur de créer une connexion avec Postgres, je lance l'application native, elle s'exécute et affiche les données de la base de données.
Lien vers le référentiel:
Github: native-java-postgres-demo .
Infrastructure DI et serveur Web intégré
Lors du développement d'une application Java complexe, ils utilisent généralement une sorte de framework, par exemple Spring Boot. Mais à en juger par cet article sur la
prise en
charge des images natives GraalVM , le travail de Spring Boot dans l'image native «
prête à l'emploi» ne nous est promis que dans la version de Spring Boot 5.3.
Mais il existe un merveilleux framework
Micronaut qui
prétend fonctionner dans l'image native GraalVM . En général, la connexion d'un Micronaut à une application qui sera assemblée dans un binaire ne nécessite aucun réglage spécial ni résolution de problème. En effet, de nombreux paramètres d'utilisation des ressources de réflexion et de connexion pour la VM Substrate sont déjà définis dans Micronaut. Soit dit en passant, les mêmes paramètres peuvent être placés dans votre application dans le fichier de paramètres META-INF / native-image / $ {groupId} / $ {artifactId} /native-image.properties (un tel chemin pour le fichier de paramètres est recommandé par Substrate VM), voici un exemple typique contenu du fichier:
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
Les fichiers resource-config.json, reflect-config.json, proxy-config.json contiennent les paramètres de connexion des ressources, de la réflexion et des proxys utilisés (Proxy.newProxyInstance). Ces fichiers peuvent être créés manuellement ou récupérés en utilisant agentlib: native-image-agent. Dans le cas de l'utilisation de l'agent d'image native, vous devez exécuter le fichier jar habituel (et non le binaire natif) à l'aide de l'agent:
java -agentlib:native-image-agent=config-output-dir=output -jar my.jar
où sortie est le répertoire où les fichiers décrits ci-dessus seront situés. Dans ce cas, le programme doit non seulement exécuter, mais également exécuter des scripts dans le programme, car les paramètres sont écrits dans les fichiers lorsque vous utilisez la réflexion, ouvrez les ressources, créez un proxy. Ces fichiers peuvent être placés META-INF / native-image / $ {groupId} / $ {artifactId} et référencés dans native-image.properties.
J'ai décidé de connecter la journalisation à l'aide de logback: j'ai ajouté une dépendance à la bibliothèque logback-classic et au fichier logback.xml. Après cela, j'ai compilé un pot régulier et l'ai exécuté en utilisant natif-image-agent. À la fin du programme, les fichiers de paramètres nécessaires. Si vous regardez leur contenu, vous pouvez voir que l'agent a enregistré l'utilisation de logback.xml pour compiler dans le binaire. De plus, le fichier réflexion-config.json contient tous les cas d'utilisation de la réflexion: pour des classes données, les méta-informations entreront dans le binaire.
Ensuite, j'ai ajouté une dépendance à la bibliothèque micronaut-http-server-netty pour utiliser le serveur Web intégré basé sur netty et créé un contrôleur:
Startup.java @Controller("/hello") public class HelloController { @Get("/{name}") @Produces(MediaType.TEXT_PLAIN) public HttpResponse<String> hello(String name) { return HttpResponse.ok("Hello " + name); } }
Et classe principale:
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); } }
Vous pouvez maintenant essayer de créer un binaire natif. Mon montage a pris 4 minutes. Si vous l'exécutez et accédez à l'adresse
http: // localhost: 8080 / hello / user, une erreur se produit:
{"_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"}
Honnêtement, il n'est pas entièrement clair pourquoi cela se produit, mais après l'avoir poussé, j'ai constaté que l'erreur disparaît si les lignes suivantes sont supprimées du fichier resource-config.json (qui a été créé par l'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"},
Micronaut enregistre ces ressources et il semble que le réenregistrement entraîne un double enregistrement de mon contrôleur et une erreur. Si après avoir corrigé le fichier, vous reconstruisez le binaire et l'exécutez, il n'y aura plus d'erreurs, le texte «Hello user» s'affichera sur
http: // localhost: 8080 / hello / user .
Je veux attirer l'attention sur l'utilisation de la ligne suivante dans la classe principale:
Signal.handle(new Signal("INT"), sig -> System.exit(0));
Il doit être inséré pour que Micronaut se termine correctement. Malgré le fait que Micronaut accroche un crochet pour l'arrêter, cela ne fonctionne pas dans le binaire natif. Il y a un problème correspondant:
Shutdownhook ne tire pas avec le natif . Il est marqué comme fixe, mais, en fait, il n'a qu'une solution de contournement utilisant la classe Signal.
Lien vers le référentiel:
Github: native-java-postgres-micronaut-demo .
Connexion ORM
JDBC est bon, mais se fatigue de code répétitif, de SELECT et UPDATE sans fin. J'essaierai de faciliter (ou de compliquer, selon le côté à regarder) ma vie en connectant une sorte d'ORM.
Hibernate
Au début, j'ai décidé d'essayer
Hibernate , car il s'agit de l'un des ORM les plus courants pour Java. Mais je n'ai pas réussi à créer une image native en utilisant Hibernate en raison d'une erreur de construction:
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) ...
Il existe un problème ouvert correspondant:
[native-image] Micronaut + Hibernate entraîne une erreur rencontrée lors de l'analyse de java.lang.reflect.Method.getDefaultValue () .
jOOQ
J'ai alors décidé d'essayer
jOOQ . J'ai réussi à construire un binaire natif, mais j'ai dû faire beaucoup de paramètres: spécifier les classes à initialiser (buildtime, runtime) et jouer avec la réflexion. Au final, tout se résume au fait que lorsque l'application démarre, jOOQ initialise le proxy org.jooq.impl.ParserImpl $ Ignore en tant que membre statique de la classe org.jooq.impl.Tools. Et ce proxy utilise MethodHandle, que Substrate VM
ne prend pas encore en charge . Voici un problème ouvert similaire:
[native-image] Micronaut + Kafka ne parvient pas à créer une image native avec l'argument MethodHandle n'a pas pu être réduit à tout au plus un seul appel .
Apache cayenne
Apache Cayenne est moins commun, mais semble assez fonctionnel. Je vais essayer de le connecter. J'ai créé des fichiers XML pour décrire le schéma de la base de données, ils peuvent être créés manuellement ou à l'aide de l'outil GUI CayenneModeler, ou basés sur une base de données existante. En utilisant le plugin cayenne-maven dans le fichier pom, la génération de code des classes correspondant aux tables de la base de données sera effectuée:
plugin cayenne-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>
J'ai ensuite ajouté la classe CayenneRuntimeFactory pour initialiser la fabrique de contexte de base de données:
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(); } }
Contrôleur 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(","))); } }
Il a ensuite lancé le programme sous la forme d'un pot ordinaire, en utilisant agentlib: native-image-agent, pour collecter des informations sur les ressources utilisées et la réflexion.
J'ai récupéré le binaire natif, l'exécuter, aller à l'adresse
http: // localhost: 8080 / hello / user et obtenir une erreur:
{"message":"Internal Server Error: Provider com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl not found"}
il s'avère agentlib: native-image-agent n'a pas détecté l'utilisation de cette classe en réflexion.
Ajoutée manuellement au fichier reflect-config.json:
{ "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", "allDeclaredConstructors":true }
Encore une fois, je récupère le binaire, démarre, met à jour la page Web et obtient une autre erreur:
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.
Tout est clair ici, j'ajoute le réglage, comme indiqué dans la solution proposée. Encore une fois, je récupère le binaire (cela fait 5 minutes), je le redémarre encore et encore une erreur, une autre:
No DataMap found, can't route query org.apache.cayenne.query.SelectQuery@2af96966[root=class name.voyachek.demos.nativemcp.db.User,name=]"}
J'ai dû bricoler avec cette erreur, après de nombreux tests, en étudiant les sources, il est devenu clair que la raison de l'erreur réside dans cette ligne de la classe org.apache.cayenne.resource.URLResource:
return new URLResource(new URL(url, relativePath));
Il s'est avéré que Substrate VM charge la ressource par l'URL, qui est indiquée comme base, et non par l'URL, qui doit être formée sur la base de la base et du chemin d'accès relatif. Objet du problème suivant enregistré par moi:
Contenu de ressource non valide lors de l'utilisation d'une nouvelle URL (contexte URL, spécification de chaîne) .
L'erreur est déterminée, vous devez maintenant rechercher des solutions de contournement. Heureusement, Apache Cayenne s'est avéré être une chose assez personnalisable. Vous deviez enregistrer votre propre chargeur de ressources:
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();
Voici son 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); } } } }
Il a une ligne
return new URLResource(new URL(url, relativePath));
remplacé par:
String url = getURL().toString(); url = url.substring(0, url.lastIndexOf("/") + 1) + relativePath; return new URLResource(new URI(url).toURL());
Je récupère le binaire (70 Mo), le démarre, je vais sur
http: // localhost: 8080 / hello / user et tout fonctionne, les données de la base de données sont affichées sur la page.
Lien vers le référentiel:
Github: native-micronaut-cayenne-demo .
Conclusions
L'objectif est atteint: une application web simple avec accès à la base de données à l'aide d'ORM a été développée. L'application est compilée en un binaire natif et peut s'exécuter sur des systèmes sans machine Java installée. Malgré de nombreux problèmes, j'ai trouvé une combinaison de cadres, de paramètres et de solutions de contournement qui m'a permis d'obtenir un programme de travail.
Oui, la possibilité de créer des binaires réguliers à partir du code source Java est toujours à l'état expérimental. Cela est évident par l'abondance de problèmes, la nécessité de rechercher des solutions de contournement. Mais en fin de compte, il s'est tout de même avéré atteindre le résultat souhaité. Qu'est-ce que j'ai eu?
- Le seul fichier autonome (presque, il existe des dépendances sur des bibliothèques telles que libc) qui peut fonctionner sur des systèmes sans machine Java.
- L'heure de début est en moyenne de 40 millisecondes contre 2 secondes lors du démarrage d'un bocal ordinaire.
Parmi les lacunes, je voudrais noter le long temps de compilation du binaire natif. Cela me prend en moyenne cinq minutes et augmentera très probablement lors de l'écriture de code et de la connexion des bibliothèques. Par conséquent, il est logique de créer des fichiers binaires basés sur du code entièrement débogué. De plus, les informations de débogage pour les binaires natifs sont disponibles uniquement dans l'édition commerciale de Graal VM - Enterprise Edition.