从springboot3.0开始,已经不⽀持JDK8了。 从3.0开始,转变为JDK17。
官⽅维护的版本都是SpringBoot3.X的了,但是之前的版本也是可以⽤的,只是官⽅不再进⾏功能更新了.
综上所述,选⽤Java17,概括起来主要有下⾯⼏个主要原因:
1. JDK 17 是 LTS (⻓期⽀持版),可以免费商⽤到 2029 年。⽽且将前⾯⼏个过渡版(JDK 9-JDK 16)去其糟粕,取其精华的版本;
2. JDK 17 性能提升不少,⽐如重写了底层 NIO,⾄少提升 10% 起步;
3. ⼤多数第三⽅框架和库都已经⽀持,不会有什么⼤坑;
语法层面新特性
1、文本块
2 、Switch 表达式增强
yield关键字⽤于从case的代码块中返回值。
正常的switch
public static void main(String[] args) {
String data = "one" ;
int result = 0 ; // 接收数据的返回值
switch (data) {
case "one":
result = 1 ; // 为result重新赋值
break ;
case "two":
result = 2 ; // 为result重新赋值
break ;
default:
result = -1 ; // 为result重新赋值
break ;
}
System.out.println(result) ;
}
简化后的switch
public static void main(String[] args) {
String data = "one" ;
int result = switch (data) {
case "one"->1;
case "two"->2;
default->-1;
};
System.out.println(result) ;
}
如果不想使⽤指向符-> 可以使⽤yield来代替:
public static void main(String[] args) {
String data = "one" ;
int result = switch (data) {
case "one" : yield 1;
case "two": yield 2;
default : yield -1;
};
System.out.println(result) ;
}
3、instanceof的模式匹配
instanceof 增加了模式匹配的功能,如果变量类型经过instanceof判断能够匹配目标类型,则对应分支中无需再做类型强转。
if (o instanceof Integer i && i > 0) {
System.out.println(i.intValue());
} else if (o instanceof String s && s.startsWith("t")) {
System.out.println(s.charAt(0));
}
4、var 局部变量推导
对于某些可以直接推导出类型的局部变量,可以使用var进行声明。
var nums = new int[] {1, 2, 3, 4, 5};
var sum = Arrays.stream(nums).sum();
System.out.println("数组之和为:" + sum);
5、空指针异常
出现异常的具体⽅法和原因都⼀⽬了然。如果你的⼀⾏代码中有多个⽅法、多个变量,可以快速定位问题所在,如果是 JDK8,有些情况下真的不太容易看出来。
JDK8:
JDK17:
模块化及类封装
1、记录类 record
在 JDK17 中,可以声明一种特殊的类,record 。被reocrd定义的类代表的是一种不可变的常量,只能用来描述一种简单的不可变的数据结构。这样我们未来或许就不用再定义一大堆的 BO、 VO 、 DTO 这些只用来进行值传递的复杂对象了。
例如可以这样声明一个带有x,y两个属性的Point类。
public record Point(int x, int y) {
}
然后,这个类只能初始化设置属性值,初始化后,不允许修改属性值,用反射也不行。唯一和我们自己写的 POJO 有点不同的是,获取属性的方法,与属性同名,而不再是getXXX这样的了。
public class RecordTest {
@Test
public void getPoint() throws IllegalAccessException {
Point p = new Point(10,20);
for (Method method : p.getClass().getMethods()) {
System.out.println(method);
}
for (Field field : p.getClass().getDeclaredFields()) {
System.out.println(field);
// 不允许通过反射修改值。
// field.setAccessible(true);
// field.set(p,30);
}
System.out.println(p.x()+"===="+p.y());
}
}
record记录类的实现原理,其实大致相当于给每个属性添加了private final声明。这样就不允许修改。另外,从字节码也能看到,对于record类,同时还实现了toString,hashcode,equals方法,而这些方法都被声明成了final,进一步阻止应用定制record相关的业务逻辑。
2 、隐藏类 Hidden Classes
从 JDK15 开始,JDK 引入了一个很有意思的特性,隐藏类。隐藏类是一种不能被其他类直接使用的类。隐藏类不再依赖于类加载器,而是通过读取目标类字节码的方式,创建一个对其他类字节码隐藏的class对象,然后通过反射的方式创建对象,调用方法。
我们先来一个示例理解一下什么是隐藏类,再来思考隐藏类有什么用处。
比如先编写一个普通的测试类
public class HiddenClass {
public String sayHello(String name) {
return "Hello, " + name;
}
public static void printHello(String name) {
System.out.printf("""
Hello, %s !
Hello, HiddenClass !
%n""", name);
}
}
传统方式下,要使用这个类,就需要经过编译,然后类加载的整个过程。但是隐藏类机制允许直接从编译后的class字节码入手,并且绕过整个类加载的复杂过程,直接使用这个类。
比如,我们可以使用下面的方法获取class字节数组:
public void printHiddenClassBytesInBase64(){
//编译后的 class 文件地址
String classPath = "/Users/roykingw/DevCode/JDK17Demo/demoModule/target/classes/com/roy/hidden/HiddenClass.class";
try {
byte[] bytes = Files.readAllBytes(Paths.get(classPath));
System.out.println(Base64.getEncoder().encodeToString(bytes));
} catch (IOException e) {
e.printStackTrace();
}
}
这样就可以拿到一串编码后的class文件的字节码。接下来,就可以用这个字节码直接生成这个类。例如:
public void testInvokeHiddenClass() throws Throwable {
//class文件的字节码
String CLASS_INFO = "yv66vgAAAD0ANgoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWEgAAAAgMAAkACgEAF21ha2VDb25jYXRXaXRoQ29uc3RhbnRzAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsJAAwADQcADgwADwAQAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwgAEgEAI0hlbGxvLCAlcyAhCkhlbGxvLCBIaWRkZW5DbGFzcyAhCiVuCgAUABUHABYMABcAGAEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAZwcmludGYBADwoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9pby9QcmludFN0cmVhbTsHABoBABpjb20vcm95L2hpZGRlbi9IaWRkZW5DbGFzcwEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAcTGNvbS9yb3kvaGlkZGVuL0hpZGRlbkNsYXNzOwEACHNheUhlbGxvAQAEbmFtZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEACnByaW50SGVsbG8BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAApTb3VyY2VGaWxlAQAQSGlkZGVuQ2xhc3MuamF2YQEAEEJvb3RzdHJhcE1ldGhvZHMPBgApCgAqACsHACwMAAkALQEAJGphdmEvbGFuZy9pbnZva2UvU3RyaW5nQ29uY2F0RmFjdG9yeQEAmChMamF2YS9sYW5nL2ludm9rZS9NZXRob2RIYW5kbGVzJExvb2t1cDtMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL2ludm9rZS9NZXRob2RUeXBlO0xqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9pbnZva2UvQ2FsbFNpdGU7CAAvAQAISGVsbG8sIAEBAAxJbm5lckNsYXNzZXMHADIBACVqYXZhL2xhbmcvaW52b2tlL01ldGhvZEhhbmRsZXMkTG9va3VwBwA0AQAeamF2YS9sYW5nL2ludm9rZS9NZXRob2RIYW5kbGVzAQAGTG9va3VwACEAGQACAAAAAAADAAEABQAGAAEAGwAAAC8AAQABAAAABSq3AAGxAAAAAgAcAAAABgABAAAAAwAdAAAADAABAAAABQAeAB8AAAABACAACgABABsAAAA7AAEAAgAAAAcrugAHAACwAAAAAgAcAAAABgABAAAABQAdAAAAFgACAAAABwAeAB8AAAAAAAcAIQAiAAEACQAjACQAAQAbAAAAQAAGAAEAAAASsgALEhEEvQACWQMqU7YAE1exAAAAAgAcAAAACgACAAAACQARAA0AHQAAAAwAAQAAABIAIQAiAAAAAwAlAAAAAgAmACcAAAAIAAEAKAABAC4AMAAAAAoAAQAxADMANQAZ";
byte[] classInBytes = Base64.getDecoder().decode(CLASS_INFO);
Class<?> proxy = MethodHandles.lookup()
.defineHiddenClass(classInBytes, true, MethodHandles.Lookup.ClassOption.NESTMATE)
.lookupClass();
// 输出类名
System.out.println(proxy.getName());
// 输出类有哪些函数
for (Method method : proxy.getDeclaredMethods()) {
System.out.println(method.getName());
}
// 2. 调用对应的方法
MethodHandle mhPrintHello = MethodHandles.lookup().findStatic(proxy, "printHello", MethodType.methodType(void.class, String.class));
mhPrintHello.invokeExact("loulan");
Object proxyObj = proxy.getConstructors()[0].newInstance();
MethodHandle mhSayHello = MethodHandles.lookup().findVirtual(proxy, "sayHello", MethodType.methodType(String.class, String.class));
System.out.println(mhSayHello.invoke(proxyObj, "loulan"));
}
3 、密封类 Sealed Classes
在 JDK8 中,每一个类都可以被任意多个子类继承,并修改其中的内置功能。比如 JDK8 中最重要的类加载双亲委派机制,在应用当中,程序员可以随意挑选一个内置的类加载器,继承出新的类加载器实现,随意打破双亲委派机制。这其实是不太安全的,意味着很多内置的行为得不到保护。而密封类就是用来限制每一个父类可以被哪些子类继承或者实现。
首先在声明类或方法时,如果增加sealed修饰,那么还需要同时增加permits指定这个类可以被哪些类来继承或实现。例如:
public sealed abstract class Shape permits Circle, Rectangle, Square {
public abstract int lines();
}
接下来,Shape 的子类也会要收到限制。在声明类时,需要声明自己的密封属性。可以有三个选项:
- final,表示这个子类不能再被继承了。
- non-sealed 表示这个子类没有密封特性,可以随意继承。
- sealed 表示这个子类有密封特性。再按照之前的方式声明他的子类。
例如针对 Shape,就可以声明这样的一些子类:
// 非密封子类,可以随意继承
public non-sealed class Square extends Shape{
@Override
public int lines() {
return 4;
}
}
//final 子类,不可再被继承
public final class Circle extends Shape{
@Override
public int lines() {
return 0;
}
}
// 密封子类,继续声明他所允许的子类。
public sealed class Rectangle extends Shape permits FilledRectangle {
@Override
public int lines() {
return 3;
}
}
public final class FilledRectangle extends Rectangle{
@Override
public int lines() {
return 0;
}
}
4、模块化 Module System
JDK8 中,我们写的 Java 代码是在一个一个的package下面的,模块化在包之上增加了更高级别的聚合,它包括一组密切相关的包和资源以及一个新的模块描述符文件。简单点说,module是 java 中package包的上一层抽象。通过module,java 可以更精确的分配对象的生效范围。
比如,在 JDK8 的安装目录下,JDK 预设的功能是以一个一个jar包的形式存在的,但是在JDK17 的安装目录下,你就看不到那些jar包了,取而代之的,是一系列以jmod后缀的文件,这些就是一个一个的模块。
安装 JDK17 后,也可以使用java --list-modules 查看到所有的系统模块。
可以看到,整个 JDK 都已经用模块化的方式进行了重组,并且,在 JDK 的安装目录下,也已经取消了 JRE 目录。这意味着,在新版本的 JDK 中,应用程序可以定制自己的JRE,只选择应用所需要的模块,而不再需要引入整个 JDK 庞大的后台功能。
比如,我们如果只需要使用java.base模块中的类型,那么随时可以用一下指令打包出一个可以在服务器上运行的 JRE:
jlink -p $JAVA_HOME/jmods --add-modules java.base --output basejre
这个basejre就可以像安装 JDK 一样部署到一台新的服务器上运行了。
5 、GC调整
另外,从 JDK8 到 JDK17,还涉及到了非常多的调整,这些调整让 Java 具备了更多现代语言的特性,并且执行效率也在不断提高。
比如,重写了Socket 底层的 API 代码,让这写代码使用更简单,实现也更易于维护和替换。
默认禁用偏向锁。从 JDK1.6 开始,对synchronized关键字的实现就形成了 无锁 ->偏向锁->轻量级锁->重量级锁 的锁升级过程。而在 JDK15 中,默认就废弃了偏向锁。当然,目前还是可以通过添加参数 -XX:+UseBiasedLocking 手动开启偏向锁。
不过 JDK 在升级过程中,一直保持着良好的向下兼容性,因此,有很多优化调整对开发工作影响都还不是太大。但是在这些调整当中,有一部分调整是大家需要额外关心一下的,那就是对于 GC 的调整。
1 、ZGC 转正
ZGC 在之前已经做过介绍,是现在最为强大的一个垃圾回收器,自 JDK11 开始引入,从 JDK15 开始正式投入了使用。现在使用-XX:+UseZGC参数就可以快速使用 ZGC 。
另外,ZGC 的具体实现其实也在版本升级过程中不断优化。在 JDK17 中使用指令 java -XX:+PrintFlagsFinal -version 可以简单看到,与 ZGC 相关的系统不稳定参数已经基本没有了。G1 的还有一大堆
这也说明 ZGC 的算法优化已经相当成熟了。
随 ZGC 登场的,还有 RedHat 推出的Shenandoah 垃圾回收器。尽管 Oracle 一直比较抵触这个非官方推出的垃圾回收器,但是最终也还是将Shennandoah 垃圾回收器以可选的方案集成了进来。现在可以使用 -XX:+UseShenandoahGC 参数手动选择shennandoah。
2 、废除 CMS
虽然 CMS作为 G1 之前唯一的一款并发的垃圾回收器,在相当长的时间里,都扮演者非常重要的角色。在最为经典的 JDK8 时代,尽管 CMS 一直没有作为默认垃圾回收器登场过,但是关于 G1 和 CMS 的比较以及取舍,一直都是业界津津乐道的话题。但是,随着 G1 垃圾回收器发展得更为完善,以及后续ZGC,shennandoah等现代垃圾回收器开始登场,过于复杂的 CMS 垃圾回收器还是退出了历史舞台。
在 JDK14 中,就彻底删除了 CMS 垃圾回收器。与 CMS 一起退场的,还有Parallel Scavenge +SerialOld的经典 GC 组合。SerialOld 这个最早的垃圾回收器其实早就应该退出历史舞台了,只不过由于他一直作为 CMS 的补充方案而一直保留。这次也终于随着 CMS 一起退出了。