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 依赖对应插件
相关推荐
阿伟*rui2 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj4 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck4 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei4 小时前
java的类加载机制的学习
java·学习
Yaml46 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~6 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616886 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7896 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java7 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~7 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust