借助 Spring Boot 和 GraalVM 实现原生 Java

2022年7月11日 418点热度 0人点赞 0条评论

图片

作者 | Josh Long
译者 | 张卫滨
策划 | 丁晓昀
本文是“Native Compilations Boosts Java”系列文章的一部分。你可以通过 RSS 订阅接收新文章更新通知。

Java 在主导着企业级应用。但是在云中,采用 Java 的成本要比其竞争者更高。使用 GraalVM 进行原生编译降低了在云中 Java 的成本:它所创建的应用启动更快,使用的内存也更少。

原生编译为 Java 用户带来了很多的问题:原生 Java 会如何改变开发方式?我们在什么情况下该转向原生 Java?在什么情况下又该避免转向原生 Java?要使用原生 Java,我们该采用哪个框架?本系列的文章将回答这些问题。
1 Java 社区包罗万象

我们知道,Java 是一个神奇的生态系统,很难列出哪些东西是最适合我们的。这样做的话,清单似乎是无穷无尽的。但是,要说出它的几个缺点也不是那么困难。正如 本系列文章 所说的,在 JRE 上运行的应用往往需要 10 秒钟或更长的时间才能启动,并需要数百或数千兆字节的内存。

这样的性能在如今的世界并不处于领先的位置。有些新的领域和机会正在出现:函数即服务产品、容器化与容器编排。它们有一个共同点,即对启动速度和内存占用有很高的要求。

2 迈向 GraalVM!

GraalVM 提供了一个前进的方向,但它也有一定的代价。GraalVM 是 OpenJDK 的替代方案,它有一个名为 Native Image 的工具,支持预先(ahead-of-time,AOT)编译。

AOT 编译与常规的 Java 编译有一些差异。正如 本系列的第一篇文章 所概述的那样,Native Image 消除了 Java 中“所有不必要的东西”。那么,Native Image 是如何知道 Java 或 Spring Boot 中哪些是不必要的呢?

Native Image 会探查我们的源码,并确定所有可达的代码,也就是通过调用或我们代码的使用所能链接到的代码。其他的所有内容,不管是位于应用的 classpath 下还是位于 JRE 中,都会被视为不必要的,它们都会被抛弃掉。

当我们做了一些 Native Image 无法明确知道该怎么处理的事情时,麻烦就来了。毕竟,Java 是一个非常动态化的语言。我们有可能会创建这样一个 Java 应用:在运行时,将一个字符串编译成文件系统中一个合法 Java 类文件,并将其加载到 ClassLoader 中,然后使用反射创建它的实例或者为其创建代理。我们还可能会将实例序列化到磁盘上,然后将其加载到另外一个 JVM 中。在这个过程中,我们可能不需要链接任何比java.lang.Object更具体的类。但是,如果这些类型没有被放到原生可执行堆中,所有的这些方式在原生 Java 中是无法正常运行的。

但是,我们并没有失去任何东西。我们可以在一个配置文件中告诉 Native Image 要保留哪些类型,这样,在运行时使用反射、代理、classpath 资源加载、JNI 等特性的时候,它依然可以运行。

现在,Java 和 Spring 生态系统非常庞大。所有的东西都要进行配置将会非常痛苦。所以我们有了两种方案:1)教会 Spring 尽可能避免使用这些机制,或者 2)教会 Spring 尽可能多地提供配置文件,这个配置文件必然要包含 Spring 框架和 Spring Boot,并且要在一定程度上包含 Spring Boot 支持的第三方集成功能。友情剧透一下,这两种方案我们都需要!

要运行样例项目,你需要在自己的机器上安装 GraalVM。GraalVM 有 安装指南。如果你使用 Mac 的话,也可以 使用 SDKMAN! 来安装 GraalVM。

3 Spring Native

Spring 团队在 2019 年启动了 Spring Native 项目,为 Spring Boot 生态系统引入了原生可执行程序编译的功能。它已经为多种不同的方式提供了研究场所。但是,Spring Native 并没有从根本上改变 Framework 5.x 或 Spring Boot 2.x。而且它也绝不是终点,只是漫长旅程中的第一步:它已经为下一代 Spring Framework(6.x)和 Spring Boot(3.x)证明了很多概念,这两个版本预计都会在 2022 年晚些时候发布。这些新一代的项目会进行更多的优化,所以前景看起来是非常光明的!鉴于这些版本尚未发布,我们将会在本文中研究一下 Spring Native。

