Imagen nativa de Java: comprobación de usabilidad



No hace mucho tiempo, Oracle lanzó la primera versión del proyecto GraalVM (https://www.graalvm.org/). Al lanzamiento se le asignó inmediatamente el número 19.0.0, aparentemente para convencer de que el proyecto está maduro y listo para usar en aplicaciones serias. Una de las partes de este proyecto: Substrate VM es un marco que le permite convertir aplicaciones Java en archivos ejecutables nativos (así como bibliotecas nativas que pueden conectarse en aplicaciones escritas, por ejemplo, en C / C ++). Esta característica hasta ahora ha sido declarada experimental. También vale la pena señalar que las aplicaciones Java nativas tienen algunas limitaciones: debe enumerar todos los recursos utilizados para incluirlos en el programa nativo; debe enumerar todas las clases que se usarán con reflexión y otras restricciones. Aquí se proporciona una lista completa de las limitaciones de Java de imágenes nativas . Habiendo estudiado esta lista, es comprensible en principio que las restricciones no sean tan significativas como para que sea imposible desarrollar aplicaciones más complejas que las palabras infernales. Establecí este objetivo: desarrollar un pequeño programa que tenga un servidor web incorporado, use una base de datos (a través de la biblioteca ORM) y compile en un binario nativo que pueda ejecutarse en sistemas sin una máquina Java instalada.

Experimentaré en Ubuntu 19.04 (Intel Core i3-6100 CPU @ 3.70GHz × 4).

Instalar GraalVM


La instalación de GraalVM se realiza convenientemente con SDKMAN . Comando de instalación de GraalVM:

sdk install java 19.0.0-grl 

Se instalará OpenJDK GraalVM CE 19.0.0, CE es Community Edition. También hay Enterprise Edition (EE), pero esta edición debe descargarse de Oracle Technology Network, el enlace se encuentra en la página de descargas de GraalVM .

Después de instalar GraalVM, ya usando el administrador de actualizaciones de componentes gu de GraalVM, instalé el soporte de compilación en el binario nativo:

 gu install native-image 

Todo, las herramientas de trabajo están listas, ahora puede comenzar a desarrollar aplicaciones.

Aplicación nativa simple


Como sistema de compilación, uso Maven. Para crear binarios nativos, hay un complemento maven:

plugin de imagen-nativa-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> 


Todavía es necesario establecer la clase principal de la aplicación. Esto puede hacerse tanto en native-image-maven-plugin como en la forma tradicional, a través de:

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> 


Crea la clase principal:

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


Ahora puede ejecutar el comando maven para construir la aplicación:

 mvn clean package 

Construir un binario nativo en mi máquina lleva 35 segundos. Como resultado, se obtiene un archivo binario de 2.5 MB de tamaño en el directorio de destino. El programa no requiere la máquina Java instalada y se ejecuta en máquinas donde falta Java.

Enlace al repositorio: Github: native-java-helloworld-demo .

Controlador JDBC Postgres


Y así, una aplicación simple funciona, muestra "Hola mundo". No se necesitaban soluciones. Intentaré ir un nivel más arriba: conectaré el controlador JDBC de Postgres para solicitar datos de la base de datos. Los problemas en el github de GraalVM se encuentran con errores relacionados con el controlador Postgres, pero con los candidatos de lanzamiento de GraalVM. Todos ellos están marcados como fijos.

Conecto la dependencia postgresql:

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

Estoy escribiendo el código para extraer datos de la base de datos (se creó la placa de usuario más simple):

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")); } } } } 


Recopilo el binario nativo e inmediatamente recibo un error de compilación:

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


El hecho es que el creador de aplicaciones nativas inicializa todos los campos estáticos durante el proceso de compilación (a menos que se especifique lo contrario), y lo hace examinando las dependencias de las clases. Mi código no hace referencia a org.postgresql.Driver, por lo que el recopilador no sabe cómo inicializarlo mejor (al compilar, o cuando se inicia la aplicación) y ofrece registrarlo para la inicialización al compilar. Esto se puede hacer agregándolo a los argumentos de Maven del plugin native-image-maven-plugin, como se indica en la descripción del error. Después de agregar Driver, obtengo el mismo error relacionado con org.postgresql.util.SharedTimer. Nuevamente, recopilo y encuentro un error de compilación:

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


No hay recomendaciones para la corrección. Pero, mirando el origen de la clase, está claro que se relaciona con la ejecución de código en Windows. En Linux, su inicialización (que ocurre durante el ensamblaje) falla con un error. Existe la oportunidad de retrasar su inicialización al inicio de la aplicación: --initialize-at-run-time = org.postgresql.sspi.SSPIClient. La inicialización en Linux no ocurrirá y ya no obtendremos errores relacionados con esta clase. Argumentos de construcción:

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

El ensamblaje comenzó a tomar 1 minuto y 20 segundos y el archivo aumentó a 11 MB. Agregué un indicador adicional para construir el binario: --no-fallback prohíbe generar un binario nativo que requiera una máquina Java instalada. Dicho binario se crea si el recopilador detecta el uso de características de lenguaje que no son compatibles con la VM de sustrato o requieren configuración, pero aún no hay configuración. En mi caso, el recopilador descubrió el uso potencial de la reflexión en el controlador JDBC. Pero esto es solo un uso potencial, no se requiere en mi programa y, por lo tanto, no se requiere configuración adicional (cómo se mostrará más adelante). También está el indicador --static, que obliga al generador a vincular estáticamente libc. Pero si lo usa, el programa se bloquea con un error de segmentación cuando intenta resolver el nombre de la red a una dirección IP. Busqué alguna solución a este problema, pero no encontré nada adecuado, así que dejé la dependencia del programa en libc.

Ejecuto el binario resultante y obtengo el siguiente error:

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


Después de investigar un poco, se identificó la causa del error: Postgres establece de forma predeterminada una conexión TLS utilizando la curva elíptica. SubstrateVM no incluye la implementación de dichos algoritmos para TLS, aquí está el problema abierto correspondiente : compatibilidad TLS de ECC de un solo binario (ECDSA / ECDHE) para SubstrateVM . Hay varias soluciones: coloque la biblioteca del paquete GraalVM: libsunec.so al lado de la aplicación, configure la lista de algoritmos en el servidor Postgres, excluya los algoritmos de curva elíptica o simplemente desactive la conexión TLS en el controlador Postgres (se eligió esta opción):

 dataSource.setSslMode(SslMode.DISABLE.value); 

Después de eliminar el error de crear una conexión con Postgres, inicio la aplicación nativa, se ejecuta y muestra datos de la base de datos.

Enlace al repositorio: Github: native-java-postgres-demo .

Marco DI y servidor web incorporado


Al desarrollar una aplicación Java compleja, generalmente usan algún tipo de marco, por ejemplo, Spring Boot. Pero a juzgar por este artículo del soporte de imágenes nativas de GraalVM , el trabajo de Spring Boot en la imagen nativa " listo para usar " se nos promete solo en Spring Boot 5.3.

Pero hay un marco maravilloso de Micronaut que dice funcionar en la imagen nativa de GraalVM . En general, conectar un Micronaut a una aplicación que se ensamblará en un binario no requiere ninguna configuración especial o resolución de problemas. De hecho, muchas configuraciones para usar la reflexión y conectar recursos para la máquina virtual Substrate ya están hechas dentro de Micronaut. Por cierto, la misma configuración se puede colocar dentro de su aplicación en el archivo de configuración META-INF / native-image / $ {groupId} / $ {artifactId} /native-image.properties (Substrate VM recomienda esta ruta para el archivo de configuración), aquí hay una típica contenido del archivo:

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 


Los archivos resource-config.json, reflect-config.json, proxy-config.json contienen configuraciones para conectar los recursos, la reflexión y los proxies utilizados (Proxy.newProxyInstance). Estos archivos se pueden crear manualmente o recuperar utilizando agentlib: native-image-agent. En el caso de usar native-image-agent, debe ejecutar el jar habitual (y no el binario nativo) usando el agente:

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

donde salida es el directorio donde se ubicarán los archivos descritos anteriormente. En este caso, el programa no solo necesita ejecutarse, sino también ejecutar scripts en el programa, porque la configuración se escribe en los archivos a medida que usa la reflexión, abre los recursos y crea un proxy. Estos archivos se pueden colocar META-INF / native-image / $ {groupId} / $ {artifactId} y hacer referencia en native-image.properties.

Decidí conectar el registro utilizando logback: agregué una dependencia a la biblioteca logback-classic y al archivo logback.xml. Después de eso, compilé un jar normal y lo ejecuté usando native-image-agent. Al final del programa, los archivos de configuración necesarios. Si observa su contenido, puede ver que el agente registró el uso de logback.xml para compilar en el binario. Además, el archivo reflection-config.json contiene todos los casos de uso de la reflexión: para determinadas clases, la metainformación entrará en el binario.

Luego agregué una dependencia a la biblioteca micronaut-http-server-netty para usar el servidor web incorporado basado en netty y creé un controlador:

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


Y clase principal:

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


Ahora puedes intentar construir un binario nativo. Mi asamblea tomó 4 minutos. Si lo ejecuta y va a la dirección http: // localhost: 8080 / hello / user , se produce un error:

 {"_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"} 

Honestamente, no está del todo claro por qué sucede esto, pero después de buscar escribiendo, descubrí que el error desaparece si las siguientes líneas se eliminan del archivo resource-config.json (que fue creado por el agente):

  {"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 registra estos recursos y parece que el nuevo registro conduce a un doble registro de mi controlador y un error. Si después de corregir el archivo, reconstruye el binario y lo ejecuta, no habrá más errores, el texto "Hola usuario" se mostrará en http: // localhost: 8080 / hello / user .

Quiero llamar la atención sobre el uso de la siguiente línea en la clase principal:

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

Debe insertarse para que Micronaut termine correctamente. A pesar de que Micronaut cuelga un gancho para apagarse, no funciona en el binario nativo. Hay un problema correspondiente: Shutdownhook no dispara con nativo . Está marcado como fijo, pero, de hecho, solo tiene una solución alternativa usando la clase Signal.

Enlace al repositorio: Github: native-java-postgres-micronaut-demo .

Conexión ORM


JDBC es bueno, pero se cansa de código repetitivo, interminable SELECT y UPDATE. Intentaré facilitar (o complicar, dependiendo de qué lado mirar) mi vida conectando algún tipo de ORM.

Hibernar


Al principio decidí probar Hibernate , ya que es uno de los ORM más comunes para Java. Pero no pude construir una imagen nativa usando Hibernate debido a un error de compilación:

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

Hay un problema abierto correspondiente: [imagen nativa] Micronaut + Hibernate resulta en un error encontrado al analizar java.lang.reflect.Method.getDefaultValue () .

jOOQ


Entonces decidí probar jOOQ . Logré construir un binario nativo, aunque tuve que hacer una gran cantidad de configuraciones: especificar qué clases inicializar (tiempo de construcción, tiempo de ejecución) y jugar con la reflexión. Al final, todo se redujo al hecho de que cuando se inicia la aplicación, jOOQ inicializa el proxy org.jooq.impl.ParserImpl $ Ignorar como miembro estático de la clase org.jooq.impl.Tools. Y este proxy usa MethodHandle, que Substrate VM aún no admite . Aquí hay un problema abierto similar: [imagen nativa] Micronaut + Kafka no puede construir una imagen nativa con el argumento MethodHandle no se puede reducir a lo más en una sola llamada .

Apache Cayena


Apache Cayenne es menos común, pero parece bastante funcional. Intentaré conectarlo. Creé archivos XML para describir el esquema de la base de datos, se pueden crear manualmente o con la herramienta GUI CayenneModeler, o en base a una base de datos existente. Usando el plugin cayenne-maven-en el archivo pom, se llevará a cabo la generación de código de clases que corresponden a las tablas de la base de datos:

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> 


Luego agregué la clase CayenneRuntimeFactory para inicializar la fábrica de contexto de la base de datos:

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(); } } 


