背景
我们的项目有几个工程,分开开发和迭代,有时候需要将某个工程下的某块业务迁移到另一个工程中(可以简单理解成module源码的copy😂),这个文章不考虑迁移本身,也不考虑模块的划分、api-impl之类的编译约束设计,而是考虑其中一个小问题:在主线中用户id是long类型,而另一条线中因为某些原因uid是String类型,那么原先在主线java业务代码中的 uidA == uidB
或者 uidA != uidB
之类的判断就变成了 String 类型的 ==
or !=
判断,这样显然是有问题的,容易导致业务逻辑出现问题,所以我们需要一种方式能够找到所有java代码中的这种判断,把它改成 equals
调用。
思路
如何才能快速的找到哪些地方有通过 ==
或者 !=
进行String的判断呢?
-
如果是个方法,那很容找,通过IDE的FindUsage就可以找出来了,然而这是一个operator,似乎IDE不支持对 java operator 的FindUsage
-
通过正则,以字符串匹配的方式去找?因为上面提到的是 user id,那我们在代码里面搜
id ==
?这样搜索其实很容易遗漏,比如:-
有些id是通过辅助方法获取的,但是方法名未必以 id 结尾,比如:
getUser() == currentUser()
-
有些局部变量命名不会以id结尾,比如:
arduinoString u = user.id; // xxx if (u != currentUser()) { // xxx }
-
IDE find 似乎没法很好的解决这个问题,那么扫描字节码能否找出这些问题呢?在 java 中,以 s1 == s2
为例,假设 s1 是方法的第一个参数,s2是第二个参数,那么这个相等判断会生成如下的字节码:
csharp
0: aload_0 # 加载本地变量表item0到操作数栈 ,也就是 s1
1: aload_1 # 加载本地变量表item1到操作数栈 ,也就是 s2
2: if_acmpne 9 # 如果不相等,跳到 9
5: iconst_1 # 相等,将 1(也就是 true)加载到操作数栈(这也就是表达式的值)
6: goto 10 # 执行表达式后面的逻辑
9: iconst_0 # 不相等,将 0(也就是 false)加载到操作数栈(这也就是表达式的值)
10: ...
如果我们要通过扫描字节码(比如扫描 if_acmpne, if_acmpeq指令),有几个问题比较难处理:
- 我们需要找到
==
!=
的左右操作数,而单看 if_acmpne,if_acmpeq指令是不行的,得加上数据流分析之类的技术 - 我们需要进行类型推导,不然,我们代码中可能有很多类似:
Looper.myLooper() == Looper.getMainLooper()
的调用都会被报出来,误报太多
另外,其实我们还希望能够提示用户哪个表达式写的不对,应该怎么改(甚至自动改,自动commit)比如上面的例子:u != currentUser()
我们希望能够提示用户把它改成:!TextUtils.equals(u, currentUser())
。
目前我们会提示哪些类的哪些方法中有不合理的使用,以及建议怎么改,像下面这样:
实现
根据上面的描述,我们的需求其实比较明确:
- 能获取
==
!=
的左右子表达式 - 能获得字表达式的类型
如果我们能拿到AST,那就好办了,刚好javac提供了一套plugin机制,可以让我们获取到AST,你可以进行遍历,甚至直接修改AST(当然就我们这个需求而言并不需要~),我之前一个文章里面其实也提到过这个机制的一个运用,为 java 增加 @Private 避免生成access方法同时又可以保证private可见性的机制:mp.weixin.qq.com/s/ZHisCVjO_...
在 javac plugin中我们可以通过 getCompilationUnit
来获取到 AST,然后通过 TreeScanner
来遍历他,对于我们现在要处理的 ==
& !=
其实对应的是一个 binary expression,也就是他里面提供的BinaryTree
,我们可以通过他拿到左右子表达式,当javac ANALYZE阶段完成后,表达式的类型推导也完成了,我们可以直接从 JCExpression
的type
字段中直接拿到表达式的类型,这个时候实现我们上面的提示需求也就很容易了,代码如下:
scss
@Override
public Void visitBinary(BinaryTree node, Void unused) {
Tree.Kind kind = node.getKind();
if (kind == Tree.Kind.EQUAL_TO || kind == Tree.Kind.NOT_EQUAL_TO) {
ExpressionTree left = node.getLeftOperand();
ExpressionTree right = node.getRightOperand();
if ((left instanceof JCTree.JCExpression) && (right instanceof JCTree.JCExpression)) {
Type lType = ((JCTree.JCExpression) left).type;
Type rType = ((JCTree.JCExpression) right).type;
if (lType.getTag() != TypeTag.BOT && rType.getTag() != TypeTag.BOT) {
if ("java.lang.String".equals(lType.toString()) || "java.lang.String".equals(rType.toString())) {
// 这里可以直接输出 warning
// 也可以作为编译错误报出来
}
}
}
}
return super.visitBinary(node, unused);
}
这个逻辑可以放到一个gradle plugin中,然后在 build.gradle中配置一下就OK了:
arduino
// 比如
classpath 'com.xxx:xxx:xxx'// 项目根目录 build.gradle 添加对应组件依赖
apply plugin: 'com.xxx.StringEqChecker'// app模块 build.gradle 依赖对应插件