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 依赖对应插件
相关推荐
爱读源码的大都督3 分钟前
Java已死?别慌,看我如何用Java手写一个Qwen Code Agent,拯救Java
java·人工智能·后端
lssjzmn4 分钟前
性能飙升!Spring异步流式响应终极指南:ResponseBodyEmitter实战与架构思考
java·前端·架构
LiuYaoheng20 分钟前
【Android】View 的基础知识
android·java·笔记·学习
出海小纸条27 分钟前
Google Play 应用被拒-数据安全表单无效(设备上的应用)
android
勇往直前plus27 分钟前
Sentinel微服务保护
java·spring boot·微服务·sentinel
星辰大海的精灵28 分钟前
SpringBoot与Quartz整合,实现订单自动取消功能
java·后端·算法
小鸡脚来咯30 分钟前
一个Java的main方法在JVM中的执行流程
java·开发语言·jvm
江团1io031 分钟前
深入解析三色标记算法
java·开发语言·jvm
天天摸鱼的java工程师40 分钟前
RestTemplate 如何优化连接池?—— 八年 Java 开发的踩坑与优化指南
java·后端
和煦的春风42 分钟前
简单讨论下lmkd 查杀机制
android