2024年精彩纷呈的Spring Boot(第一部分)

工程 | Josh Long | 2024年3月11日 | ...

注意:代码在我的Github账户上这里github.com/joshlong/bootiful-spring-boot-2024-blog

嗨,Spring 粉丝们!我是Josh Long,我在Spring团队工作。我很高兴今年能够在微软的JDConf上做主题演讲和发表演讲。我是一位Kotlin GDE和Java Champion,我认为现在是成为Java和Spring Boot开发人员的最佳时机。我完全意识到我们今天所处的位置。自从Spring框架最早发布以来已经21年以上了,自从Spring Boot最早发布以来已经11年以上了。今年是Spring框架发布20周年和Spring Boot发布10周年。因此,当我说是成为Java和Spring开发人员的最佳时机时,请记住我已经在这个领域工作了大部分时间。我热爱Java和JVM,我热爱Spring,这真是太棒了。

但这是最好的时机。从未如此接近。因此,让我们像往常一样,通过访问我在互联网上第二喜欢的地方(仅次于生产环境)start.spring.io来开发一个新应用程序,我们将看到我的意思。点击“添加依赖项”,然后选择“Web”、“Spring Data JDBC”、“OpenAI”、“GraalVM Native Support”、“Docker Compose”、“Postgres”和“Devtools”。

给它起一个工件名称。我将我的服务命名为……“service”。我擅长起名字。这是我从我父亲那里继承的。我父亲也擅长起名字。当我还是个小男孩的时候,我们有一只小小的白色小狗,我父亲给它取名为“小白狗”。多年来,它是我们的家庭宠物。但大约十年后,它消失了。我不知道它后来怎么样了。也许它找到了一份工作;我不知道。但后来奇迹般地,另一只小白狗出现在我们家门口。所以我们收留了它,我父亲给它取名为“Too”或“Two”。我不知道。总之,他非常擅长起名字。也就是说,我妈妈总是告诉我,我很幸运她给我取名为……是的,这可能是真的。

无论如何,选择Java 21。这部分是关键。如果不使用Java 21,就无法使用Java 21。所以,你需要Java 21。但我们也将使用GraalVM及其原生镜像功能。

没有安装Java 21?下载它!使用很棒的SDKMAN工具:sdk install java 21-graalce。然后将其设置为默认值:sdk default java 21-graalce。打开一个新的shell。下载.zip文件。

Java 21太棒了。它比Java 8好得多。它在各个方面都技术上更胜一筹。它更快、更健壮、语法更丰富。它在道德上也更优越。当你的孩子看到你在生产环境中使用Java 8时,你不会喜欢他们眼中羞愧和耻辱的表情。不要这样做。成为你希望看到的改变。使用Java 21。

你将得到一个zip文件。解压它并在你的IDE中打开它。

我使用的是IntelliJ IDEA,它安装了一个名为idea的命令行工具。

cd service
idea build.gradle 
# idea pom.xml if you're using Apache Maven

如果你使用的是Visual Studio Code,请确保在Visual Studio Code Marketplace上安装Spring Boot Extension Pack

这个新应用程序将与数据库进行通信;它是一个以数据为中心的应用程序。在Spring Initializr中,我们添加了对PostgreSQL的支持,但现在我们需要连接到它。我们最不想要的是一个很长的README.md文件,其中包含一个标题为“一百个简单的开发步骤”的部分。我们想体验一下`git clone` & Run的生活!

为此,Spring Initializr生成一个Docker Compose compose.yml文件,其中包含PostgreSQL(这个很棒的SQL数据库)的定义。

Docker Compose文件,compose.yaml

services:
  postgres:
    image: 'postgres:latest'
    environment:
      - 'POSTGRES_DB=mydatabase'
      - 'POSTGRES_PASSWORD=secret'
      - 'POSTGRES_USER=myuser'
    ports:
      - '5432'

更好的是,Spring Boot配置为在Spring Boot应用程序启动时自动运行Docker Compose(docker compose up)配置。无需配置连接详细信息,例如spring.datasource.urlspring.datasource.password等。所有这些都通过Spring Boot令人惊叹的自动配置完成。太棒了!而且,为了不留下任何垃圾,Spring Boot也会在应用程序关闭时关闭Docker容器。