Spring Native 会对发送给 Native Image 的源码进行转换。比如,Spring Native 会将spring.factories服务加载机制转换为静态类,从而使 Spring Native 应用知道要使用它们。它会将所有的 Java 配置类(带有@Configuration注解的类)转换成 Spring 的函数式配置,从而消除应用及其依赖的反射。

Spring Native 还会自动分析我们的代码,探测需要 GraalVM 配置的场景,并以编程的方式提供这些配置。Spring Native 为 Spring、Spring Boot 以及第三方集成提供了线索(hint)类。

4 第一个 Spring Native 应用:JPA、Spring MVC 和 H2

我们开始使用 Spring Native 的方式与所有其他 Spring 项目相同:访问 Spring Initializr,点击 cmd + B(或 Ctrl + B)或者 Add Dependencies,并选择 Spring Native。

Spring Initializr 会配置 Apache Maven 和 Gradle 构建。随后,只需添加必要的依赖即可。我们先从一些典型的依赖开始。将 Artifact 名称改为 jpa,接下来添加如下依赖:Spring NativeSpring WebLombokH2 DatabaseSpring Data JPA。请确保选择 Java 17,当然你也可以选择 Java 11,但这就像你挥舞着一个橡胶做的小鸡满世界乱跑,这看上去非常傻,对吧?点击“Generate”,解压生成的项目并将其导入到你最喜欢的 IDE 中。

这个样例非常简单,将JpaApplication.java类改成如下所示:

package com.example.jpa;
import lombok.*;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Component;import org.springframework.web.bind.annotation.*;import javax.persistence.*;import java.util.Collection;import java.util.stream.Stream;
@SpringBootApplicationpublic class JpaApplication {
public static void main(String[] args) { SpringApplication.run(JpaApplication.class, args); }}
@Componentrecord Initializr(CustomerRepository repository) implements ApplicationRunner {
@Override public void run(ApplicationArguments args) throws Exception { Stream.of("A", "B", "C", "D") .map(c ->; new Customer(null, c)) .map(this.repository::save) .forEach(System.out::println); }}
@RestControllerrecord CustomerRestController(CustomerRepository repository) {
@GetMapping("/customers") Collection<Customer> customers() { return this.repository.findAll(); }}
interface CustomerRepository extends JpaRepository<Customer, Integer> {}
@Entity@Getter@Setter@ToString@NoArgsConstructor@AllArgsConstructor@Table (name = "customer")class Customer { @Id @GeneratedValue private Integer id; private String name;}

我们也可以将测试以原生可执行文件的形式进行编译和运行。但是需要注意的是,有些内容还不能很好的运行,比如 Mockito。我们修改测试类JpaApplicationTests.java,使其如下所示:

package com.example.jpa;
import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.util.Assert;
@SpringBootTestclass JpaApplicationTests {
private final CustomerRepository customerRepository;
@Autowired JpaApplicationTests(CustomerRepository customerRepository) { this.customerRepository = customerRepository; }
@Test void contextLoads() { var size = this.customerRepository.findAll().size(); Assert.isTrue(size > 0, () -> "there should be more than one result!"); }
}

在本文中,我将会展示 macOS 下的命令。对于 Windows 和 Linux,请相应的进行调整。

我们可以按照常规的方式运行应用和测试,比如在终端中运行mvn spring-boot:run命令。直接运行这些样例其实是个不错的主意,至少可以保证应用能够正常运行。但是,这并不是我们的目的。相反,我们想要将应用及其测试编译成 GraalVM 原生应用。

如果你看过 pom.xml 文件的话,你就会发现里面有很多额外的配置,它们搭建了 GraalVM 原生镜像并添加了一个 Maven profile(叫做native)以支持构建原生可执行文件。我们可以使用mvn clean package像以往那样编译应用。也可以使用mvn -Pnative clean package对应用进行原生编译。需要记住的是,你需要将 GraalVM 设置为成自己的 JDK。这个过程会持续几分钟,所以现在是来一杯茶、咖啡、水或其他饮品的时间。我就是这么做的,因为我需要它。当我回来的时候,我看到了如下所示的输出:

...13.9s (16.9% of total time) in 71 GCs | Peak RSS: 10.25GB | CPU load: 5.66...[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time:  03:00 min[INFO] Finished at: 2022-04-28T17:57:56-07:00[INFO] ------------------------------------------------------------------------

我们花费了三分钟的时间来编译原生测试,如果测试成功的话,还会编译原生应用本身。在这个过程中,Native Image 使用了高达 10.25GB 的 RAM。为了加快讲解的速度,在后文中我将会跳过编译和运行测试的过程。所以,当我们编译下面的样例时,将会使用如下的命令:

mvn -Pnative -DskipTests clean package

编译时间因应用的 classpath 不同而有所差异。根据经验,如果跳过编译测试的话,我的大多数构建将会需要 1 分钟到 90 秒的时间。例如,本应用包含了 JPA(和 Hibernate)、Spring Data、H2 数据库、Apache Tomcat 和 Spring MVC。

运行应用:

./target/jpa

在我的机器上,将会看到:

…Started TraditionalApplication in 0.08 seconds (JVM running for 0.082)

非常不错,80 毫秒,也就是千分之八十秒!更棒的,该应用几乎不占用任何内存。我使用如下的脚本来测试应用的 RSS(resident set size)。

#!/usr/bin/env bash  PID=$1RSS=`ps -o rss ${PID} | tail -n1`RSS=`bc <<< "scale=1; ${RSS}/1024"`echo "RSS memory (PID: ${PID}): ${RSS}M"

我们需要正在运行的应用的进程 ID(PID)。在 macOS 上,我可以通过运行pgrep jpa来获取它。我所使用的脚本如下所示:

~/bin/RSS.sh $(pgrep jpa)RSS memory (PID: 35634): 96.9M

大约 97MB 的 RAM。这个数值可能会因运行应用的操作系统和架构的不同而有所差异。在 Intel 上的 Linux 和 M1 上的 macOS 中运行应用时,这个值就是不一样的。与 JRE 应用相比,这当前是一个明显的改进,但依然并不是最好的。

我喜欢反应式编程,而且我认为它更适合我现在的工作负载。我创建了一个类似的反应式应用。它不仅耗费了更少的空间(原因很多,包括 Spring Data R2DBC 支持 Java 17 的 record 语法),应用的编译时间是 1:14(差不多快了两分钟),启动时间是 0.044 秒。它占用的内存少了 35%,大约为 63.5MB。这个应用每秒还可以处理更多的请求。所以,它的编译和执行速度更快,内存效率更高,启动更快并且能够处理更高的流量。我说的是,在各方面这都是一笔不亏的买卖。

5 集成应用

Spring 不仅仅是 HTTP 端点,还有很多其他的东西。它包括很多框架,比如 Spring Batch、Spring Integration、Spring Security、Spring Cloud 以及不断增加的其他框架,它们都提供了对 Spring Native 的良好支持。

我们看一个 Spring Integration 的应用样例。Spring Integration 是一个支持企业级应用集成(enterprise-application integration,EAI)的框架。Gregor Hohpe 和 Bobby Woolf 的开创性著作 Enterprise Integration Patterns 为集成模式提供了通用的术语。Spring Integration 提供了实现这些模式的抽象。

返回 Spring Initializr,将项目命名为 integration,并选择 Java 17,添加Spring NativeSpring IntegrationSpring Web,然后点击Generate。我们需要在pom.xml文件中手动添加一个依赖项:

<dependency>  <groupId>org.springframework.integration</groupId>  <artifactId>spring-integration-file</artifactId>  <version>${spring-integration.version}</version></dependency>

修改IntegrationApplication.java的代码,如下所示:

package com.example.integration;
import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;import org.springframework.integration.dsl.IntegrationFlow;import org.springframework.integration.dsl.IntegrationFlows;import org.springframework.integration.file.dsl.Files;import org.springframework.integration.file.transformer.FileToStringTransformer;import org.springframework.integration.transformer.GenericTransformer;
import java.io.File;
@SpringBootApplicationpublic class IntegrationApplication {
@Bean IntegrationFlow integration(@Value("file://${user.home}/Desktop/integration") File root) { var in = new File(root, "in"); var out = new File(root, "out"); var inboundFileAdapter = Files .inboundAdapter(in) .autoCreateDirectory(true) .get(); var outboundFileAdapter = Files .outboundAdapter(out) .autoCreateDirectory(true) .get(); return IntegrationFlows // .from(inboundFileAdapter, spec -> spec.poller(pm -> pm.fixedRate(1000)))// .transform(new FileToStringTransformer()) .transform((GenericTransformer<String, String>) source -> new StringBuilder(source) .reverse() .toString() .trim()) .handle(outboundFileAdapter) .get(); }
public static void main(String[] args) { SpringApplication.run(IntegrationApplication.class, args); } }

这个应用非常简单:它会监控一个目录($HOME/Desktop/integration/in)中的新文件。一旦发现新文件,它就会创建一个副本,其String内容与源文件恰好相反,并将其写入到$HOME/Desktop/integration/out中。在 JRE 上,该应用的启动时间为 0.429 秒。这已经非常不错了,接下来我们看一下将其转换成 GraalVM 可执行文件会带来什么变化。

mvn -Pnative -DskipTests clean package

该应用的编译时间为 55.643 秒。它的启动时间(./target/integration)为 0.029 秒,占用了 35.5MB 的 RAM。很不错!

我们可以看到,没有所谓的典型结果。编译过程的输入对输出有着很大的影响。

6 将应用带入生产环境

在某个时间点,我们可能希望将应用部署到生产环境中,如今典型的生产环境就是 Kubernetes 了。Kubernetes 以容器的方式运行。Buildpacks 项目 背后的核心概念是集中和重用将应用制品转换成容器的习惯性做法。使用 Buildpacks 的方式有很多,可以借助 pack CLI,也可以在 Kubernetes 集群中使用 KPack,还可以使用 Spring Boot 的构建插件。我们将使用最后一种方式,因为它仅需要 Docker Desktop 即可。请 点击官网 了解 Docker Desktop 的更多信息。

mvn spring-boot:build-image

该命令会在容器内构建原生可执行文件,所以我们会得到一个包含 Linux 原生二进制文件的 Linux 容器。随后,我们可以通过 docker tag 和 docker push 为其添加标签并推送至所选择的容器 registry 中。当我在 2022 年 5 月撰写这篇文章的时候,在 M1 架构的 Mac 上,Docker Buildpacks 仍然有点不稳定。但我相信这很快就会得到解决。

7 为 Native Image 提供一些线索

在到目前为止所看到的样例中,为了让应用能够以原生可执行文件的形式运行,我们并没有做其他更多的事情。按照上述默认的配置,它自然就可以运行。在大多数情况下,这种易用性就是我们期望的结果。但有时候,我们需要给 Native Image 提供一些线索,正如我在前面的“迈向 GraalVM!”章节所提到的那样。

我们看一下另外一个样例。首先,进入 Spring Initializr,将项目命名为 extensions,选择 Java 17 并添加Spring Native,然后点击Generate。接下来,我们会手动添加一个在 Initialzr 上不存在的依赖项:

<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-json</artifactId></dependency>

我们在这里的目标是看一下当出错的时候,会发生些什么。Spring Native 提供了一组线索,允许我们很容易地增强默认的配置。将ExtensionsApplication.java修改为如下所示:

package com.example.extensions;
import com.fasterxml.jackson.core.type.TypeReference;import com.fasterxml.jackson.databind.ObjectMapper;import org.aopalliance.intercept.MethodInterceptor;import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.boot.*;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.core.io.ClassPathResource;import org.springframework.nativex.hint.*;import org.springframework.stereotype.Component;import org.springframework.util.*;
import java.io.InputStreamReader;import java.util.List;import java.util.function.Supplier;
@SpringBootApplicationpublic class ExtensionsApplication { public static void main(String[] args) { SpringApplication.run(ExtensionsApplication.class, args); }}
@Componentclass ReflectionRunner implements ApplicationRunner {
private final ObjectMapper objectMapper ;
ReflectionRunner(ObjectMapper objectMapper) { this.objectMapper = objectMapper; }
record Customer(Integer id, String name) { }
@Override public void run(ApplicationArguments args) throws Exception { var json = """ [ { "id" : 2, "name": "Dr. Syer"} , { "id" : 1, "name": "Jürgen"} , { "id" : 4, "name": "Olga"} , { "id" : 3, "name": "Violetta"} ] """; var result = this.objectMapper.readValue(json, new TypeReference<List<Customer>>() { }); System.out.println("there are " + result.size() + " customers."); result.forEach(System.out::println); }}
@Componentclass ResourceRunner implements ApplicationRunner {
@Override public void run(ApplicationArguments args) throws Exception { var resource = new ClassPathResource("Log4j-charsets.properties"); Assert.isTrue(resource.exists(), () -> "the file must exist"); try (var in = new InputStreamReader(resource.getInputStream())) { var contents = FileCopyUtils.copyToString(in); System.out.println(contents.substring(0, 100) + System.lineSeparator() + "..."); } }}
@Componentclass ProxyRunner implements ApplicationRunner {
private static Animal buildAnimalProxy(Supplier<String> greetings) { var pfb = new ProxyFactoryBean(); pfb.addInterface(Animal.class); pfb.addAdvice((MethodInterceptor) invocation -> { if (invocation.getMethod().getName().equals("speak")) System.out.println(greetings.get());
return null; }); return (Animal) pfb.getObject(); }
@Override public void run(ApplicationArguments args) throws Exception { var cat = buildAnimalProxy(() -> "meow!"); cat.speak();
var dog = buildAnimalProxy(() -> "woof!"); dog.speak(); }
interface Animal { void speak(); }}

这个样例包含了三个 ApplicationRunner 实例,Spring 应用在启动的时候会运行它们。每个 Bean 都会做一些让 GraalVM Native Image 感觉不爽的事情。但是,在 JVM 上,它们能够很好地运行:mvn spring-boot:run。

第一个ApplicationRunner,即ReflectionRunner,会读取 JSON 数据并使用反射将它的结构映射到一个 Java 类Customer上。它无法正常运行,因为 Native Image 将会移除Customer类。使用mvn -Pnative -DskipTests clean package构建应用,并使用./target/extensions运行它。我们将会看到“com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces”这样的错误。

我们可以使用@TypeHint注解来修复该问题。添加如下的内容到ExtensionsApplication类上:

@TypeHint(types =  ReflectionRunner.Customer.class, access = { TypeAccess.DECLARE

在这里,我们声明我们希望对ReflectionRunner.Customer的构造器和方法进行反射访问。对于不同类型的反射,还有其他的TypeAccess值。

第二个ApplicationRunnerResourceRunner,会从 classpath 下某个依赖的.jar中加载文件。它也无法正常运行,并且会提示“java.lang.IllegalArgumentException: the file must exist”这样的错误。原因在于该文件位于其他的.jar中,而不是在我们的应用代码中。如果文件位于src/main/resources中的话,加载资源是可以正常运行的。我们可以使用@ResourceHint注解来解决这个问题。将如下的内容添加到ExtensionsApplication类中:

@ResourceHint(patterns = "Log4j-charsets.properties", isBundle = false)

第三个ApplicationRunner,即ProxyRunner,创建了一个 JDK 代理。代理会创建相关类型的子类或实现类。Spring 支持两种类型的代理,即 JDK 代理和 AOT 代理。JDK 代理仅限于使用 Java java.lang.reflect.Proxy的接口。AOT 代理则是 Spring 特有的,并不是 JRE 的一部分。JDK 代理通常是给定具体类的子类,也可能是接口。Native Image 需要知道我们的代理要使用哪些接口和具体类。

继续将第三个应用编译为原生可执行文件。Native Image 将会给出一条友好的错误信息“com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces”,并且会列出所有 Spring 试图要代理的接口。请注意这些类型:com.example.extensions.ProxyRunner.Animalorg.springframework.aop.SpringProxyorg.springframework.aop.framework.Advisedorg.springframework.core.DecoratingProxy。我们将会使用它们为ExtensionsApplication添加如下的线索:

@JdkProxyHint(types = {    com.example.extensions.ProxyRunner.Animal.class,    org.springframework.aop.SpringProxy.class,    org.springframework.aop.framework.Advised.class,    org.springframework.core.DecoratingProxy.class})

如果你现在尝试构建(mvn -DskipTests -Pnative clean package)并运行(./target/extensions)样例的话,就不会有任何问题了。

8 构建期和运行期的 Processor

Spring 有很多的Processor实现。Spring Native 提供了一些新的Processor接口,它们只会在构建期激活。这些Processor会动态地为构建过程提供线索信息。理想情况下,这些Processor的实现会位于一个可重用的库中。访问 Spring Initializr,将项目命名为 processors,并添加Spring Native。在 IDE 中打开生成的项目,在pom.xml文件中移除build节点,这样会删除所有的 Maven 插件配置。接下来,我们需要手动添加一个新的库:

<dependency>  <groupId>org.springframework.experimental</groupId>  <artifactId>spring-aot</artifactId>  <version>${spring-native.version}</version>  <scope>provided</scope></dependency>

Maven 构建会生成一个常规的 Java “.jar”制品,我们可以像对待任意 Maven “.jar”那样对其进行安装和部署:mvn -DskipTests clean install。

这个新的库会引入新的类型,包括:

  • BeanFactoryNativeConfigurationProcessor:它在构建期的行为等同于BeanFactoryPostProcessor

  • BeanNativeConfigurationProcessor:它在构建期的行为等同于BeanPostProcessor

我发现自己大多数时候都在和这两个接口打交道。在每个接口中,我们都可以得到一个可供探测的引用以及一个注册表的引用,我们据此能够以编程的方式向注册表中贡献线索内容。如果使用BeanNativeConfigurationProcessor,我们会得到一个 bean 元数据的实例,它代表了 bean factory 中的一个 bean。如果使用BeanFactoryNativeConfigurationProcessor的话,我们会得到对整个BeanFactory本身的引用。需要注意的是,我们只能使用 bean 的名称和BeanDefinition实例,无法使用真正的 bean。BeanFactory能够知道所有在运行时会存在的对象,但是它此时还没有实例化它们。相反,它的作用是帮助我们理解运行中的应用中 bean 的样子,比如类、方法等,以便于得到适当的线索信息。

我们不能以常规 Spring bean 的形式来注册这些Processor类型,而是要在spring.factories服务加载器中进行注册。所以,鉴于BeanFactoryNativeConfigurationProcessor的实现名为com.example.nativex.MyBeanFactoryNativeConfigurationProcessorBeanNativeConfigurationProcessor的实现名为com.example.nativex.MyBeanNativeConfigurationProcessor,spring.factories文件如下所示:

org.springframework.aot.context.bootstrap.generator.infrastructure.nativex.BeanFactoryNativeConfigurationProcessor=\  com.example.nativex.MyBeanFactoryNativeConfigurationProcessororg.springframework.aot.context.bootstrap.generator.infrastructure.nativex.BeanNativeConfigurationProcessor=\  com.example.nativex.MyBeanNativeConfigurationProcessor

借助这些 Processor 类型,我们可以很容易地在 Spring Native 应用中消费集成功能或库。我写了一个库(com.joshlong:hints:0.0.1),里面包含了各种集成功能(如 Kubernetes Java 客户端、Fabric8 Kubernetes Java 客户端、Spring GraphQL、Liquibase 等),这些集成功能不大适合放到官方的 Spring Native 版本中。目前这就是一个大杂烩,但结果是很酷的:只要把相关的功能添加到 classpath 中,就像 Spring Boot 的自动配置一样,我们就会得到一个很棒的结果!

9 更多信息

我希望你能够从这个关于 Spring Native 原生可执行文件的简单介绍中有所收获。请继续关注 Spring 博客 和我的 Twitter (@starbuxman) ,以获取更多信息。

作者介绍:

Josh Long(Twitter 为 @starbuxman)是第一个 Spring 开发者倡导者,始于 2010 年。Josh 是一个 Java Champion,写了 6 本图书(包括 O'Reilly 的“Cloud Native Java: Designing Resilient Systems with Spring Boot, Spring Cloud, and Cloud Foundry”和“Reactive Spring”)和制作了许多畅销的视频培训(包括与 Spring Boot 联合创始人 Phil Webb 合作的“Building Microservices with Spring Boot Livelessons”),并且是开源贡献者(Spring Boot、Spring Integration, Spring Cloud、Activiti 和 Vaadin 等)、播客(“A Bootiful Podcast”)和 YouTuber。

原文链接:

https://www.infoq.com/articles/native-java-spring-boot/

点击底部阅读原文 访问 InfoQ 官网,获取更多精彩内容!

今日好文推荐

微软开始封禁商业开源:从 App Store 入手,7 月 16 日生效?!

迁移进行时,告别 GitHub 的时候到了?

腾讯安全回应数据产品线裁撤;马斯克称终止收购推特;拼多多“砍一刀”涉嫌欺诈案一审宣判 |Q 资讯

GitLab 技术选型为何如此不同:坚持用过气 Web 框架十多年、坚决不用微服务

图片

点个在看少个 bug ?

38470借助 Spring Boot 和 GraalVM 实现原生 Java

这个人很懒,什么都没留下

文章评论