Controlador 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(","))); } } 


Luego lanzó el programa como un jar regular, usando agentlib: native-image-agent, para recopilar información sobre los recursos utilizados y la reflexión.

Recopilé el binario nativo, ejecútelo, vaya a la dirección http: // localhost: 8080 / hello / user y obtenga un error:

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

Resulta que agentlib: native-image-agent no detectó el uso de esta clase en la reflexión.

Lo agregó manualmente al archivo reflect-config.json:

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

Nuevamente, recopilo el binario, inicio, actualizo la página web y obtengo otro 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. 

Aquí todo está claro, agrego la configuración, como se indica en la solución propuesta. Nuevamente colecciono el binario (esto es 5 minutos de tiempo), lo inicio una y otra vez un error, otro:

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

Tuve que jugar con este error, después de numerosas pruebas, estudiando las fuentes, quedó claro que la razón del error radica en esta línea de la clase org.apache.cayenne.resource.URLResource:

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

Al final resultó que, Substrate VM carga el recurso por la url, que se indica como la base, y no por la url, que debe formarse sobre la base de la base y la ruta relativa. Sobre lo que he registrado el siguiente problema: Contenido de recurso no válido cuando se usa una nueva URL (contexto de URL, especificación de cadena) .

El error está determinado, ahora debe buscar soluciones alternativas. Afortunadamente, Apache Cayenne resultó ser una cosa bastante personalizable. Debes registrar tu propio cargador de recursos:

 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(); 