我们希望尽快行动。为此,我们在Spring Initializr中选择了DevTools。它将使我们能够快速行动。这里的核心概念是重启Java非常慢。但是,重启Spring非常快。那么,如果我们有一个进程来监控我们的项目文件夹,并且可以注意到新编译的.class文件,将它们加载到类加载器中,然后创建一个新的Spring ApplicationContext,丢弃旧的,给我们一种实时重载的错觉呢?这正是Spring的DevTools所做的。在开发过程中运行它,看看你的重启时间减少了多少!

这是有效的,因为Spring启动速度非常快……除了每次重启时都要启动PostgreSQL数据库。我喜欢PostgreSQL,但是,是的,它不适合每次调整方法名称、修改HTTP端点路径或完善一些CSS时都不断重启。因此,让我们配置Spring Boot来简单地启动Docker Compose文件,并让它一直运行,而不是每次都重启。

将属性添加到application.properties

spring.docker.compose.lifecycle-management=start_only

我们将从一个简单的记录开始。

package com.example.service;

import org.springframework.data.annotation.Id;

// look mom, no Lombok!
record Customer(@Id Integer id, String name) {
}

我爱Java记录!你也应该爱!不要忽视记录。这个不起眼的record不仅仅是比Lombok的@Data注解更好的方法,它实际上是一系列功能的一部分,这些功能在Java 21中达到顶峰,共同支持称为“面向数据编程”的东西。

Java语言架构师Brian Goetz在2022年关于面向数据编程的InfoQ文章中谈到了这一点。

人们认为Java主导了整体式应用程序的世界,因为它强大的访问控制、良好且快速的编译器、隐私保护等等。Java使创建相对模块化、可组合的整体式应用程序变得容易。整体式应用程序通常是大型、庞大的代码库,而Java支持它。事实上,如果你想要模块化并希望很好地构建大型整体式代码库,请查看Spring Modulith项目。

但情况已经发生了变化。如今,我们表达系统变化的方式不再是深层抽象类型层次结构的专门实现(通过动态分派和多态性),而是通过经常临时发送的跨网络消息,通过HTTP/REST、gRPC、消息基元(如Apache Kafka和RabbitMQ)等。这是数据,傻瓜!

Java已经发展到支持这些新的模式。让我们看看四个关键特性——记录、模式匹配、智能switch表达式和密封类型——看看我的意思。假设我们在一个受严格监管的行业工作,例如金融业。

假设我们有一个名为Loan的接口。显然,贷款是受到严格监管的金融工具。我们不希望有人来添加Loan接口的匿名内部类实现,从而绕过我们辛辛苦苦构建到系统中的验证和保护。

所以,我们将使用sealed类型。密封类型是一种新的访问控制或可见性修饰符。

package com.example.service;

sealed interface Loan permits SecuredLoan, UnsecuredLoan {

}

record UnsecuredLoan(float interest) implements Loan {
}

final class SecuredLoan implements Loan {

}

在这个例子中,我们明确地规定系统中只有两个Loan的实现:SecuredLoanUnsecuredLoan。类默认情况下是开放的,可以进行子类化,这违反了密封层次结构所暗示的保证。因此,我们明确地将SecuredLoan设为finalUnsecuredLoan实现为一个记录,并且隐式为final。

记录是Java对元组的答案。它们是元组。只是Java是一种名义语言:**事物有名称**。这个元组也有一个名称:UnsecuredLoan。如果我们同意它们所暗示的契约,记录将赋予我们强大的能力。记录的核心概念是对象的标识等于字段的标识,在记录中称为“组件”。因此,在这种情况下,记录的标识等于interest变量的标识。如果我们同意这一点,那么编译器可以为我们提供构造函数,它可以为每个组件提供存储空间,它可以为我们提供toString方法、hashCode方法和equals方法。它还将为构造函数中的组件提供访问器。很好!并且,它支持解构!语言知道如何提取记录的状态。

现在,假设我想为每种类型的Loan显示一条消息。我将编写一个方法。这是天真的第一个实现。

 @Deprecated
    String badDisplayMessageFor(Loan loan) {
        var message = "";
        if (loan instanceof SecuredLoan) {
            message = "good job! ";
        }
        if (loan instanceof UnsecuredLoan) {
            var usl = (UnsecuredLoan) loan;
            message = "ouch! that " + usl.interest() + "% interest rate is going to hurt!";
        }
        return message;
    }

这个方法有效,但它没有发挥其作用。

我们可以清理它。让我们利用模式匹配,如下所示

 @Deprecated
    String notGreatDisplayMessageFor(Loan loan) {
        var message = "";
        if (loan instanceof SecuredLoan) {
            message = "good job! ";
        }
        if (loan instanceof UnsecuredLoan usl) {
            message = "ouch! that " + usl.interest() + "% interest rate is going to hurt!";
        }
        return message;
    }

更好。请注意,我们正在使用模式匹配来匹配对象的形状,然后将可明确转换的内容提取到变量usl中。不过,我们实际上并不需要usl变量,对吧?相反,我们想要取消引用interest变量。因此,我们可以更改模式匹配以提取该变量,如下所示。

 @Deprecated
    String notGreatDisplayMessageFor(Loan loan) {
        var message = "";
        if (loan instanceof SecuredLoan) {
            message = "good job! ";
        }
        if (loan instanceof UnsecuredLoan(var interest) ) {
            message = "ouch! that " + interest + "% interest rate is going to hurt!";
        }
        return message;
    }

如果我注释掉其中一个分支会发生什么?什么也没有!编译器不在乎。我们没有处理这段代码可能通过的其中一条关键路径。

同样,我的这个值存储在一个名为message的变量中,我将其赋值为某些条件的副作用。如果我可以省略中间值并只返回某个表达式,岂不是很好?让我们看看使用智能 switch 表达式(Java 中另一个不错的特性)的更简洁的实现。

 String displayMessageFor(Loan loan) {
        return switch (loan) {
            case SecuredLoan sl -> "good job! ";
            case UnsecuredLoan(var interest) -> "ouch! that " + interest + "% interest rate is going to hurt!";
        };
    }

此版本使用智能 switch 表达式返回一个值和模式匹配。如果注释掉其中一个分支,编译器会报错,因为由于密封类型,它知道您还没有穷尽所有可能的选项。很好!编译器为我们做了很多工作!结果更简洁,也更具表现力。大部分情况下是这样的。

好了,让我们回到我们例行的编程。为 Spring Data JDBC 存储库添加一个接口和一个 Spring MVC 控制器类。然后启动应用程序。注意,这需要非常长的时间!这是因为在幕后,它使用 Docker 守护程序启动 PostgreSQL 实例。

但是从现在开始,我们将使用 Spring Boot 的DevTools。您只需要重新编译。如果应用程序正在运行,并且您使用的是 Eclipse 或 Visual Studio Code,则只需要保存文件:在 macOS 上使用 CMD+S。IntelliJ IDEA 没有“保存”选项;在 macOS 上使用 CMD+Shift+F9 强制构建。很好。

好了,我们的 HTTP Web 端点正在监控数据库,但是数据库中没有任何内容,所以这肯定会失败。让我们用一些模式和一些示例数据初始化我们的数据库。

添加schema.sqldata.sql

我们的应用程序的 DDL,schema.sql

create table if not exists customer  (
    id serial primary key ,
    name text not null
) ;

应用程序的一些示例数据,data.sql

delete from customer;
insert into customer(name) values ('Josh') ;
insert into customer(name) values ('Madhura');
insert into customer(name) values ('Jürgen') ;
insert into customer(name) values ('Olga');
insert into customer(name) values ('Stéphane') ;
insert into customer(name) values ('Dr. Syer');
insert into customer(name) values ('Dr. Pollack');
insert into customer(name) values ('Phil');
insert into customer(name) values ('Yuxin');
insert into customer(name) values ('Violetta');

确保通过将以下属性添加到application.properties来指示 Spring Boot 在启动时运行 SQL 文件

spring.sql.init.mode=always

重新加载应用程序:在 macOS 上使用 CMD+Shift+F9。在我的电脑上,重新加载的时间大约是重新启动 JVM 和应用程序本身时间的 1/3,或者减少了 66%。太棒了。

它已经启动并运行。访问https://127.0.0.1:8080/customers查看结果。成功了!当然成功了。这是一个演示。它本来就会成功。

这都是相当标准的东西。十年前你就可以做类似的事情。当然,代码会冗余得多。从那时起,Java 的改进非常巨大。当然,速度也无法相比。当然,现在的抽象也更好。但是你可以做类似的事情——一个监控数据库的 Web 应用程序。

事物在变化。总会有新的领域。现在,新的领域是**人工智能**,或 AI。AI:因为寻找良好的旧**智能**显然还不够难。

AI 是一个巨大的产业,但是大多数人在想到 AI 时指的是利用 AI。您不需要使用 Python 来使用大型语言模型 (LLM),就像大多数人不需要使用 C 来使用 SQL 数据库一样。您只需要与 LLM 集成,而在这里,Java 在选择和功能方面首屈一指。

