
Há pouco tempo, a Oracle lançou o primeiro lançamento do projeto GraalVM (https://www.graalvm.org/). O lançamento foi atribuído imediatamente ao número 19.0.0, aparentemente para convencer que o projeto está maduro e pronto para uso em aplicativos sérios. Uma das partes deste projeto:
Substrate VM é uma estrutura que permite transformar aplicativos Java em arquivos executáveis nativos (bem como bibliotecas nativas que podem ser conectadas em aplicativos gravados, por exemplo, em C / C ++). Até agora, esse recurso foi declarado experimental. Também é importante notar que os aplicativos Java nativos têm algumas limitações: você deve listar todos os recursos usados para incluí-los no programa nativo; você precisa listar todas as classes que serão usadas com reflexão e outras restrições. Uma lista completa é fornecida aqui pelo
Native Image Java Limitations . Depois de estudar essa lista, é compreensível em princípio que as restrições não sejam tão significativas que seria impossível desenvolver aplicativos mais complexos do que palavras do inferno. Defino este objetivo: desenvolver um pequeno programa que tenha um servidor da web interno, use um banco de dados (via biblioteca ORM) e compile em um binário nativo que possa ser executado em sistemas sem uma máquina Java instalada.
Vou experimentar no Ubuntu 19.04 (CPU Intel Core i3-6100 a 3.70GHz × 4).
Instale o GraalVM
A instalação do GraalVM é feita convenientemente usando o
SDKMAN . Comando de instalação do GraalVM:
sdk install java 19.0.0-grl
O OpenJDK GraalVM CE 19.0.0 será instalado, o CE é Community Edition. Também existe a Enterprise Edition (EE), mas essa edição precisa ser baixada da Oracle Technology Network, o link está na página de
downloads do
GraalVM .
Depois de instalar o GraalVM, já usando o gerenciador de atualização de componentes gu do GraalVM, instalei o suporte de compilação no binário nativo -
gu install native-image
Tudo, as ferramentas de trabalho estão prontas, agora você pode começar a desenvolver aplicativos.
Aplicativo nativo simples
Como um sistema de construção, eu uso o Maven. Para criar binários nativos, existe um 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>
Ainda é necessário definir a classe principal do aplicativo. Isso pode ser feito no plug-in-image-nativo-maven e da maneira tradicional, via:
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>
Crie a classe principal:
Startup.java public class Startup { public static void main(String[] args) { System.out.println("Hello world!"); } }
Agora você pode executar o comando maven para criar o aplicativo:
mvn clean package
Construir um binário nativo na minha máquina leva 35 segundos. Como resultado, um arquivo binário de tamanho de 2,5 MB é obtido no diretório de destino. O programa não requer a máquina Java instalada e é executado em máquinas nas quais o Java está ausente.
Link do repositório:
Github: native-java-helloworld-demo .
Driver JDBC Postgres
E assim, um aplicativo simples funciona, exibe "Olá, mundo". Não foram necessárias soluções. Vou tentar subir um nível acima: conectarei o driver JDBC do Postgres para solicitar dados do banco de dados.
Os problemas no github do GraalVM encontram bugs relacionados ao driver do Postgres, mas nos candidatos à liberação do GraalVM. Todos eles estão marcados como fixos.
Eu conecto a dependência do postgresql:
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency>
Estou escrevendo o código para extrair dados do banco de dados (a placa do usuário mais simples foi criada):
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")); } } } }
Eu coleciono o binário nativo e recebo imediatamente um erro de compilação:
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
O fato é que o construtor de aplicativos nativo inicializa todos os campos estáticos durante o processo de construção (a menos que seja especificado de outra forma), e faz isso examinando as dependências das classes. Meu código não se refere ao org.postgresql.Driver, portanto, o coletor não sabe como inicializá-lo melhor (ao criar ou quando o aplicativo inicia) e oferece registrá-lo para inicialização ao criar. Isso pode ser feito adicionando-o aos argumentos maven do plugin native-image-maven-plugin, conforme indicado na descrição do erro. Após adicionar o Driver, recebo o mesmo erro relacionado ao org.postgresql.util.SharedTimer. Mais uma vez, coleciono e encontro um erro de construção:
Error: Class initialization failed: org.postgresql.sspi.SSPIClient
Não há recomendações para correção. Mas, olhando para a fonte da classe, fica claro que ela se relaciona com a execução do código no Windows. No Linux, sua inicialização (que ocorre durante a montagem) falha com um erro. Há uma oportunidade para adiar sua inicialização no início do aplicativo: --inicialize no momento da execução = org.postgresql.sspi.SSPIClient. A inicialização no Linux não ocorrerá e não obteremos mais erros relacionados a esta classe. Argumentos de compilação:
<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>
A montagem começou a levar 1 minuto e 20 segundos e o arquivo aumentou até 11 MB. Adicionei um sinalizador adicional para a construção do binário: --no-fallback proíbe a geração de um binário nativo que requer uma máquina Java instalada. Esse binário é criado se o coletor detectar o uso de recursos de idioma que não são suportados na VM do Substrate ou exigir configuração, mas ainda não há configuração. No meu caso, o coletor descobriu o uso potencial de reflexão no driver JDBC. Mas isso é apenas uso potencial, não é necessário no meu programa e, portanto, não é necessária configuração adicional (como fazer isso será mostrado mais adiante). Há também a flag --static, que força o gerador a vincular estaticamente a libc. Mas se você usá-lo, o programa trava com uma falha de segmentação quando você tenta resolver o nome da rede para um endereço IP. Procurei soluções para esse problema, mas não encontrei nada adequado, então deixei a dependência do programa na libc.
Eu executo o binário resultante e recebo o seguinte erro:
Exception in thread "main" org.postgresql.util.PSQLException: Could not find a java cryptographic algorithm: TLS SSLContext not available.
Após algumas pesquisas, a causa do erro foi identificada: O Postgres, por padrão, estabelece uma conexão TLS usando a curva elíptica. O SubstrateVM não inclui a implementação de tais algoritmos para TLS; aqui está o problema em aberto correspondente -
Suporte a TLS ECC binário único (ECDSA / ECDHE) para SubstrateVM . Existem várias soluções: coloque a biblioteca do pacote GraalVM: libsunec.so ao lado do aplicativo, configure a lista de algoritmos no servidor Postgres, exclua os algoritmos Elliptic Curve ou simplesmente desative a conexão TLS no driver Postgres (esta opção foi escolhida):
dataSource.setSslMode(SslMode.DISABLE.value);
Tendo eliminado o erro de criar uma conexão com o Postgres, inicio o aplicativo nativo, ele executa e exibe dados do banco de dados.
Link do repositório:
Github: native-java-postgres-demo .
Estrutura DI e servidor da Web incorporado
Ao desenvolver um aplicativo Java complexo, eles geralmente usam algum tipo de estrutura, por exemplo, Spring Boot. Porém, a julgar por este artigo pelo
suporte à imagem nativa do GraalVM , o trabalho do Spring Boot na imagem nativa “
pronta para o uso” nos é prometido apenas na versão do Spring Boot 5.3.
Mas existe uma estrutura maravilhosa do
Micronaut que
afirma funcionar na imagem nativa do GraalVM . Em geral, conectar um Micronaut a um aplicativo que será montado em um binário não requer configurações especiais ou solução de problemas. De fato, muitas configurações para usar a reflexão e conectar recursos para a Substrate VM já foram feitas no Micronaut. A propósito, as mesmas configurações podem ser colocadas dentro do seu aplicativo no arquivo de configurações META-INF / native-image / $ {groupId} / $ {artifactId} /native-image.properties (esse caminho para o arquivo de configurações é recomendado pelo Substrate VM). Aqui está um exemplo típico conteúdo do arquivo:
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
Os arquivos resource-config.json, reflect-config.json, proxy-config.json contêm configurações para conectar recursos, reflexão e proxies usados (Proxy.newProxyInstance). Esses arquivos podem ser criados manualmente ou recuperados usando agentlib: native-image-agent. No caso de usar o native-image-agent, você precisa executar o jar usual (e não o binário nativo) usando o agente:
java -agentlib:native-image-agent=config-output-dir=output -jar my.jar
em que output é o diretório em que os arquivos descritos acima estarão localizados. Nesse caso, o programa precisa não apenas executar, mas também executar scripts no programa, porque as configurações são gravadas nos arquivos à medida que você usa reflexão, abre recursos, cria um proxy. Esses arquivos podem ser colocados META-INF / native-image / $ {groupId} / $ {artifactId} e referenciados em native-image.properties.
Decidi conectar o log usando o logback: adicionei uma dependência à biblioteca clássica do logback e ao arquivo logback.xml. Depois disso, compilei um jar comum e executei-o usando o agente de imagem nativo. No final do programa, os arquivos de configurações necessários. Se você observar o conteúdo deles, poderá ver que o agente registrou o uso do logback.xml para compilar no binário. Além disso, o arquivo reflection-config.json contém todos os casos de uso da reflexão: para determinadas classes, as informações meta entram no binário.
Em seguida, adicionei uma dependência à biblioteca micronaut-http-server-netty para usar o servidor da Web incorporado baseado em netty e criei um 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); } }
E classe 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); } }
Agora você pode tentar construir um binário nativo. Minha montagem levou 4 minutos. Se você executá-lo e acessar o endereço
http: // localhost: 8080 / hello / user , ocorrerá um erro:
{"_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, não está totalmente claro por que isso acontece, mas após a pesquisa digitando, descobri que o erro desaparece se as seguintes linhas forem removidas do arquivo resource-config.json (criado pelo 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"},
O Micronaut registra esses recursos e parece que o re-registro leva ao dobro do registro do meu controlador e a um erro. Se depois de corrigir o arquivo, você reconstruir o binário e executá-lo, não haverá mais erros, o texto “Hello user” será exibido em
http: // localhost: 8080 / hello / user .
Quero chamar a atenção para o uso da seguinte linha na classe principal:
Signal.handle(new Signal("INT"), sig -> System.exit(0));
Ele precisa ser inserido para que o Micronaut termine corretamente. Apesar do Micronaut travar um gancho para desligar, ele não funciona no binário nativo. Existe um problema correspondente: o
Shutdownhook não está sendo disparado com o nativo . É marcado como fixo, mas, na verdade, possui apenas uma solução alternativa usando a classe Signal.
Link do repositório:
Github: native-java-postgres-micronaut-demo .
Conexão ORM
JDBC é bom, mas cansa de código repetitivo, infinito SELECT e UPDATE. Vou tentar facilitar (ou complicar, dependendo de qual lado procurar) minha vida conectando algum tipo de ORM.
Hibernate
No começo, decidi experimentar o
Hibernate , pois é um dos ORMs mais comuns para Java. Mas não consegui criar uma imagem nativa usando o Hibernate devido a um erro de compilação:
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) ...
Há um problema em aberto correspondente:
[imagem nativa] O Micronaut + Hibernate resulta em Erro encontrado ao analisar java.lang.reflect.Method.getDefaultValue () .
jOOQ
Então eu decidi experimentar o
jOOQ . Eu consegui construir um binário nativo, mas tive que fazer muitas configurações: especificar quais classes inicializar (tempo de construção, tempo de execução) e mexer com a reflexão. No final, tudo se resumiu ao fato de que, quando o aplicativo é iniciado, o jOOQ inicializa o proxy org.jooq.impl.ParserImpl $ Ignore como um membro estático da classe org.jooq.impl.Tools. E esse proxy usa MethodHandle, que o Substrate VM
ainda não oferece suporte . Aqui está uma questão em aberto semelhante:
[imagem nativa] O Micronaut + Kafka falha ao criar imagem nativa com o argumento MethodHandle não pôde ser reduzido para, no máximo, uma única chamada .
Apache cayenne
O Apache Cayenne é menos comum, mas parece bastante funcional. Vou tentar conectá-lo. Criei arquivos XML para descrever o esquema do banco de dados, eles podem ser criados manualmente ou usando a ferramenta GUI CayenneModeler ou com base em um banco de dados existente. Usando o cayenne-maven-plugin no arquivo pom, será realizada a geração de código das classes que correspondem às tabelas do banco de dados:
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>
Em seguida, adicionei a classe CayenneRuntimeFactory para inicializar a fábrica de contexto do banco de dados:
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(","))); } }
Em seguida, ele lançou o programa como um jar comum, usando agentlib: native-image-agent, para coletar informações sobre os recursos utilizados e a reflexão.
Eu coletei o binário nativo, execute-o, vá para o endereço
http: // localhost: 8080 / hello / user e receba um erro:
{"message":"Internal Server Error: Provider com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl not found"}
Acontece que agentlib: native-image-agent não detectou o uso dessa classe na reflexão.
Adicionou-o manualmente ao arquivo reflect-config.json:
{ "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", "allDeclaredConstructors":true }
Mais uma vez, coleciono o binário, inicio, atualizo a página da web e recebo outro erro:
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.
Tudo está claro aqui, eu adiciono a configuração, conforme indicado na solução proposta. Mais uma vez eu coleciono o binário (são 5 minutos), inicio novamente um erro, outro:
No DataMap found, can't route query org.apache.cayenne.query.SelectQuery@2af96966[root=class name.voyachek.demos.nativemcp.db.User,name=]"}
Eu tive que mexer com esse erro, após vários testes, estudando as fontes, ficou claro que o motivo do erro está nessa linha da classe org.apache.cayenne.resource.URLResource:
return new URLResource(new URL(url, relativePath));
Como se viu, o Substrate VM carrega o recurso pela URL, que é indicada como base, e não pela URL, que deve ser formada com base na base e no relativoCaminho. Sobre o que o seguinte problema foi registrado por mim:
Conteúdo de recurso inválido ao usar o novo URL (contexto do URL, especificação de cadeia) .
O erro foi determinado, agora você precisa procurar soluções alternativas. Felizmente, o Apache Cayenne acabou sendo uma coisa bastante personalizável. Você teve que registrar seu próprio carregador 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();
Aqui está o código dele:
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); } } } }
Tem uma linha
return new URLResource(new URL(url, relativePath));
substituído por:
String url = getURL().toString(); url = url.substring(0, url.lastIndexOf("/") + 1) + relativePath; return new URLResource(new URI(url).toURL());
Eu coleciono o binário (70 MB), inicio, vá para
http: // localhost: 8080 / hello / user e tudo funciona, os dados do banco de dados são exibidos na página.
Link do repositório:
Github: native-micronaut-cayenne-demo .
Conclusões
O objetivo foi alcançado: um aplicativo da Web simples com acesso ao banco de dados usando ORM foi desenvolvido. O aplicativo é compilado em um binário nativo e pode ser executado em sistemas sem uma máquina Java instalada. Apesar de vários problemas, encontrei uma combinação de estruturas, configurações e soluções alternativas que me permitiram obter um programa de trabalho.
Sim, a capacidade de criar binários regulares a partir do código-fonte Java ainda está em status experimental. Isso é evidente pela abundância de problemas, pela necessidade de procurar soluções alternativas. Mas, no final, ele acabou atingindo o resultado desejado. O que eu recebi?
- O único arquivo independente (quase, existem dependências em bibliotecas como a libc) que podem ser executadas em sistemas sem uma máquina Java.
- O tempo de início é uma média de 40 milissegundos contra 2 segundos ao iniciar um jar comum.
Entre as deficiências, gostaria de observar o longo tempo de compilação do binário nativo. Demora em média cinco minutos e provavelmente aumentará ao escrever código e conectar bibliotecas. Portanto, faz sentido criar binários com base no código totalmente depurado. Além disso, as informações de depuração para binários nativos estão disponíveis apenas na edição comercial do Graal VM - Enterprise Edition.