王子
为了使用新技术,我从 Java8 升到了 Java11
把一个项目从 Java 8 迁移到 Java 11,我该怎么办呢?
最简单的办法,当然是直接强行升级,遇到一个错就改一个错,别看它 low,但是对于一个小型且非核心的项目来说,已经足够了。
当然,对于比较重要的项目,且代码行数不少的情况,最标准的姿势就是对着官方文档,就是这份 Java 11 的迁移说明文档。
https://docs.oracle.com/en/java/javase/11/migrate/index.html
里面详细说明了 Java 8 到 Java 11 可能出现的兼容性问题,并给出了修改措施或建议。
理论上来说,对着官方文档一个字一个字去读,并且把代码的每一行都肉眼扫描一遍,该修改的地方就做出修改,肯定是可以完美迁移的。
但人毕竟不是机器,自己写的代码可能就有上千甚至上万行,还得算上引入的第三方类库,这显然就不是人干的事情了。
所以,就有一款神奇的工具,可以帮我们自动扫描 JDK 升级过程中需要修改或注意的地方,并直接生成一个可视化的 HTML 报告,厉害了!
它是什么
这款工具的名字叫做 EMT4J,即 Eclipse Migration Toolkit for Java,直译过来就是 Eclipse 基金会旗下的,用来迁移的,工具集,为 Java 准备的。
官网地址为:
https://projects.eclipse.org/projects/adoptium.emt4j
官网介绍它的方式非常亲民,直接来了个以 Tom 为主人公的小故事,大概说的是 Tom 准备把项目从 8 迁移到 11,非常痛苦,用上了 EMT4J 后就爽得飞起。
我非常喜欢这种方式,大家感兴趣可以读一读,没有什么难的词汇。
不过官网和推文都包含很多宣传色彩,有很多杂乱信息,作为一枚呆萌的开发者,我还是更喜欢看简单粗暴的代码仓库:
https://github.com/adoptium/emt4j
仅仅一张截图,就包含了核心功能,下载地址,快速上手体验这三个开发者最关注的点。
它怎么用
点击下载地址下载好 EMT4J 后,查看它的目录结构,发现非常简单明了直观。
➜ Downloads tree emt4j-0.3
emt4j-0.3
├── bin
│ ├── analysis.bat
│ └── analysis.sh
└── lib
├── agent
│ ├── emt4j-agent-jdk11-0.3.jar
│ └── emt4j-agent-jdk8-0.3.jar
├── analysis
│ ├── asm-9.2.jar
│ ├── commons-io-2.4.jar
│ ├── commons-lang3-3.8.jar
│ ├── emt4j-analysis-0.3.jar
│ ├── emt4j-common-0.3.jar
│ ├── gson-2.9.0.jar
│ ├── lombok-1.18.8.jar
│ ├── mvel2-2.4.12.Final.jar
│ ├── slf4j-api-1.7.30.jar
│ └── velocity-engine-core-2.3.jar
└── maven-plugin
└── emt4j-maven-plugin-0.3.jar
如果只使用 javaagent 方式来分析项目,那么对于使用者来说只需要关注 agent 目录下的两个 jar 即可。
从 8 迁移到其他 JDK 就使用:emt4j-agent-jdk8-0.3.jar
从 11 迁移到其他 JDK 就使用:emt4j-agent-jdk11-0.3.jar
我们写一个简单的 Hello World 程序,并用 JDK8 编译。
public class Hello {
public static void main( String[] args ) {
System.out.println( "Hello World!" );
}
}
然后对其进行 8 到 11 的迁移分析。
java -javaagent:emt4j-agent-jdk8-0.3.jar=to=11 Hello
对输出结果可视化为 HTML 格式。
sh analysis.sh -o report.html emt4j-XXX.dat
打开这个 HTML 文件,发现输出的结果正符合我们预期。
对嘛,一个 Hello World 程序自然不需要考虑迁移的兼容性问题~
拿一个复杂的项目举例
我们再尝试分析一个较为复杂的项目,依然进行刚刚那几步动作,这回输出的报告有点意思了。
我们就通过目录中的摘要,就可以看到这个项目从 8 迁移到 11 所需要考虑的全部问题了。比如从 JDK 9 java.version 的 schema 发生了变化,点进去。
在如何修复那里,给出了官方说明文档,我们继续点进去。
简单说就是 java.version 这个系统变量所输出的字符串格式发生了变化,你的程序要是依赖这个字符串做截取和判断啥的,就需要小心了。
比如报告中的问题上下文中,就给出了所有可能受之影响的方法,我们举个例子。
在大名鼎鼎的 log4j 中的 AbstractStringLayout 类中有个 isPreJava8 方法,用来判断是否是 Java 8 以前的版本。
// log4j-core:2.10.0
private static boolean isPreJava8() {
String version = System.getProperty("java.version");
String[] parts = version.split("\\.");
try {
int major = Integer.parseInt(parts[1]);
return major < 8;
} catch (Exception var3) {
return true;
}
}
如果使用 Java 11 来运行这个方法,会得到 true,也就是认为 11 是 8 之前的版本,这显然是不对的,简单 debug 一下就知道错在哪了。
当然,我使用的版本是 log4j-core:2.10.0,我相信大名鼎鼎的 log4j 项目一定在之后的版本修复了这个问题。
果然,在某一次 commit 上就专门修复了这个问题。
修复的方式也很 low,就是判断第一个点前面如果是 1,就按照新的方式做判断,即把点后面的数字作为主版本。
嗯,那看来,把这个项目从 8 升级到 11,最稳妥的方式是连这个使用老版本的 log4j 三方依赖也同时升级了。
什么原理
下面探索一下这个项目的原理,GitHub 官网中给出了一张架构图。
左边是 agent 方式分析运行中的项目,上面是分析静态的 Jar 和 Class 文件等,他们仅仅是解析符号时有所不同。
中间部分是,当符号解析完毕后,都需要读取规则 Rules,这些规则 Rules 就是判断是否会出现兼容性问题的逻辑,如果匹配到了,则记录下来。
右下方是,把刚刚记录下来的兼容性问题,用更友好的方式比如 HTML 的形式输出,给最终的用户看。
所以整个架构还是十分清晰的,重点是里面的细节是如何处理的,我这里只说关键的环节。
agent 方式分析可能不太直观,如果你想了解,可以从这个标准 javaagent 项目的 premain 方法开始看起。
我这里正好了解一下另一种直接静态扫描 class 文件或 jar 文件的检查方式,这个就很直观了。
首先我们故意写一个可以触发上面说的 java.version 格式兼容性问题的代码。
class Hello {
public static void main(String[] var0) {
System.out.println("hello");
System.out.println(System.getProperty("java.version"));
}
}
然后把它编译成 class 文件,放入一个 classFiles 文件夹中,作为我们的扫描文件夹。
然后执行 sh 命令,表示检查该文件夹中 8 迁移到 11 的风险项。
sh analysis.sh -f 8 -t 11 -o report.html classFiles
这个 sh 脚本其实就是执行 AnalysisMain 的主方法,并且将上面几个参数作为 args 传入进去。
// org/eclipse/emt4j/analysis/AnalysisMain.java
public class AnalysisMain {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException, URISyntaxException {
...
ReportConfig config = doAnalysis(args, ...);
doReport(config, ...);
}
...
}
所以从这个入口开始看起就好了。
主方法的结构也很简单,doAnalysis 就是对这些 class 文件进行分析,得出结果。doReport 就是将分析结果可视化,比如生成 HTML 文件。
具体调用链我不一一展开,只看很关键的环节。首先 ClassAnalyzer 中的 processClass 方法使用了 ASM 将 class 文件解析为各种符号,保存在 ClassSymbol 对象里。
// org/eclipse/emt4j/analysis/analyzer/ClassAnalyzer.java
protected static void processClass(byte[] classFileContent ...) {
ClassSymbol symbol = ClassInspectorInstance.getInstance().getSymbolInClass(classFileContent);
...
}
然后,由各种规则文件利用这些符号信息做判断,比如 WholeClassRule,表示需要整个 class 文件信息才能做判断的规则。
@RuleImpl(type = "whole-class")
public class WholeClassRule extends ExecutableRule {
...
@Override
protected CheckResult check(Dependency dependency) {
Map<String, Object> mvelMap = new HashMap<>();
mvelMap.put("typeSet", dependency.getClassSymbol().getTypeSet());
mvelMap.put("methodSet", toMethodIdentifierSet(dependency.getClassSymbol().getCallMethodSet()));
mvelMap.put("cpSet", dependency.getClassSymbol().getConstantPoolSet());
Object result = MVEL.eval(mvel2Rule, mvelMap);
if (result instanceof Boolean) {
return ((Boolean) result) ? CheckResult.FAIL : CheckResult.PASS;
} else {
throw new JdkMigrationException("Mvel2 rule file" + mvel2RuleFile + " must return a boolean result!Now result type is : " + result.getClass());
}
}
}
这里将刚刚的符号信息取出来,放入 mvelMap 中,分别是 typeSet 类型集合、methodSet 方法集合、cpSet 常量池集合。
接下来使用 MVEL 这个第三方类库进行判断,这是一个可以使用表达式进行匹配判断的工具类,很方便,规则表达式就写在这个 mvel2Rule 里。
这个 mvel2Rule 有很多,其中检查 java.version 这个兼容性问题的表达式写在下面这个文件里。
// emt4j-common/src/main/resources/default/rule/8to11/data/mvel2-rule-getjavaversion.cfg
methodSet.contains('java.lang.System.getProperty') &&
(
cpSet.contains('java.version') ||
cpSet.contains('java.specification.version') ||
cpSet.contains('java.runtime.version')
)
这个规则表达式很好理解,你不用了解它的写法也能看懂,就是当使用了
System.getProperty
方法,并且字符串常量池中有 java.version 或 java.specification.version 或 java.runtime.version
时,就视作有兼容性问题。
可以看出,这个判断非常粗糙,就是简单的字符串包含判断而已,由此可见,兼容性问题的判断,也逃脱不了这种方式,不要以为里面利用了什么智能分析方法。
当这个兼容性问题被记录下来后,最终输出 HTML 文件的时候,会通过 ResourceBundle 查看官方文档说明,将详细信息写入 HTML 文件中。
最终就看到了效果,就是这么简单。
再说两句
通过体验和了解这个项目,我们可以学到很多东西,麻雀虽小,五脏俱全。
首先它通过 agent 和 sh 两种方式提供给用户使用,但中间的分析判断逻辑都是共用的。
然后,这个项目使用了专门解析 class 文件的 ASM 工具,把符号信息提取出来方便后面使用。
又使用了 MVEL 作为规则判断的工具,使得规则判断只需要写好表达式即可。
最后输出为 HTML 时,为了查询对应兼容性问题对应的官方说明,使用了 ResourceBundle 查找官方文档说明,由此我们可以继续深入,了解下官方文档的查询规范,而且是从代码层面的,十分严谨。
好的工具就是利用了这么多知名的工具,使自己便利。那我们也可以利用这个项目,做些自己的事情。
比如你做的某款工具,甚至你定制的某款 JDK,需要检查业务代码中是否有兼容性问题,或者你就单纯想扫描下业务代码中是否有什么什么你关心的东西,那么你可以仅仅修改这个项目中的各种规则表达式,并且定制化自己的 HTML 报告格式即可出色完成这个功能。
其它的,如何扫描 class,如何解析 class 符号,如何调用规则进行判断,如何输出渲染 HTML,都不用再自己做了。
Hello,
Wondering if you accept guest posts or link inserts on existing posts on 163.174?
How much would you charge for this?
Justin
Hello,
Wondering if you accept guest posts or link inserts on existing posts on 163.174?
How much would you charge for this?
Justin
Hello,
Wondering if you accept guest posts or link inserts on existing posts on 163.174?
How much would you charge for this?
Justin