在 2023 年我们上次的重磅SpringOne 开发者活动中,我们宣布了Spring AI,这是一个旨在使与 AI 集成和使用尽可能容易的新项目。

您可能需要摄取数据,例如来自帐户、文件、服务甚至一组 PDF 的数据。您需要将它们存储到向量数据库中以便于检索,以支持相似性搜索。然后,您需要与 LLM 集成,向其提供来自该向量数据库的数据。

当然,您可以为任何您可能想要的 LLM 提供客户端绑定——Amazon BedrockAzure OpenAIGoogle VertexGoogle GeminiOllamaHuggingFace,当然还有OpenAI 本身,但这仅仅是开始

为 LLM 提供支持的所有知识都烘焙到模型中,然后该模型会告知 LLM 对世界的理解。但是该模型有一定的有效期,在此之后,其知识就会过时。如果模型是两周前构建的,它将不知道昨天发生的事情。因此,如果您想构建一个自动助手来处理用户对其银行帐户的请求,那么 LLM 在这样做时需要了解世界的最新状态。

您可以在您发出的请求中添加信息并将其用作上下文来告知响应。如果只有这么简单,那也不会太糟糕。还有一个问题。不同的 LLM 支持不同的令牌窗口大小。令牌窗口决定您可以为给定请求发送和接收多少数据。窗口越小,您可以发送的信息越少,LLM 在其响应中的信息就越少。

您可以在这里做的一件事是将数据放入向量存储中,例如pgvectorNeo4jWeaviate或其他,然后将您的 LLM 连接到该向量数据库。向量存储使您可以根据单词或词组找到与其相似的其他内容。它将数据存储为数学表示,并允许您查询相似的内容。

整个摄取、丰富、分析和消化数据以告知 LLM 响应的过程称为**检索增强生成**(RAG),Spring AI 支持所有这些。更多信息,请参见我关于 Spring AI 的Spring Tips 视频。但是,我们不会在此处利用所有这些功能。只用一个。

我们在 Spring Initializr 上添加了OpenAI支持,因此 Spring AI 已经在类路径上了。添加一个新的控制器,如下所示

一个 AI 驱动的 Spring MVC 控制器

package com.example.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;

@Controller
@ResponseBody
class StoryController {

    private final ChatClient singularity;

    StoryController(ChatClient singularity) {
        this.singularity = singularity;
    }

    @GetMapping("/story")
    Map<String, String> story() {
        var prompt = """
                Dear Singularity,

                Please write a story about the good folks of San Francisco, capital of all things Artificial Intelligence,
                and please do so in the style of famed children's author Dr. Seuss.

                Cordially,
                Josh Long
                """;
        var reply = this.singularity.call(prompt);
        return Map.of("message", reply);
    }

}

非常简单!注入 Spring AI 的ChatClient,使用它向 LLM 发送请求,获取响应,并将其作为 JSON 返回到 HTTP 客户端。

您需要使用属性spring.ai.openai.api-key=配置与 OpenAI API 的连接。在运行程序之前,我将其导出为环境变量SPRING_AI_OPENAI_API_KEY。我不会在此处复制我的密钥。请原谅我没有泄露我的 API 凭据。

使用 CMD+Shift+F9 重新加载应用程序,然后访问端点:https://127.0.0.1:8080/story。LLM 可能需要几秒钟才能生成响应,所以准备好一杯咖啡、一杯水或其他任何东西,以便快速但令人满意地喝一口。

story time ai response
在我的浏览器中使用启用了 JSON 格式化程序插件的 JSON 响应

就是这样!我们生活在一个奇迹的时代!奇点时代!你现在可以做任何事情。

但是它确实花了几秒钟,不是吗?我不责怪电脑花了这么长时间!它做得很好!我做不到这么快。看看它生成的故事情节!这是一件艺术品。

但是它确实花了一段时间。这对我们的应用程序的扩展性有影响。在我们向 LLM 发出调用时,在幕后我们正在进行网络调用。在代码深处,有一个java.net.Socket,我们从中获得了表示来自服务的byte数组数据的java.io.InputStream。我不知道你还记不记得直接使用InputStream。这是一个例子

    try (var is = new FileInputStream("afile.txt")) {
        var next = -1;
        while ((next = is.read()) != -1) {
            next = is.read();
            // do something with read
        }
    }

