JDK9~17新特性
JDK 9新特性: jshell交互式工具
jshell是JDK 9引入的一个交互式命令行工具, 主要用于快速测试和学习Java代码
主要特点
- 即时执行:无需编写完整的Java程序,可以直接输入和执行Java代码片段。
- 交互式环境:提供类似REPL(Read-Eval-Print Loop)的交互式环境。
- 自动补全:支持Tab键自动补全,提高编码效率。
- 命令历史:可以使用上下箭头键查看和重用之前输入的命令
- 变量共享:在一个会话中定义的变量可以在后续命令中使用。
- 方法定义:支持直接定义和调用方法。
- 无需分号:每行代码结束可以省略分号。
使用示例
- 启动jshell
jshell- 打印语句
System.out.println("test")- 定义变量
String hello = "hello world"- 使用变量
System.out.println(hello)主要用途
- 教学工具:适合大学教师或培训机构用于Java教学。
- 快速测试:开发人员可以快速测试小段代码或API。
- 学习和探索:方便学习新的Java特性或库
JDK 9模块化开发特性
JDK 9 引入的模块化开发时Java平台的一次重要升级, 旨在提高代码的封装性和可维护性
模块化概念
模块化在包(package)的基础上增加了一个新的抽象层次-模块(module). 这种结构允许开发者更精确地控制代码的可见性和依赖关系
graph TD
A[模块 Module] --> B[包 Package]
B --> C[类 Class]
B --> D[接口 Interface]
核心组件
- module-info.java: 模块的描述文件,位于模块根目录。
- exports: 声明可以被外部访问的包。
- equires: 声明模块依赖。
实现步骤
- 创建模块并添加 module-info.java 文件。
- 在被调用模块中使用 exports 声明开放的包。
- 在调用模块中使用 requires 声明所需的外部模块。
// 模块A的module-info.java
module test.a {
exports com.test.a.package1;
// exports com.test.a.package2; // 可选:开放多个包
}
// 模块B的module-info.java
module test.b {
requires test.a;
}优势
- 提高代码封装性
- 明确模块间依赖关系
- 增强应用安全性
- 改善性能(通过更有效的类加载)
实际应用
虽然模块化提供了诸多优势, 但在实际项目中的应用还不普遍, 主要原因包括:
- 增加了项目复杂度
- 对现有代码库的改造成本高
- 部分第三方库尚未完全支持
我的理解
我自己也整理了一些JDK的新特性, 我试着理解一下模块化开发的内容, 首先JDK 9模块化开发简介, 想象现在我们正在建造一座大楼(Java程序), 在JDK 8及以前呢, 我们可以理解为, 我们有很多房间(就是包/packages), 每个房间里有家具(类/classes), 这种方式, 会出现一个问题: 任何人都可以进入任何房间, 使用任何家具. 而在JDK 9模块化开发之后, 我们就可以把房间分组成套房(模块/modules), 也可以指定哪些套房需要相互连接(requires), 也可以指定哪些套房的门是开着的(exports). 那么我们应该如何用呢, 那首先就是需要我们创建一个特殊的文件(module-info.java)放在你的”套房”的入口, 这个文件里你写到”这个套房叫什么名字(module 名称),哪些房间可以让外人进入(exports),这个套房需要连接到哪些套房(requires)” 这样带给我们很多好处, 提供了更好的安全性—可以隐藏不想让别人看到的”房间”,更清晰的结构—很容易看出哪些”套房”依赖于其他”房间”,可能运行的更快—系统知道只需要加载必要的”套房”
举个🌰 : 图书管理系统
核心模块(library-core)
这个模块包含基本的图书和用户管理功能
目录结构:
library-core/
├── src/
│ ├── com.library.core/
│ │ ├── Book.java
│ │ ├── User.java
│ │ └── internal/
│ │ └── DatabaseConnection.java
│ └── module-info.java
用tree命令可以获取到项目结构
module-info.java 内容
module com.library.core {
exports com.library.core;
// 注意我们没有导出 internal 包
}Book.java
package com.library.core;
public class Book {
private String title;
private String author;
// 构造函数、getter和setter
}用户界面模块 (library-ui)
这个模块提供图形界面,依赖于核心模块。
library-ui/
├── src/
│ ├── com.library.ui/
│ │ └── LibraryApp.java
│ └── module-info.java
module-info.java 内容
module com.library.ui {
requires com.library.core;
// 如果使用JavaFX,还需要添加:
// requires javafx.controls;
}LibraryApp.java
package com.library.ui;
import com.library.core.Book;
// import com.library.core.internal.DatabaseConnection; // 这行会导致编译错误,因为internal包没有被导出
public class LibraryApp {
public static void main(String[] args) {
Book book = new Book();
// 使用Book类的代码
// DatabaseConnection db = new DatabaseConnection(); // 这行会导致编译错误
}
}- 在library-core模块中, 只导出了com.library.core包, 而没有导出internal包. 这一位着其他模块可以使用Book和User类, 但不能直接访问DatabaseConnection类
- 在library-ui模块中, 声明了对com.library.core模块的依赖. 这允许我们使用核心模块中导出的类
- 如果尝试在LibraryApp中使用DatabaseConnection类, 编译器会报错, 因为这个类在一个未导出的包中
通过模块化我们可以精确地控制哪些部分对外可见, 哪些部分保持内部使用. 这提高了代码的封装性和安全性, 同时也使得系统的结构更加清晰
JDK 10新特性: var局部变量推导
基本概念
var 关键字允许在局部变量声明时进行类型推导, 简化代码编写
使用要求
- 必须要能推到出实际类型
- 只能用于声明局部变量
使用示例
// 正确用法
var test1 = new Test1();
var number = 1;
// 错误用法
var test11; // 编译错误:必须初始化
class SomeClass {
var classField = 10; // 编译错误:不能用于类字段
}优点
- 简化代码, 减少冗长的类型声明
- 特别适用于复杂类型的声明
// 旧方式
ArrayList<String> list = new ArrayList<>();
// 使用var
var list = new ArrayList<String>();注意事项
- 过度使用可能减低代码可读性
- 不适用于没有初始化的变量声明
个人观点
虽然var可以简化代码, 但是显式声明类型可能在某些情况下更清晰. 使用与否主要取决于个人或团队的编码风格和偏好. 总的来说, var提供了一种更简洁的局部变量声明方式, 但应谨慎使用, 确保代码的清晰度和可读性不受影响
举个🌰
我试了试, 还挺好用的, 一些基础用法都适用, 可以看一下
基本数据类型
var i = 10; // 推导为int
var d = 3.14; // 推导为double
var b = true; // 推导为boolean
var c = 'A'; // 推导为char
var s = "Hello"; // 推导为String复杂数据类型
var list = new ArrayList<String>(); // 推导为ArrayList<String>
var map = new HashMap<String, Integer>(); // 推导为HashMap<String, Integer>
var entry = map.entrySet().iterator().next(); // 推导为Map.Entry<String, Integer>匿名内部类
var runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello, var!");
}
}; // 推导为匿名Runnable实现类循环中的使用
for (var i = 0; i < 10; i++) {
System.out.println(i);
}
var numbers = Arrays.asList(1, 2, 3, 4, 5);
for (var num : numbers) {
System.out.println(num);
}Lambda表达式 (不能直接使用var)
// 错误用法
var lambda = (x, y) -> x + y; // 编译错误
// 正确用法
BiFunction<Integer, Integer, Integer> lambda = (x, y) -> x + y;方法返回值
public class Example {
public static void main(String[] args) {
var result = getComplexObject();
// 使用result,无需知道确切类型
}
private static SomeComplexType getComplexObject() {
return new SomeComplexType();
}
}不推荐的用法
var obj = null; // 编译错误,无法推导类型
var x = 1, y = 2; // 编译错误,不支持多变量声明
// 可读性降低的例子
var x = someMethodWithUnclearReturnType();与泛型结合
var list = new ArrayList<Map<String, List<Integer>>>();
// 等同于 ArrayList<Map<String, List<Integer>>> list = new ArrayList<>();JDK 11新特性: 单文件程序
概述
JDK 11引入了单文件程序特性, 允许直接运行单个Java源文件, 无需先编译成class文件
主要特点
- 直接执行: 可以直接用
java命令运行.java文件 - 无需编译: 跳过了传统的javac编译步骤
- 限于单文件: 只适用于单个Java源文件
- 包限制: 不支持包声明和导入其他自定义类
使用方法
java FileName.java示例
假设有一个名为TestB.java的文件, 内容如下:
public class TestB {
public static void main(String[] args) {
System.out.println("Hello World");
}
}可以直接运行
java TestB.java输出
Hello World
注意事项
- 不支持复杂的项目结构
- 不能使用外部依赖
- 主要用于简单, 独立的程序
JDK 11新特性: Shebang脚本
什么是SHebang?
Shebang(也写作She-bang)是一个由 #! 开头的字符序列,通常出现在Unix系统的脚本文件第一行。它指定了执行这个脚本文件的解释器。
Java中的Shebang支持
JDK 11允许Java文件使用Shebang, 使得Java代码可以像脚本一样直接执行
基本格式
#!/path/to/java --source 11使用步骤
- 创建一个不带
.java后缀的文件(例如test) - 文件首行添加Shebang
#!/path/to/your/jdk/bin/java --source 11- 编写Java代码(无需public class声明)
- 给文件添加执行权限(Unix系统)
chmod +x test- 执行脚本
./test注意事项
- 需要使用Unix-like环境(Linux, macOS, 或Windows的Git Bash
- 文件无需
.java后缀 - 使用—source 11参数指定Java版本
- 脚本中的Java代码不需要声明public class
局限性
- 主要用于简单脚本, 不适合复杂应用
- 执行环境需要支持Shebang(Windows CMD不支持)
这个特性使得Java可以更方便地用于编写简单的脚本和工具, 特别是在Unix-like系统中, 它为Java带来了类似脚本语言的便利性, 同时保留了Java的强大功能
JDK 14新特性: 文本块
文本块是JDK 14引入的一个新特性
主要优点
- 提高多行字符串的可读性
- 减少字符串拼接和转义的需求
- 特别适合编写JSON, HTML, SQL等多行文本
使用方法
传统方法(JDK 8及以前)
String json1 = "{\n" +
" \"name\": \"test\"\n" +
"}";新方法(JDK 14及以后)
String json2 = """
{
"name": "test"
}
""";特点
- 使用三个双引号
(""")来开始和结束文本块 - 可以直接包含换行符,无需显式添加 \n
- 保留文本的格式,包括缩进
- 结果字符串与传统方式相同
使用场景
- JSON字符串
- HTML模版
- SQL查询
- 任何需要保留格式的多行文本
注意事项
- 开始的三个双引号后必须紧跟换行
- 结束的三个双引号可以单独占一行, 用于控制最后的换行
结论
文本块大大简化了多行字符串的编写, 提高了代码的可读性和维护性. 对于需要处理大量格式化文本的开发者来说, 这是一个非常有用的特性
JDK 14新特性: instanceof的增强
背景
- instanceof关键字在Java 14之前就已存在
- 用于判断对象类型
传统用法
Object a = "hello";
if (a instanceof String) {
String b = (String) a;
System.out.println(b);
}特点:
- 需要单独进行类型转换
- 代码较为冗长
Java 14增强用法
Object a = "hello";
if (a instanceof String b) {
System.out.println(b);
}特点:
- 类型检查和转换合并为一步
- 代码更简洁, 易读
优势
- 减少代码量
- 提高代码可读性
- 避免重复的类型转换代码
注意事项
- 两种写法的输出结果相同
- 新语法仅在if语句中有效
总结
Java 14增强是一个小而有用的改进, 使得类型检查和转换的代码更加简洁和优雅
我的理解
这一部分不是太重要, 因为肯定都用过, 用来测试拿到的类型到底是否正确, 我之前学c++的时候有typeof可以判断出当前数据是什么类型, js当中也有, 而java当中的instanceof操作符用于检查对象是否为特定类型的实例, 这是Java 14引入的匹配模式(Pattern Matching) for instanceof 它的工作原理结合了类型检查和类型转换, 引入了模式变量(Pattern Variable)的概念, 让我们来做个对比 传统语法
if (obj instanceof String) {
String s = (String) obj;
// 使用 s
}新语法
if (obj instanceof String s) {
// 直接使用 s
}上述的工作流程如下:
- 类型检查: 验证obj是否为String类型
- 类型转换: 如果是, 自动将obj转换为String类型
- 变量绑定: 将转换后的值绑定到新声明的变量s
- 作用域: s只在if语句块内有效
新的语法帮助我们合并了检查和转换步骤, 同时避免了显式类型转换可能引发的ClassCastException, 当然模式变量在条件为false时不会被初始化, 且不能再instanceof表达式中使用已声明的变量, 编译器负责生成必要的类型检查和转换代码, 在运行时, JVM确保类型安全和正确的变量绑定
Java 14新特性: 空指针异常提示增强
背景
- 在java 14之前, 空指针异常(NullPointerException)的提示信息不够详细
- 对于复杂表达式,难以确定具体哪个变量导致了空指针异常
Java 8的空指针异常提示
- 只能定位到发生异常的行号
- 不能之处具体哪个变量是null
示例:
List<String> list = null;
System.out.println(list.size());输出:
Exception in thread "main" java.lang.NullPointerException
at com.example.Main.main(Main.java:6)
Java 14的空指针异常提示增强
- 能够精确定位到导致空指针异常的变量
- 对于复杂表达式,可以指出具体哪部分是null
(Java 14+)输出
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.size()" because "list" is null
at com.example.Main.main(Main.java:6)
配置方法
- 打开项目结构(Project Structure)
- 选择模块(Modules)
- 在每个模块的Dependencies标签下, 将Module SDK设置为Java 14或更高版本
- 在项目设置中, 确保Language Level也设置为相应的Java版本
Java 16新特性: Record类
背景
- Record类是Java 16引入的一种特殊类型
- 主要用于存储和传输数据, 类似于DTO,VO,PO等
传统数据类
public class TestDTO {
private String name;
private String password;
// 构造函数、getter、setter、toString、equals、hashCode方法
}特点:
- 需要手动编写或使用IDE生成getter,setter等方法
- 可以使用Lombok等插件简化代码
Record类
public record TestRecord(String name, String password) {}特点:
- 使用record关键字声明
- 在括号内直接声明字段
- 自动生成构造函数、getter、toString、equals、hashCode方法
- 字段默认为final,不可变
使用对比
传统数据类使用
TestDTO dto = new TestDTO();
dto.setName("test1");
dto.setName("test2"); // 可以多次修改
dto.setPassword("password");Record类的使用
TestRecord record = new TestRecord("test", "password");
String name = record.name(); // 使用方法而非字段访问
// record.name = "newName"; // 编译错误,不能修改Record类的特点
- 不可变性:一旦创建,字段值不能更改
- 简洁性:大大减少了样板代码
- 自动生成方法:无需手动编写或生成常用方法
- 适用场景:适合用于只需要初始化一次的数据存储
注意事项
- Record类是final的,不能被继承
- 不能声明实例字段(除了在参数列表中声明的)
- 可以声明静态字段和方法
- 可以实现接口
总结
Record类提供了一种简洁、不可变的数据存储方式,特别适合用于数据传输对象(DTO)和值对象(VO)。它通过减少样板代码提高了开发效率,同时保证了数据的不可变性。
我的理解
Record类的本质
Record类本质上是一种特殊的不可变数据类, 它的主要目的是用来存储和传输数据. 可以将其视为一种简化的, 不可变的POJO(Plain Old Java Object)
Record类的工作原理
- 自动生成的内容:
当声明一个 Record 类时,Java 编译器会自动为您生成以下内容:- 私有的、final 的字段(对应于您在 Record 声明中定义的组件)- 一个包含所有组件的公共构造函数 - 对应每个组件的公共访问方法(getter,但命名与字段相同)- equals() 和 hashCode() 方法 - toString() 方法
- 不可变性
Record 类的所有字段都是 final 的,这意味着一旦对象被创建,其状态就不能被改变。
为什么使用 Record 类?
- 代码简洁:减少了大量样板代码(boilerplate code)。
- 不可变性:保证了数据的一致性和线程安全。
- 语义清晰:明确表示这个类仅用于存储数据。
举个🌰
// 传统的 POJO
public class PersonPOJO {
private final String name;
private final int age;
public PersonPOJO(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonPOJO that = (PersonPOJO) o;
return age == that.age && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "PersonPOJO{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// 等效的 Record 类
public record PersonRecord(String name, int age) {}使用这两个类
PersonPOJO pojo = new PersonPOJO("Alice", 30);
System.out.println(pojo.getName()); // 输出: Alice
PersonRecord record = new PersonRecord("Bob", 25);
System.out.println(record.name()); // 输出: Bob
// 尝试修改 Record(会导致编译错误)
// record.name = "Charlie"; // 错误:Record 的字段是 final 的
// 使用 toString()
System.out.println(record); // 输出: PersonRecord[name=Bob, age=25]
// 使用 equals()
PersonRecord record2 = new PersonRecord("Bob", 25);
System.out.println(record.equals(record2)); // 输出: trueRecord类的局限性
- 不能添加额外的实例字段。
- 不能继承其他类(但可以实现接口)
- 总是 final 的,不能被继承。
何时使用Record类?
Record 类特别适合用于:
- 数据传输对象(DTO)
- 值对象(Value Objects)
- 不可变数据的封装
- 简单的数据结构,如点、坐标等
Java 17 新特性:Sealed 类详解
什么是 Sealed 类?
Sealed 类(密封类)是 Java 17 引入的一个新特性,它允许类的作者精确控制哪些类可以继承自该类。这个特性的主要目的是提供对继承的更细粒度的控制。
为什么需要 Sealed 类?
在传统的 Java 类继承中,我们面临以下问题:
- 类可以被任意继承,难以管理。
- 子类可能会不恰当地重写父类方法,导致行为不一致。
- 难以一目了然地知道一个类被哪些类继承。
Sealed 类就是为了解决这些问题而设计的。
Sealed 类的工作原理
基本语法
public sealed class Animal permits Cat, Dog, Bird {
// 类的内容
}在这个例子中:
sealed 关键字表明 Animal 是一个密封类。
permits 关键字后面列出了允许继承 Animal 的所有直接子类。
继承关系图示
graph TD
A[Animal] --> B[Cat]
A --> C[Dog]
A --> D[Bird]
B --> E[Persian]
C --> F[Labrador]
D -.- G[Parrot]
style A fill:#f9f,stroke:#333,stroke-width:4px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#bbf,stroke:#333,stroke-width:2px
style D fill:#bbf,stroke:#333,stroke-width:2px
style E fill:#dfd,stroke:#333,stroke-width:2px
style F fill:#dfd,stroke:#333,stroke-width:2px
style G fill:#fdd,stroke:#f66,stroke-width:2px,stroke-dasharray: 5, 5
在这个图中:
- Animal 是密封类,只允许 Cat、Dog 和 Bird 继承。
- Cat、Dog 和 Bird 可以选择是否进一步限制继承。
- Parrot 试图继承 Bird 可能会失败,除非 Bird 允许进一步继承。
Sealed 类的规则
密封类必须有子类
如果一个类被声明为 sealed,它必须至少有一个子类。否则,编译器会报错。
子类的声明方式
Sealed 类的直接子类必须使用以下三种修饰符之一:
- final:表示这个子类不能再被继承。
- sealed:表示这个子类也是一个密封类,需要指定它允许的子类。
- non-sealed:表示这个子类是一个普通类,可以被任意继承。
示例
public sealed class Shape permits Circle, Square, Triangle {
// Shape 的内容
}
public final class Circle extends Shape {
// Circle 不能再被继承
}
public sealed class Square extends Shape permits ColoredSquare {
// Square 只允许 ColoredSquare 继承
}
public non-sealed class Triangle extends Shape {
// Triangle 可以被任意继承
}
public final class ColoredSquare extends Square {
// ColoredSquare 的内容
}
public class IsoscelesTriangle extends Triangle {
// 可以正常继承 Triangle
}Sealed 类的优势
- 明确的继承结构:通过查看密封类的声明,可以立即知道所有可能的子类。
- 防止未经授权的继承:避免了类被意外或恶意继承的风险。
- 优化潜力:编译器和 JVM 可能会利用已知的继承结构进行优化。
- 更好的模式匹配:在未来的 Java 版本中,密封类可能会与模式匹配特性更好地集成。
使用场景
- 领域模型:当你有一个固定的、已知的子类集合时。
- 设计模式实现:例如,在实现状态模式或策略模式时。
- API 设计:当你想限制 API 的扩展方式时
注意事项
- 密封类和它的所有允许的子类必须在同一个模块中(如果使用了模块系统),或者在同一个包中(如果没有使用模块)。
- 使用 non-sealed 可以”打破”密封,允许进一步的任意继承
- Sealed 类的概念主要用于类的设计阶段,它不会在运行时提供额外的安全性。
Java 17 新特性:Switch 增强
背景介绍
Java 17 对 switch 语句进行了进一步的增强,这个增强建立在 Java 14 引入的 switch 表达式基础之上,并与 instanceof 模式匹配结合,提供了更简洁、更强大的语法。
Switch 增强的主要特点
- 与 instanceof 模式匹配结合
- 支持多个条件的简洁写法
- 使用箭头语法 (→) 代替传统的 case 和 break 语句
- 引入了模式匹配的概念
示例
Object obj = // 某个对象
String result = switch (obj) {
case String s -> "It's a String: " + s;
case Integer i -> "It's an Integer: " + i;
case Long l -> "It's a Long: " + l;
default -> "It's something else: " + obj;
};
System.out.println(result);与传统 switch 的对比
传统 switch
String result;
if (obj instanceof String) {
String s = (String) obj;
result = "It's a String: " + s;
} else if (obj instanceof Integer) {
Integer i = (Integer) obj;
result = "It's an Integer: " + i;
} else if (obj instanceof Long) {
Long l = (Long) obj;
result = "It's a Long: " + l;
} else {
result = "It's something else: " + obj;
}增强版 switch
String result = switch (obj) {
case String s -> "It's a String: " + s;
case Integer i -> "It's an Integer: " + i;
case Long l -> "It's a Long: " + l;
default -> "It's something else: " + obj;
};图解 Switch 增强
graph TD
A[Object] --> B{Switch}
B -->|case String s| C[处理字符串]
B -->|case Integer i| D[处理整数]
B -->|case Long l| E[处理长整数]
B -->|default| F[处理其他类型]
C --> G[返回结果]
D --> G
E --> G
F --> G
注意事项
- 这个特性在 Java 17 中仍然是预览特性,需要使用 —enable-preview 标志来启用。
- 使用箭头语法 (→) 时,不需要显式的 break 语句。
- 每个 case 分支都必须要么返回一个值,要么抛出一个异常
优势
- 代码简洁:减少了大量的 if-else 语句和类型转换。
- 类型安全:编译器可以检查类型匹配,减少运行时错误。
- 可读性:代码结构更清晰,意图更明确。
- 性能潜力:编译器可能会对这种结构进行优化。
使用场景
- 处理多态对象:当需要根据对象的实际类型执行不同操作时。
- 复杂的条件判断:替代复杂的 if-else 链。
- 模式匹配:结合 instanceof 进行更复杂的模式匹配。
Spring Boot 3: AOT 与 JIT 介绍
JIT (Just-In-Time) 编译
JIT 是 “Just-In-Time” 的缩写,意为”即时编译”。
JIT 工作原理
graph LR
A[Java 源代码] --> B[字节码]
B --> C[JVM]
C --> D[解释执行]
C --> E[JIT 编译]
E --> F[机器码]
F --> G[直接执行]
- Java 源代码被编译成字节码
- JVM 加载字节码
- JVM 解释执行字节码
- 热点代码被 JIT 编译器编译成机器码
- 直接执行机器码,提高性能
JIT 的优点
- 跨平台:一次编写,到处运行
- 动态优化:根据运行时情况优化代码
- 支持动态特性:如反射、动态加载等
JIT 的缺点
- 启动时间较长:需要预热
- 内存占用较大:需要 JVM 和运行时编译器
AOT (Ahead-Of-Time) 编译
AOT 是 “Ahead-Of-Time” 的缩写,意为”预先编译”或”提前编译”。
AOT 工作原理
graph LR
A[Java 源代码] --> B[AOT 编译器]
B --> C[本地机器码]
C --> D[直接执行]
- Java 源代码直接被 AOT 编译器编译成本地机器码
- 生成的可执行文件可以直接运行,无需 JVM
AOT 的优点
- 快速启动:启动时间可以从秒级降到毫秒级
JIT: ~2000ms
AOT: ~100ms 或更少
-
小体积:
- 不需要完整的 JDK(可能 200MB+)
- 生成的可执行文件较小(几十 MB)
-
云原生友好:
- 快速扩展:可以在短时间内启动多个实例
- 资源效率:占用更少的系统资源
AOT 的缺点
-
不支持跨平台:
- 为 Windows 编译的程序无法在 Linux 上运行
- 需要为每个目标平台单独编译
-
不支持动态特性:
- 无法使用反射、动态代理等特性
- 不支持某些 AOP(面向切面编程)功能
-
编译环境要求:
- 需要目标平台特定的编译工具链
AOT 在 Spring Boot 3 中的应用
Spring Boot 3 引入 AOT 支持,主要用于
- 提高应用启动速度
- 减少内存占用
- 支持生成原生镜像(Native Images)
示例: 使用SpringBoot3的AOT功能
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
</plugins>
</build>使用以上 Maven 配置,可以生成原生可执行文件。
结论
AOT 编译为 Spring Boot 应用带来了显著的性能提升,特别是在启动时间和资源占用方面。然而,由于其限制(如缺乏跨平台支持和动态特性),在生产环境中使用时需要谨慎评估。 AOT 特别适合云原生和微服务架构,可以实现快速扩展和高效资源利用。在选择使用 JIT 还是 AOT 时,需要根据具体的应用场景和需求来权衡。
JIT 在高并发场景中的生产问题分享
问题现象
- 热点应用在重启后出现业务超时
- 几分钟后恢复正常
- 影响所有重启的实例
Java程序执行流程
graph TD
A[Java 源文件] --> B[编译]
B --> C[Class 文件]
C --> D{解释执行还是 JIT 编译?}
D -->|解释执行| E[解释器]
D -->|JIT 编译| F[JIT 编译器]
E --> G[执行]
F --> H[机器码]
H --> G
- Java 源文件编译成 Class 文件
- Class 文件可以通过解释器直接执行
- 热点代码触发 JIT 编译,生成机器码
- 后续执行直接使用机器码,提高效率
JIT 编译触发条件
- 方法被频繁调用(例如超过 10,000 次)
- 在一定时间范围内的高频调用
- 不是所有类都会触发 JIT 编译
问题原因分析
- 高并发应用重启后,大量请求同时涌入
- 多个热点类同时触发 JIT 编译
- JIT 编译过程消耗大量 CPU 资源
- 导致请求处理变慢,出现超时
- JIT 编译完成后,性能恢复正常
解决方案
预热策略
public class Preheater {
public static void preheat() {
for (int i = 0; i < 1000; i++) {
// 调用热点方法
hotMethod1();
hotMethod2();
// ...
}
}
private static void hotMethod1() {
// 热点方法的实现
}
private static void hotMethod2() {
// 热点方法的实现
}
}- 在应用启动后,流量进入前进行预热
- 自动运行热点代码数百次
- 触发 JIT 编译,但不影响实际业务
流量控制
public class TrafficController {
private static final int FULL_TRAFFIC = 100;
private static int currentTraffic = 10; // 初始 10% 流量
public static boolean allowRequest() {
return Math.random() * 100 < currentTraffic;
}
public static void increaseTraffic() {
if (currentTraffic < FULL_TRAFFIC) {
currentTraffic += 10; // 每次增加 10% 流量
}
}
}- 启动时开启小流量(如 10%)
- 逐步增加流量,直到达到正常水平
- 可以设置定时任务,每隔一段时间增加一定比例的流量
实施步骤
- 实现预热机制:
- 识别应用中的热点方法
- 在应用启动后,自动调用这些方法数百次
- 实现流量控制:
- 在负载均衡器或应用入口处实现流量控制逻辑
- 设置定时任务,逐步增加流量
- 监控和调整:
- 监控应用性能和 JVM 指标
- 根据实际情况调整预热次数和流量增加策略
注意事项
- 预热过程不应影响实际业务逻辑
- 流量控制应考虑整体系统的负载均衡
- 持续监控应用性能,及时调整策略
Spring Boot 3 使用 GraalVM 实现 AOT
准备工作
下载并安装 GraalVM
- 访问 GraalVM 官方下载页面
- 选择 Java 17 版本(Spring Boot 3 最低支持 Java 17)
- 下载适合您操作系统的版本
配置环境变量
- 设置 JAVA_HOME 指向 GraalVM 安装目录
- 更新 PATH 变量,包含 GraalVM 的 bin 目录
安装 Native Image
- 在线安装: gu install native-image
- 离线安装:
- 下载 Native Image 组件
- 使用命令:gu install -L
安装 Visual Studio 2022(Windows 用户)
- 下载并安装 Visual Studio 2022 社区版
- 勾选”使用 C++ 的桌面开发”
- 安装 MSVC 和 Windows SDK
创建 Spring Boot 3 项目
- 使用 Spring Initializr 或 IDE 创建项目
- 选择 Spring Boot 3.0.0 版本
- 添加依赖:
- Spring Web
- GraalVM Native Support
项目配置
修改 pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>注意事项
- Maven 仓库路径不要包含中文字符
- 确保使用的是 GraalVM Java 17 版本
AOT 编译
使用 x64 Native Tools Command Prompt
- 打开 x64 Native Tools Command Prompt for VS 2022
- 导航到项目目录
执行编译命令
mvn -Pnative native:compile运行本地可执行文件
- 编译完成后,在 target 目录下找到 .exe 文件
- 直接运行该文件或通过命令行运行
性能对比
- 传统 Spring Boot 启动时间:约 1.5 秒
- AOT 编译后启动时间:约 100 毫秒
局限性
- 不支持某些动态特性,如 AOP(示例中的 LogAspect 无法生效)
- 可能与某些依赖库不兼容
总结
使用 GraalVM 和 AOT 编译可以显著提高 Spring Boot 应用的启动速度和减小部署体积。然而,它也带来了一些限制,特别是在动态特性方面。随着技术的发展,这些限制可能会在未来的版本中得到改善。
上一节 项目日记Day02