javac 编译器插件在代码迁移中的简单应用

背景

我们的项目有几个工程,分开开发和迭代,有时候需要将某个工程下的某块业务迁移到另一个工程中(可以简单理解成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结尾,比如:

      arduino 复制代码
      String 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())

目前我们会提示哪些类的哪些方法中有不合理的使用,以及建议怎么改,像下面这样:

实现

根据上面的描述,我们的需求其实比较明确:

  1. 能获取== !=的左右子表达式
  2. 能获得字表达式的类型

如果我们能拿到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阶段完成后,表达式的类型推导也完成了,我们可以直接从 JCExpressiontype字段中直接拿到表达式的类型,这个时候实现我们上面的提示需求也就很容易了,代码如下:

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 依赖对应插件
相关推荐
砖厂小工1 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心2 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心2 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
SimonKing3 小时前
OpenCode AI辅助编程,不一样的编程思路,不写一行代码
java·后端·程序员
FastBean3 小时前
Jackson View Extension Spring Boot Starter
java·后端
Kapaseker4 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴4 小时前
Android17 为什么重写 MessageQueue
android
Seven975 小时前
剑指offer-79、最⻓不含重复字符的⼦字符串
java
皮皮林55114 小时前
Java性能调优黑科技!1行代码实现毫秒级耗时追踪,效率飙升300%!
java
冰_河14 小时前
QPS从300到3100:我靠一行代码让接口性能暴涨10倍,系统性能原地起飞!!
java·后端·性能优化