看到我们通过调用InputStream.readInputStream中读取字节的那部分吗?我们称之为**阻塞操作**。如果我们在第四行调用InputStream.read,那么我们必须等到调用返回才能到达第五行。

如果我们连接的服务只是返回了太多数据怎么办?如果服务宕机了怎么办?如果它永远不返回怎么办?如果我们被卡住,永远等待怎么办?如果呢?

如果只发生一次,这很乏味。但是,如果它可能发生在用于处理 HTTP 请求的系统中的每个线程上,那么这对我们的服务来说就是一个生存威胁。这种情况经常发生。这就是为什么可以登录到一个无响应的 HTTP 服务并发现 CPU 基本处于休眠状态——空闲!——完全没有做任何事情或几乎没有做任何事情的原因。线程池中的所有线程都卡在等待状态,等待一些没有来的东西。

这是对我们已付费的宝贵 CPU 容量的巨大浪费。即使最佳情况也不好。即使该方法最终会返回,它仍然意味着正在处理该请求的线程对系统中的任何其他内容都不可用。该方法正在独占该线程,因此系统中的任何其他人无法使用它。如果线程便宜且充足,这不会成为问题。但它们不是。在 Java 的大部分生命周期中,每个新线程都与一个操作系统线程一对一配对。它并不便宜。每个线程都有一些簿记工作。一到两兆字节。因此,您将无法创建许多线程,并且您正在浪费您拥有的少量线程。太可怕了!反正谁需要睡觉呢?

一定有更好的方法。

您可以使用非阻塞 IO。例如令人头疼且复杂的 Java NIO 库。这是一个选项,就像与一群臭鼬一起生活是一个选项一样:它很臭!我们大多数人无论如何都不会考虑非阻塞 IO 或常规 IO。我们生活在抽象阶梯的更高层次。我们可以使用反应式编程。我喜欢反应式编程。我甚至写了一本书——Reactive Spring。但是如果您不习惯像函数式程序员那样思考,那么让它工作并不容易。这是一个不同的范例,意味着要重写您的代码。

如果我们既能拥有非阻塞的优势,又能享用其成果呢?借助Java 21,现在我们做到了!一个名为虚拟线程的新特性让这一切变得容易得多!如果你在一个新的虚拟线程上执行阻塞操作——例如java.io.InputStream.readjava.io.OutputStream.writejava.lang.Thread.sleep——运行时会检测到你的阻塞行为,并将这个阻塞的空闲活动从线程转移到RAM中。然后,它会为休眠设置一个计时器,或监控文件描述符的IO操作,同时让运行时将线程重新用于其他任务。当阻塞操作完成后,运行时会将其移回线程,并让它从中断的地方继续执行,而你的代码几乎无需任何改动。这很难理解,所以让我们通过一个例子来看一下。我厚颜无耻地从Oracle开发者布道者José Paumard那里“借用”了这个例子。

此示例演示了创建1000个线程并在每个线程上休眠400毫秒,同时记录这1000个线程中第一个线程的名称。

package com.example.service;

import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;

@Configuration
class Threads {