Aquí está su código:

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


Tiene una linea

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

reemplazado por:

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

Recopilo el binario (70 MB), lo inicio, voy a http: // localhost: 8080 / hello / user y todo funciona, los datos de la base de datos se muestran en la página.

Enlace al repositorio: Github: native-micronaut-cayenne-demo .

Conclusiones


El objetivo se ha logrado: se ha desarrollado una aplicación web simple con acceso a la base de datos utilizando ORM. La aplicación se compila en un binario nativo y puede ejecutarse en sistemas sin una máquina Java instalada. A pesar de numerosos problemas, encontré una combinación de marcos, configuraciones y soluciones que me permitieron obtener un programa de trabajo.

Sí, la capacidad de crear binarios regulares a partir del código fuente de Java todavía está en estado experimental. Esto es evidente por la abundancia de problemas, la necesidad de buscar soluciones. Pero al final, todavía resultó lograr el resultado deseado. ¿Qué obtuve?

  • El único archivo autónomo (casi, hay dependencias en bibliotecas como libc) que pueden ejecutarse en sistemas sin una máquina Java.
  • El tiempo de inicio es un promedio de 40 milisegundos versus 2 segundos cuando se inicia un frasco normal.

Entre las deficiencias, me gustaría señalar el largo tiempo de compilación del binario nativo. Me lleva un promedio de cinco minutos, y lo más probable es que aumente al escribir código y conectar bibliotecas. Por lo tanto, tiene sentido crear archivos binarios basados ​​en código completamente depurado. Además, la información de depuración para los binarios nativos solo está disponible en la edición comercial de Graal VM - Enterprise Edition.

Source: https://habr.com/ru/post/454790/


All Articles