1. Java11的定位:为什么它是绕不开的版本
1.1 LTS版本意味着什么
聊Java11之前,得先把它放回JDK的发布历史里看。Java8之后的版本节奏变化很大,Oracle把发布周期改成了半年一次,Java9、Java10都是快速迭代的过渡版本,很多生产环境根本来不及跟进。Java11是2018年9月发布的,它是继Java8之后第一个长期支持版本,官方会提供多年的稳定性更新和补丁支持。这正好戳中了企业级项目的痛点:不是所有人都有精力每半年追一个新版本,但长期不升级又会积累越来越多的技术债。所以Java11在JDK的版本史上扮演了一个“承上启下”的角色,一边把Java9以来的模块化、新API等积累沉淀下来,一边作为新的LTS基准线,成了大量团队从Java8迁移的首选目标。
作为开发者,我个人的体感是:Java8到Java11之间的变化远比表面看起来大。语法层面改动不多,真正硬核的是JVM内部、类库、工具链、许可模式等一系列配套调整。很多从Java8直接跳到Java11的团队,一上来就被模块化、被移除的Java EE包、被新的GC参数搞得焦头烂额,但只要跨过这道坎,后面再往Java17、Java21升级就没那么痛苦了。
1.2 从Java8迁移到Java11,到底图什么
经常有人问我,Java8又不是不能跑,为什么要折腾升级?我的回答一般集中在三件事上:安全补丁、新特性落地、长期维护成本。Java8虽然是神级版本,但它的公共更新早已停止,继续用意味着要么自己扛漏洞风险,要么付费买商业支持。而Java11作为LTS,大厂和开源社区都在持续维护,生态兼容性也在不断验证中,选它做基线比守着Java8要省心得多。
另外从特性收益看,Java11解决了几个真实痛点:String和Files类补了多年缺失的API,HttpClient终于从“第三方依赖”变成了“官方标配”,GC层面也有了ZGC和Epsilon这样的新方向。这些改进不会让Java一夜之间变成另一门语言,但确实让日常编码和性能调优有了更多顺手工具。这篇博文就从语法、API、性能、工具链、迁移实战几个角度,把Java11值得关注的新特性逐个拆开讲清楚。
2. 日常编码手感提升:语法与核心库的新东西
2.1 var局部变量类型推断:让代码更瘦身
Java10引入的局部变量类型推断,在Java11里被继续支持和巩固。你可以在局部变量声明时用var替代显式类型,编译器会根据初始化表达式自动推断类型。比如:
var list = new ArrayList<String>(); var stream = list.stream().filter(s -> s.length() > 3); var map = new HashMap<String, List<Integer>>();我见过很多人第一次看到var的反应是:“这不就是JavaScript吗?”实际上二者有本质区别:Java的var是编译期推断,类型在编译完成后就已经确定,运行时的类型安全和反射行为跟显式声明完全一致,它不会引入动态类型,也不会让Java变成弱类型语言。
使用var有几个值得注意的边界,我直接列一下:
var只能用于局部变量声明,不能用于成员变量、方法参数、返回类型。- 必须在声明时就初始化,否则编译器无法推断。
- 不能声明为
null,因为var x = null;推断不出具体类型。 - 在Lambda表达式的参数上可以使用var,但必须带注解时才需要这么做,否则直接写类型或者省略即可:
// 这样是允许的(Java11开始) BiFunction<Integer, Integer, Integer> f = (var a, var b) -> a + b; // 如果没加注解,直接这样写更简洁 BiFunction<Integer, Integer, Integer> f2 = (a, b) -> a + b;从实际的团队协作看,我对var的建议是“局部变量大胆用,但别打爆可读性”。像var result = service.querySomething();这种就很好,一眼能看出语义。但如果类型本身是关键信息,比如var price = getPrice();返回BigDecimal还是Double会影响后续运算,那我会倾向保留显式类型。
2.2 String类新方法:终于不用再写工具类了
Java11给String类加了几个特别“解渴”的方法,几乎是日常开发里高频使用的。先说isBlank(),它判断字符串是否为空白,比trim().isEmpty()更全面,因为它会处理所有Unicode空白字符,而不仅仅是空格。
String s1 = " \n\t "; System.out.println(s1.isBlank()); // true String s2 = " java11 "; System.out.println(s2.strip()); // "java11"注意strip()和trim()的区别:trim()只去掉码点小于等于 U+0020 的字符,而strip()根据Character.isWhitespace()来判断,能正确处理全角空格、Unicode空格等场景。如果代码里要处理国际化文本,strip()更靠谱。
再就是repeat(int count),这个对生成测试数据、填充字符串特别有用:
String line = "-".repeat(50); System.out.println(line); // 50个连字符还有lines()方法,它把字符串按换行符拆成Stream,比手写split("\\r?\\n")要贴合真实文件内容,因为《Java Platform SE 8 Spec》定义的换行符包括\n、\r\n、\r。举个读取多行配置项的例子:
String config = "host=localhost\nport=8080\r\ntimeout=3000"; config.lines() .filter(s -> !s.isBlank()) .map(s -> s.split("=")) .forEach(kv -> System.out.println(kv[0] + " -> " + kv[1]));这类API的补充并不是Java11才有的思路,但确实把开发者从大量字符串工具类里解放出来了。
2.3 Files与集合API的实用增强
Java11给Files类加了一组读写文件的便捷方法,核心就是readString和writeString。在没有它们之前,读一个小文件需要处理InputStream、BufferedReader、Charset等一堆对象,现在一行搞定:
Path path = Paths.get("demo.txt"); // 写文件,默认UTF-8,文件不存在会创建 Files.writeString(path, "hello java11"); // 读文件,按UTF-8解码 String content = Files.readString(path); System.out.println(content);两个方法都有重载版本,可以指定Charset或OpenOption:
Files.writeString(path, "中文内容", StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);这里有个容易踩的坑:writeString默认打开方式是CREATE, TRUNCATE_EXISTING, WRITE,也就是说如果文件已存在,会直接覆盖。如果你的需求是追加,必须显式传入StandardOpenOption.APPEND,否则会把之前的内容冲掉。
集合API方面,Java11给Collection.toArray加了一个带IntFunction的重载方法,写法和效率都比以前好。以前把集合变成数组要这么写:
List<String> list = new ArrayList<>(List.of("a", "b", "c")); String[] arr = list.toArray(new String[0]);Java11可以这样写:
String[] arr = list.toArray(String[]::new);String[]::new会被编译成一个IntFunction,由集合内部根据需求创建指定长度的数组。从性能角度看,JDK官方是做过度化处理的,不需要你手工指定数组长度来“省内存”,反而传入一个空数组更高效。
3. 网络编程进入标准化时代:HttpClient正式转正
3.1 为什么还需要一个新的HTTP客户端
Java领域最不缺的就是HTTP客户端,Apache HttpClient、OkHttp、Spring的RestTemplate,任挑一个都能干活。那Java官方为什么还要在Java11里推出标准化的java.net.http.HttpClient?核心原因有两条:一是老的HttpURLConnection确实难用,API设计老套,支持HTTP/2和WebSocket能力很差;二是JDK一直缺乏一个“官方默认”的现代HTTP客户端,导致每个框架都自己造轮子,升级依赖时经常互相打架。
Java11的HttpClient在JDK9时以孵化模块进入,经过两个版本的打磨后在Java11正式转正。它支持HTTP/1.1和HTTP/2、原生异步请求、服务端推送(虽然使用条件较严格)、WebSocket,还可以配合CompletableFuture做响应式组合。对于新项目来说,不引入第三方依赖就能完成HTTP调用,这对内网微服务场景特别友好。
3.2 同步请求写法与核心参数
先看一个最基础的同步GET请求:
HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .version(HttpClient.Version.HTTP_2) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/users")) .header("Accept", "application/json") .GET() .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body());这里有几个参数需要解释一下。connectTimeout设置的是建立连接的超时时间,不是整个请求的响应超时,这是新手最容易忽略的点。如果你需要限制“请求发出到拿到响应”的总时间,得自己用异步方式加上超时逻辑,或者使用HttpRequest.Builder.timeout(Duration)来设置请求级超时:
HttpRequest request = HttpRequest.newBuilder() .uri(uri) .timeout(Duration.ofSeconds(30)) .POST(BodyPublishers.ofString(jsonBody)) .build();BodyHandlers.ofString()是把响应体转换成一个字符串,这是最简单的消费方式。如果响应体很大,也可以用BodyHandlers.ofInputStream()转成流,或者用BodyHandlers.ofFile(Path)直接落盘。
3.3 异步请求:CompletableFuture一趟到底
异步是Java11 HttpClient最香的特性之一。你发起一个请求,不用阻塞当前线程,等响应回来后代理回调:
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/data")) .GET() .build(); CompletableFuture<HttpResponse<String>> future = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); future.thenApply(HttpResponse::body) .thenAccept(System.out::println) .join();这种写法的好处是:代码的书写顺序跟执行顺序一致,但不会阻塞调用线程。结合CompletableFuture的thenXxx系列方法,可以在一个调用链里完成“多接口并行请求—聚合结果—统一处理”的需求,比如同时调用户服务和订单服务,然后把结果拼装后返回。
我在生产环境用过一段时间之后,最大的体会是:如果对吞吐量有要求,尽量用sendAsync而不要在线程池里开一堆线程做同步请求。HttpClient内部有自己的连接复用和调度机制,配合虚拟连接池可以实现很高的并发效率,而且不需要自己手动管理线程生命周期。
3.4 WebSocket支持与实战细节
HttpClient还内建了WebSocket客户端支持,对于需要长连接推送的场景非常实用。示例代码看起来大概是这样的:
HttpClient client = HttpClient.newHttpClient(); WebSocket ws = client.newWebSocketBuilder() .buildAsync(URI.create("wss://echo.example.org"), new WebSocket.Listener() { @Override public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) { System.out.println("received: " + data); webSocket.request(1); return CompletableFuture.completedFuture(data); } @Override public void onOpen(WebSocket webSocket) { System.out.println("connected"); webSocket.sendText("hello", true); } }) .join(); // 等几秒再关闭 Thread.sleep(5000); ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye");注意Listener回调里的webSocket.request(1),这是一行必须写的代码,它表示已经准备好接收下一条消息。如果没有调用request续命,连接会在收到一条消息后停止接收。这是WebSocket API设计里比较容易被忽略的地方,实际排查线上连接“莫名断流”的时候,多半就是request计数没有续上。
4. GC与性能:ZGC和Epsilon带来的想象力
4.1 ZGC:可伸缩的低延迟垃圾回收器
Java11在GC层面最吸引眼球的是ZGC(Z Garbage Collector)。它的核心目标很明确:将STW(Stop-The-World)暂停时间控制在10毫秒以内,而且暂停时间不随堆大小线性增长。也就是说,你从32GB堆扩容到512GB堆,GC暂停时间依然能维持在一个很低的水平,这在以前大部分GC实现中是不可想象的。
在Java11里,ZGC还是实验特性,需要通过-XX:+UnlockExperimentalVMOptions -XX:+UseZGC显式启用。我用它跑过一个内部服务,堆内存设置在16GB左右,在压测请求下,GC暂停基本稳定在个位数毫秒级别,比G1的几十毫秒表现好不少。但要提醒的是,Java11的ZGC还不支持分代,也没有类卸载,一些长期运行、类加载频繁的场景可能要特别关注元空间状态。
ZGC的目标人群很明确:超大堆、延迟敏感、不能容忍频繁Full GC的应用。比如证券交易系统、实时推荐服务、在线游戏后端。如果你的应用只是几十MB的堆,老老实实G1就够用了,ZGC的优势发挥不出来,反而会增加调优成本。
4.2 Epsilon:一个什么都不做的GC
Java11另一个很有意思的GC叫Epsilon,它最“极端”的地方在于:完全不进行任何垃圾回收。堆内存耗尽时,直接抛异常退出。你可能会问,一个不回收垃圾的GC有什么用?搞活动吗?
其实Epsilon的定位不是面向生产环境的垃圾回收器,而是用于性能测试、短期任务、了解内存分配行为的实验工具。比如你想精确测量某段代码分配了多少内存、分配速率是多少,用Epsilon排除掉GC的影响,数据的可信度就高得多。再比如命令行一次性短小工具,分配完就直接结束进程,根本不需要GC来回收。启用方式和ZGC类似:
java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx1g -jar short-lived-app.jar一句话总结Epsilon的适用边界:堆生命周期短于垃圾回收周期的场景,可以靠它压榨性能;长生命周期应用用它就是“自杀式运行”,不要在生产环境动这个念头。
4.3 JFR:飞行记录器正式开源
JFR(Java Flight Recorder)最早是Oracle JDK的商业特性,Java11里正式开源并收录进OpenJDK。JFR可以记录JVM运行时的各种事件——GC活动、方法采样、锁竞争、IO、类加载等等,而且设计目标是高性能低开销,官方说开销通常低于1%。这意味着在生产环境常驻开启,对应用的影响几乎可以忽略。
日常排查问题时,我最喜欢它的一点是“事后诸葛亮”式的问题定位。你不必提前预判到底哪里会出问题,只要开着JFR,出了问题后用JMC(Java Mission Control,Java任务控制工具)打开记录文件,就能看到故障时间段前后的线程栈、GC、锁和IO状态。这在处理一些偶发性延迟尖刺问题时有奇效。
Java11里JFR的开机参数已经从“必须先解锁商业特性”变成了可以直接使用:
java -XX:StartFlightRecording=duration=60s,filename=app.jfr -jar app.jar然后就可以把这个app.jfr拉到本地用JMC打开分析。我在团队内部经常建议大家统一做一轮JFR采样,再开始压测,这样能拿到一个应用在“健康状态”下的基线数据。后面出现诡异的性能劣化时,对比基线和故障快照,往往能快速定位到异常点。
5. 安全、协议与兼容性调整
5.1 TLS 1.3:更安全的网络传输
Java11默认支持TLS 1.3,这是全站HTTPS时代必须关注的一个升级。TLS 1.3相比1.2最直观的改善是握手速度:它把握手所需的往返次数从两次RTT降到了最少一次RTT,对于移动端、弱网环境的体验提升很明显。安全上,TLS 1.3废弃了RSA密钥交换以及一系列弱加密套件,迫使双方使用前向保密的密钥协商机制。
从开发者视角,这个特性的迁移成本其实很低。如果你的代码没有显式去指定TLS版本,升级到Java11之后,很多场景会自动开始协商到TLS 1.3。但有一种情况要留意:如果JDK默认的TLS协议配置里把你依赖的某个老版本禁用了,而你的服务端还没升级,可能会出现握手失败。解决办法是临时调整协议配置:
java -Djdk.tls.client.protocols=TLSv1.2 -jar app.jar这只是过渡方案,最根本的还是要推动服务端升级,毕竟TLS 1.2本身没有任何问题,但旧协议的兼容性总有一天会变成你的运维负担。
5.2 单文件源码启动:小脚本的福音
Java11引入了一个很有意思的启动方式,直接用java命令执行单个.java文件,而无需先用javac编译。也就是说:
// Hello.java public class Hello { public static void main(String[] args) { System.out.println("Hello Java11"); } }在命令行里直接java Hello.java就能运行,完全不用管class文件。这个特性对写一次性脚本、做算法题、快速验证某个API的行为特别方便。我从Java8时代过来,以前要快速测一段代码得开IDE建项目,现在直接java Test.java一把梭。
不过有几点限制得说清楚:单文件源码启动只适用于程序入口就是当前这个文件的情况,如果代码里依赖了别的class文件或者jar包,就不适用了;也不支持classpath参数。它解决的是“编译->运行”的最短路径问题,而不是一个完整的应用打包方案。
5.3 Java EE模块被移除,你的项目中招了吗
Java11做了一个硬核的减法:把JDK中与Java EE相关的模块整个移除,包括java.xml.ws(JAX-WS)、java.xml.bind(JAXB)、java.activation(JAF)、java.xml.ws.annotation(Common Annotations)等。因为在Java SE和Java EE的长期“纠缠”中,这些模块既不是Java SE规范的一部分,又会跟外部提供的实现产生冲突,Oracle认为它们放JDK里弊大于利,干脆从标准JDK中剥离。
对开发者,最直接的影响是:如果你在Java8里习惯了直接使用javax.xml.bind.JAXBContext来做XML和Java对象的转换,升到Java11后代码会直接报ClassNotFoundException。解决办法是显式引入第三方依赖,比如Maven里加上JAXB的独立版本:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.2</version> </dependency>我遇到过一些Spring Boot老项目,升级Java11后莫名启动失败,排查半天发现就是JAXB依赖缺失。这里给一个通用检查思路:如果项目里还在用javax.xml.bind、javax.ws、javax.jws这些包名,先确认有没有对应的独立依赖,别让代码偷偷依赖JDK内置模块。
5.4 Nashorn引擎废弃,脚本集成怎么办
Java11把Nashorn JavaScript引擎标记为废弃,后续版本中逐步移除。这对那些在Java应用里用Nashorn执行JS脚本做规则引擎、动态配置的项目影响不小。官方推荐的替代方案是GraalVM的JavaScript引擎,不过GraalVM的集成复杂度高不少。
我的建议是:如果你只是偶尔在Java里跑一小段JS脚本,优先考虑能不能换个实现思路,比如把规则改成数据库配置、Groovy脚本、或者干脆用Java本身的动态编译。如果确实还需要在JVM里跑JS,那就尽早调研GraalVM的迁移方案,拖得越晚成本越高。Nashorn在Java11不是立刻不能用了,只是“废弃”状态,但废弃意味着后续不会再修复关键漏洞,用它承载核心业务风险太大。
6. 实操:在Windows 10上配置Java11环境
6.1 下载安装JDK11
结合不少人在搜索“java11下载”“win10中java11怎样配置环境变量”,这块我单独写一个完整的实操流程。JDK11的发行版选择很多,OpenJDK官方构建、Oracle JDK、Adoptium(原AdoptOpenJDK)等都可以。对大多数开发者,我建议优先用Adoptium的构建版本,完全免费、长期更新,社区维护也活跃。
下载时注意区分x64和x86架构(现在基本都用x64,如果电脑是Apple Silicon这种就另行考虑)。安装版下一步下一步即可,安装路径最好避免带空格和中文字符,比如D:\Java\jdk-11。有些发行版只提供压缩包格式,解压到固定目录也一样能用。
6.2 配置环境变量:JAVA_HOME、Path、CLASSPATH
Win10下配置环境变量的路径是:右键“此电脑” -> 属性 -> 高级系统设置 -> 环境变量。第7章避坑清单实际来源 ## 7. 从Java8升到Java11的常见坑
7.1 反射与模块化限制:IllegalAccessError从哪来
Java9引入的模块化系统(JPMS)是Java11底层架构最大的变化之一,它对反射的约束直接影响了很多框架和业务代码。Java8时代,反射可以随便访问或修改非public字段和方法,Java11里如果你访问的类属于JDK内部模块,而且没有显式开放对应的包,大概率会遇到IllegalAccessError或InaccessibleObjectException。
我在帮团队迁移时遇到最多的就是CGLIB和反射代理库在Java11下的兼容性问题。比如有些老版本框架会在运行时动态生成子类并反射访问目标类的方法,这种行为在模块化环境下会被拦截。解决办法分几层:
- 优先升级框架版本,Spring、MyBatis等主流框架在Java11发布后短时间内就做了适配。
- 如果框架暂时没法升级,可以尝试在启动参数里打开“非法访问许可”:
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar不过这属于“拆东墙补西墙”,一旦涉及多个内部包,参数越堆越长,还是得靠升级解决。更优雅的方式是用jdeps命令分析项目对JDK内部API的依赖,提前发现潜在问题。命令用起来很简单:
jdeps --multi-release 11 -s your-project.jar输出结果会列出你依赖了哪些内部包,其中JDK internal API的部分就是要重点处理的。
7.2 被移除的模块:JAXB只是开始
前面提过,Java11移除了JAXB、JAX-WS、JAF等Java EE模块,迁移过程中的连锁反应比预想中要多。我先整理一份我实际遇到过的“缺失类速查表”,方便大家对照排查:
| 原JDK内置模块 | 常用包名 | Java11现状 | 替代方案 |
|---|---|---|---|
| java.xml.bind | javax.xml.bind | 已移除 | 引入jaxb-api + jaxb-runtime依赖 |
| java.xml.ws | javax.xml.ws | 已移除 | 引入jaxws-api等依赖,或改用REST方案 |
| java.activation | javax.activation | 已移除 | 引入activation依赖(如jakarta.activation) |
| java.xml.ws.annotation | javax.annotation | 已移除 | 引入javax.annotation-api依赖 |
| java.corba | org.omg.CORBA | 已移除 | 使用RMI或其他RPC方案替换 |
| jdk.xml.bind | com.sun.xml.bind | 已移除 | 使用独立JAXB实现 |
这里有个容易被带偏的细节:有些项目报的错不是类找不到,而是启动时出现“模块不可读”或“包不可见”的异常,实际上根源还是依赖缺失。我的排查顺序是先看异常堆栈里涉及的包名是不是javax.xml开头,如果是,优先检查Maven依赖树里有没有引入对应的第三方实现。
7.3 线程和并发API的细微变化
Java11在并发方面的主要变化有两个:Thread类的stop()、destroy()等废弃方法被彻底移除了,同时新增了Thread API的里程碑式调整——用Thread.onSpinWait()实现自旋等待提示。从Java8迁移过来的代码如果还在用这些老API,编译阶段就会报错,IDE会提醒你替换方案。一般改成volatile标记配合LockSupport或CountDownLatch即可。
另外,Java11还新增了针对I/O密集型的并发原语,比如CompletableFuture的completeOnTimeout和orTimeout方法,这让异步超时控制变得更优雅。在老代码里,我们经常是future.get(timeout, TimeUnit.SECONDS)来强制阻塞等待,Java11可以这样:
CompletableFuture<HttpResponse<String>> future = client .sendAsync(request, HttpResponse.BodyHandlers.ofString()) .orTimeout(5, TimeUnit.SECONDS);如果5秒内没有完成,future会以TimeoutException异常结束,你可以通过exceptionally方法处理兜底逻辑。这在构建微服务调用链时非常实用,能避免一个下游慢接口拖垮整个上游线程池。
7.4 排查工具清单:用好手上的兵器
迁移到Java11后,排查问题的工具箱也要跟上。我日常工作用得最多的几个命令和工具简单列一下,方便选择题检查:
| 工具/命令 | 用途 | 一句话经验 |
|---|---|---|
| jshell | 快速验证Java语法和API | 比写Test类快得多,适合临时验证函数行为 |
| jcmd | 查看JVM信息、执行GC等 | 支持jcmd pid GC.heap_info查看堆使用情况 |
| jfr | 记录JVM飞行记录 | 用-XX:StartFlightRecording常驻记录,出问题后分析 |
| jdeps | 分析字节码依赖 | 迁移前先用它扫描内部API依赖,减少踩坑 |
| jlink | 定制运行时镜像 | 配合模块化可以把运行时压缩到几十MB,适合容器镜像 |
这些工具都不是Java11才有,但Java11把它们的能力提升到了更稳定的程度,尤其是JFR开源后,生产环境抓问题的能力跟商业版时代完全不在一个量级。
8. 写在最后:我的几点实操体会
从Java8跨到Java11,这个升级过程并不会让你“一夜变强”,但对项目的长期健康度来说绝对值得。我个人从2018年底开始尝试把内部项目迁移到Java11,当时最大的阻力不是技术本身,而是团队的思维惯性——大家习惯了Java8的API和默认行为,一看到模块化和新GC就觉得风险大。实际上,只要把迁移前分析做扎实,用jdeps扫一遍内部API依赖,把JAXB这类被移除模块提前替换掉,再按照“编译 -> 测试 -> 压测 -> 灰度”的顺序逐步推进,Java11的兼容性比想象中要好很多。
最后再分享一个小技巧:如果你还在Java8上,又暂时不能整体迁移,可以在工程的pom.xml里设置Maven编译器的release参数,强制用Java11的API来编译代码,但运行时仍然跑在Java8上。这样做可以在不升级环境的情况下提前发现API差异,给后续迁移做铺垫。等到真正切换那一天,代码层面的意外会少很多。