    private static void run(boolean first, Set<String> names) {
        if (first)
            names.add(Thread.currentThread().toString());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Bean
    ApplicationRunner demo() {
        return args -> {

            // store all 1,000 threads
            var threads = new ArrayList<Thread>();

            // dedupe elements with Set<T>
            var names = new ConcurrentSkipListSet<String>();

            // merci José Paumard d'Oracle
            for (var i = 0; i < 1000; i++) {
                var first = 0 == i;
                threads.add(Thread.ofPlatform().unstarted(() -> run(first, names)));
            }

            for (var t : threads)
                t.start();

            for (var t : threads)
                t.join();

            System.out.println(names);
        };
    }

}

我们使用Thread.ofPlatform工厂方法来创建普通的平台线程,其本质与自Java诞生于20世纪90年代以来我们创建的线程相同。程序创建1000个线程。在每个线程中,我们休眠100毫秒,四次。期间,我们测试是否在1000个线程中的第一个线程上,如果在,我们将当前线程的名称添加到一个集合中。集合会去除重复元素;如果同一个名称出现多次,集合中仍然只有一个元素。

运行程序(CMD+Shift+F9!),你会发现程序的物理特性没有改变。Set<String>中只有一个名称。为什么不会有多个呢?我们只是反复测试同一个线程。

现在,将构造函数更改为使用虚拟线程Thread.ofVirtual。非常简单的更改。现在运行程序。CMD+Shift+F9。

你会看到集合中有多个元素。你根本没有改变代码的核心逻辑。实际上,你只需要更改一个地方,但是现在,在幕后,编译器和运行时无缝地重写了你的代码,这样当虚拟线程发生阻塞时,运行时会无缝地将你移出并移回到线程中,直到阻塞操作完成。这意味着你之前所在的线程现在可用于系统的其他部分。你的可扩展性将大幅提升!

你可能会抗议,我不希望不得不更改所有代码。首先,这是一个荒谬的论点,更改微不足道。你可能会抗议:我也不想负责创建线程。说得对。Tony Hoare在20世纪70年代写道,NULL是价值十亿美元的错误。他错了。实际上,是PHP。但是,他还详细阐述了使用线程构建系统的不可行性。你会希望使用更高阶的抽象,例如saga、actor,或者至少是ExecutorService

还有一个新的虚拟线程执行器:Executors.newVirtualThreadPerTaskExecutor。不错!如果你使用Spring Boot,则可以轻松覆盖该类型的默认bean,以便在系统的部分中使用。Spring Boot将引入它并转而使用它。足够简单。但是,如果你使用的是Spring Boot 3.2,你肯定在使用Spring Boot 3.2吧?你知道每个版本只支持大约一年吗?请务必查看给定的Spring项目的支持策略。如果你使用的是3.2,那么你只需要在application.properties中添加一个属性,我们就会为你插入虚拟线程支持。

spring.threads.virtual.enabled=true

不错!无需更改代码。现在你应该会看到可扩展性的大幅提升,如果你的服务是IO绑定的,你也许可以减少负载均衡器中的一些实例。我的建议?告诉你的老板你将为公司节省大量资金,但坚持要将这笔钱计入你的工资。然后部署此更改。瞧!

好了,我们进展很快。我们有git clone和运行能力。我们有Docker compose支持。我们有DevTools。我们拥有非常优秀的语言和语法。我们拥有奇点。我们进展很快。我们拥有可扩展性。将Spring Boot Actuator添加到构建中,现在你有了可观察性。我认为是时候转向生产环境了。

我想打包这个应用程序并使其尽可能高效。我的朋友们,这里我们需要考虑几件事。首先,我们如何容器化应用程序?很简单。使用Buildpacks。容易。记住,朋友不会让朋友编写Dockerfiles。使用Buildpacks。它们也与Spring Boot开箱即用:./gradlew bootBuildImage./mvnw spring-boot:build-image。但这并不是新的,所以下一个问题。

我们如何使这个东西尽可能高效和优化?在我们深入探讨之前,我的朋友们,重要的是要记住Java本身已经非常非常高效了。我喜欢这篇2018年的博客文章,在COVID大流行之前,也就是公元前

它研究了哪些语言最节能,或者最节能。C是最节能的。它使用的电力最少。1.0。它是基准。它很有效……对于机器。不是人!绝对不是人。

然后是Rust及其零成本抽象。做得很好。

然后是C++……gross

C++真恶心!继续……

然后是Ada语言,但是……谁在乎呢?

然后是Java,它接近2.0。让我们四舍五入,说2.0。Java的效率是C的两倍——两倍!或者说是C效率的一半。

到目前为止还好吗?很好。它位列前五位效率最高的语言!

如果你滚动列表,你会看到一些惊人的数字。Go和C#的数值在3.0+左右。向下滚动到这里,我们有JavaScript和TypeScript,其中一种——令我百思不得其解的是——效率是另一种的四倍

然后是PHP和Hack,少说为妙。继续!

然后是JRuby和Ruby。朋友们,记住JRuby是用Java编写的Ruby。而Ruby是用C编写的Ruby。然而,JRuby的效率几乎比Ruby高三分之一!仅仅因为是用Java编写的,并在JVM上运行。JVM是一个了不起的工具包。绝对非凡。

然后……是Python。这,这确实让我很难过!我喜欢Python!从20世纪90年代我就开始使用Python了!我第一次学习Python的时候,比尔·克林顿还是总统!但这些数字并不理想。想想看。75.88。让我们四舍五入到76。我不擅长数学。但你知道什么擅长吗?该死的Python!让我们问问它。

python doing math

38!这意味着,如果你用Java运行一个程序,而运行该程序所需的能量的产生会产生一些最终滞留在大气中的碳,从而导致温度升高,而这种升高的温度反过来又会杀死一棵树,那么等效的Python程序将杀死38棵树!那是一片森林!那比比特币还糟糕!我们需要尽快解决这个问题。我不知道是什么,但必须有所作为。

无论如何,我想说的是Java已经很棒了。我认为这是因为人们习以为常的两件事:垃圾回收和即时(JIT)编译器。

垃圾回收,我们都知道它是什么。哎呀,就连白宫在其关于确保软件以确保网络空间构建块安全的最新报告中也赞赏像Java这样的垃圾回收、内存安全的语言。

Java编程语言的垃圾收集器让我们可以编写平庸的软件,并侥幸逃脱。它很棒!也就是说,我确实对它是否是原始的Java垃圾收集器的说法持异议!这一荣誉属于其他地方,例如Jesslyn

JIT也是另一个了不起的工具包。它分析应用程序中经常访问的代码路径,并将它们转换为特定于操作系统和体系结构的本机代码。但是它只能对你的部分代码执行此操作。它需要知道编译代码时使用的类型是在运行代码时将使用的唯一类型。Java中的一些内容——一种具有更类似于JavaScript、Ruby和Python的运行时的非常动态的语言——允许Java程序执行违反此约束的操作。例如序列化、JNI、反射、资源加载和JDK代理。记住,使用Java,可以拥有一个内容为Java源代码文件的String,将该字符串编译成文件系统上的.class文件,将.class文件加载到ClassLoader中,反射地创建该类的实例,然后——如果该类是一个接口——创建该类的JDK代理。如果该类实现了java.io.Serializable,则可以将该类实例写入网络套接字并在另一个JVM上加载它。你可以在没有任何超越java.lang.Object的显式类型引用的情况下完成所有这些操作!它是一种了不起的语言,这种动态特性使其成为一种非常高效的语言。它还会挫败JIT的优化尝试。

尽管如此,JIT在可以的情况下仍然做得非常出色。结果不言而喻。因此,人们不禁要问:为什么我们不能主动地提前JIT整个程序呢?我们可以。有一个名为GraalVM的OpenJDK发行版,它具有一些扩展OpenJDK发行版的功能,例如native-image编译器之类的额外工具。本机映像编译器很棒。但是这个本机映像编译器有相同的限制。它无法对非常动态的事物发挥其魔力。这是一个问题。因为大多数代码——你的单元测试库、你的Web框架、你的ORM、你的日志记录库……所有东西!——都使用了一种或所有这些动态行为。

有一个应急方案。你可以以.json文件的形式向GraalVM编译器提供配置,该编译器位于众所周知的目录中:src/main/resources/META-INF/native-image/$groupId/$artifactId/*.json。这些.json文件有两个问题。

首先,“JSON”这个词听起来很蠢。我不喜欢说“JAY-SAWN”这个词。作为一个成年人,我不敢相信我们会互相这样说。我会说法语,说法语,你会把它念成 *jeeesã*。所以,.gison。好多了。闽南语有一个词——*gingsong*(幸福),也可以用。所以你可以用 .gingsong。选你的队伍!无论哪种方式,.json都不应该存在。我是.gison战队,但这并不重要。

第二个问题是,好吧,它需要的东西太多了!再说一次,想想你程序中所有进行这些有趣动态操作的地方,例如反射、序列化、JDK 代理、资源加载和 JNI!没完没了。你的 Web 框架。你的测试库。你的数据访问技术。我没有时间为每个程序编写手工制作的配置文件。我甚至没有足够的时间完成这篇博客!

因此,我们将使用在 3.0 中引入的 Spring Boot 提前 (AOT) 引擎。AOT 引擎会分析 Spring 应用程序中的 Bean,并为你发出必要的配置文件。不错!你甚至可以使用一个扩展 Spring 到编译时间的完整组件模型。我在这里不会详细介绍所有这些,但你可以阅读我的免费电子书或观看我的免费 YouTube 视频,介绍所有关于Spring 和 AOT的内容。基本上内容相同,只是消费方式不同。

所以让我们用 Gradle 启动构建,./gradlew nativeCompile,或者如果你使用的是 Apache Maven,则使用./mvnw -Pnative native:compile。你可能想跳过这个测试……这个构建需要一些时间。记住,它正在分析你代码库中的所有内容——无论是类路径上的库、JRE 还是你代码本身中的类——以确定它应该保留哪些类型,以及应该丢弃哪些类型。结果是一个精简、高效、闪电般快速的运行时机器,但代价是编译时间非常、非常慢。

事实上,它花费的时间太长了,以至于它阻塞了我的流程。它让我停滞不前,等待。就像这篇博客前面提到的平台线程一样:*阻塞*!我厌倦了。等待。等待。我现在终于理解了这幅著名的 XKCD 漫画

有时我会开始哼歌。或者主题曲。或者电梯音乐。你知道电梯音乐是什么样的,对吧?永无止境。所以,我想,如果每个人都能听到电梯音乐,岂不是很好?所以我问了。我得到了一些很棒的回应。

有人建议,我们应该播放来自任天堂 64 游戏《黄金眼》配乐中的电梯音乐,这是皮尔斯·布鲁斯南首次饰演詹姆斯·邦德的电影。我喜欢它。

adinn elevator music
《黄金眼》有一些很棒的电梯音乐!

一个回复建议发出蜂鸣声会很有用。我完全同意。我那愚蠢的微波炉会在完成时发出 *叮!* 的声音。为什么我那数百万行的编译器不行呢?

ivan beeps
叮!

然后我们收到了来自我最喜欢的医生之一的回复,Niephaus 博士,他在 GraalVM 团队工作。他说添加电梯音乐只会解决症状,而不会解决问题的原因,即提高 GraalVM 在时间和内存方面的效率。

doctor
niephaus

好吧。但他确实分享了这个有前景的原型!

graalvm
prototype

我相信它很快就会合并……

无论如何!如果你检查编译,它现在应该完成了。它位于./build/native/nativeCompile/文件夹中,名为service。在我的机器上,编译花了 52 秒。哎呀!

运行它。它会失败,因为,再说一次,我们正在过着git clone & 运行的生活方式!我们没有指定任何连接凭据!因此,请使用指定 SQL 数据库连接详细信息的环境变量运行它。这是我在我的机器上使用的脚本。这仅适用于类 Unix 操作系统,并且适用于 Maven 或 Gradle。

#!/usr/bin/env bash

export SPRING_DATASOURCE_URL=jdbc:postgresql://127.0.0.1/mydatabase
export SPRING_DATASOURCE_PASSWORD=secret
export SPRING_DATASOURCE_USERNAME=myuser

SERVICE=.
MVN=$SERVICE/target/service
GRADLE=$SERVICE/build/native/nativeCompile/service
ls -la $GRADLE && $GRADLE || $MVN

在我的机器上,它在大约 100 毫秒内启动!像火箭一样!而且,显然,如果我使用的是 Spring Cloud Function 来构建 AWS Lambda 风格的函数即服务,它会更快,因为我不需要例如打包 HTTP 服务器。事实上,如果我真正想要的只是纯粹的启动速度,那么我甚至可以使用 Spring 对Project CRaC的惊人支持。这与本文无关。我不太关心这一点,因为这是一个独立的、长期存在的服务。我关心的是资源使用情况,由常驻集大小 (RSS)表示。注意进程标识符 (PID)——它会在日志中。如果 PID 为,比如说,55,则使用几乎所有 Unix 系统上都可用的ps实用程序,像这样获取 RSS

ps -o rss 55

它会输出一个以千字节为单位的数字;除以一千,你就会得到以兆字节为单位的数字。在我的机器上,它只需要 100MB 以上就能运行。你无法用这么少的内存运行 Slack!我敢打赌,你 Chrome 中的单个浏览器标签页占用了这么多或更多内存!

因此,我们拥有一个尽可能简洁的程序,同时易于开发和迭代。它使用虚拟线程为我们提供无与伦比的可扩展性。它作为一个独立的、自包含的、特定于操作系统和体系结构的原生镜像运行。哦!而且,它支持奇点!

我们生活在一个令人惊叹的时代。对于 Java 和 Spring 开发人员来说,现在是最好的时代。我希望我也能说服你。

资源

我和 Spring 团队的其他成员将在微软的 JDConf 2024 上发言!

注册并参加我在JDConf:Bootiful Spring Boot 3的会议

其他在 JDConf 上参加的 Spring 会议:

如果你喜欢这篇博客,我希望你会订阅我们的YouTube 频道,我在Spring Tips播放列表中每周都有新视频。当然,你也可以在我的Twitter/X、网站、YouTube 频道、书籍、播客和 Biodrop 上的更多内容找到我。谢谢!

获取 Spring 新闻

与 Spring 新闻保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获得支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部