笔者近日在做代码仓库的存量代码缩减工作,首先考虑的是基于静态扫描的缩减,尝试使用了很多工具来对代码进行优化,例如 PMD、IDEA 自带的 inspect 功能、findBugs 等。但是无一例外,要么过于 “保守”,只给出扫描结果,但是无法实现一键优化,要么直接就是有 bug(这里特指 IDEA2023.1.5 专业版-inspect 功能扫描 problems 清单里的 unused declaration)。对于懒人而言,挨个手动点击几百次按钮和坐牢无异,遂自己写了一个工具对大部分已明确的优化点进行一键修改(具体是使用 lombok 的@Data注解替换显式的 getter/setter 以及 toString 方法)。
本文内容主要分为三个部分,第一部分详细讲述工具实现的思路,第二部分会对用到的开源工具 javaParser 进行简要的介绍,第三部分提供了工具细致的使用说明。
在翻阅历史代码时,发现不少工程仓库里很多类依然是用的 IDE 生成的 getter/setter,如果使用 Lombok 的@Data注解替换,可以带来几个优点。
•显而易见的是,能够使代码变得更加整洁,减少代码量,并且减少今后新增字段时带来的重复劳动。
•可读性得到了提高,在其他同事参与开发时无需检查 getter/setter 里是否做了逻辑。
•避免遗漏,减少犯错的风险,之前因为其他同事的接口数据漏写 get 方法,徒增了不少的沟通成本。
回过头来看,如果我们要写一个工具,对整个代码工程所有类进行全量扫描,并且使用 lombok 来替换其中的 “没有特殊逻辑” 的 getter 和 setter,需要哪些步骤。
1.扫描整个工程代码,可以是多模块的工程。
2.读取其中的 “.java” 文件。
3.过滤其中不需要的类,例如 interface,没有 field 的类(大概率是作为 service 出现),注解的声明等等。
4.删除 getter/setter 方法,这里需要判断在 get 和 set 方法里是否有特殊逻辑。
5.给类打上@Data注解,并且把 lombok 包引入进来。
6.把修改后的内容写入 java 文件。
下面对每个步骤的实现进行说明。
工程扫描比较简单,给一个工程路径,然后递归调用,过滤出所有的.java 文件即可。
private static List<File> scanJavaFiles(File file) {
List<File> result = Lists.newArrayList();
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files == null) {
return result;
}
for (File f : files) {
result.addAll(scanJavaFiles(f));
}
}
if (file.getName().endsWith(".java")) {
result.add(file);
}
return result;
}
在拿到所有文件的列表之后,需要对其进行处理。
1.过滤掉无字段的类。
2.过滤掉已经使用 lombok 注解的类。
3.判断是否有显式 getter/setter(这里需要注意,boolean 类型的字段需要特殊处理)
4.判断 getter/setter 是否为简单的返回和赋值操作。
5.删除 getter/setter。
6.添加@Data注解。
7.添加 lombok 包的引入。
这里使用 github 上开源的工具 javaParser 来对类进行解析、代码提取、删除以及内容新增,javaParser 会在下一章节进行介绍。
这里简单粗暴了一些,直接使用 equals 判断方法体,其实 javaParser 提供了更完善的 api 来分析语义。
在完成对代码的清理之后,需要将内容更新到 java 文件,CompilationUnit 重写了 toString 方法,可以支持直接将代码转换成字符串的形式。
JavaParser 是一个开源的 Java 源代码分析工具,它提供了一系列简单的 API 来解析、修改和生成 Java 代码。
举个例子,我们可以使用 javaparser 轻松的实现下面几个操作:
1.分析代码中的类、方法、字段等元素,提取类的继承关系、方法的参数和返回类型等。
2.更改源码,例如重命名方法、修改方法体、添加或删除代码行等。
3.可以使用它来生成代码片段,例如创建新的类、方法或字段,或者生成代码文档。
在上一章节里就用到了数据提取,源码替换功能。这里附上 JavaParser 的相关链接:
官网:https://javaparser.org/ github:https://github.com/javaparser/javaparser wiki:https://github.com/javaparser/javaparser.wiki.git javadoc:https://www.javadoc.io/doc/com.github.javaparser/javaparser-core/latest/index.html
JavaParser 的主要构成由以下几个组件组成:
1.Lexer(词法分析器):词法分析器的作用是读取源代码文本,并将其分解成一系列的词法单元(tokens),如关键字、标识符、字面量、运算符等。这是解析过程的第一步。
通常不需要咱们显式调用,JavaParser 将具体的细节实现隐藏在内部,调用方只需要使用开放 api 即可完成源码到 AST 的转换。具体可以翻阅com.github.javaparser.GeneratedJavaParser
2. Parser(语法解析器):语法分析器接收词法分析器生成的 tokens,并根据 Java 语言的语法规则将它们组合成各种语法结构,如表达式、语句、类定义等。这个过程构建出一个抽象语法树(AST)。
com.github.javaparser.JavaParser
这是最常用的类,用于触发解析过程并生成 AST,在上一章节中,使用 StaticJavaParser 将源文件解析成 CompilationUnit,在 parse 方法的内部使用了 JavaParser 完成这一解析过程。
3. AST(抽象语法树):AST 是 JavaParser 的核心数据结构,它以层次化的方式表示了源代码的结构。AST 由一系列的节点组成,每个节点表示源代码中的一个元素,如类、方法、字段、表达式等。每个节点都包含有关该元素的信息,例如名称、类型、修饰符等。
AST 是后续操作(如遍历、分析、修改)的基础,也是使用方操作最频繁的类。上一章节使用的com.github.javaparser.ast.CompilationUnit
是一个非常重要的类,它代表了 Java 源代码文件的根节点,是这个结构的抽象表示,包含整个文件的结构,例如:
• 包声明(Package Declaration)
•导入声明(Imports)
•类型声明(Type Declarations),这可能是类、接口、枚举或注解
•注释(Comments)
•任何顶级的注解
通过操作 CompilationUnit 提供的公有方法,可以访问和修改文件中的元素。包括:
•获取和设置包声明
•获取和添加导入声明
•获取和添加类型声明
•获取和添加注释
•使用访问者模式来遍历 AST 中的节点
4. Visitors(访问器):顾名思义,这是一个采用访问者模式设计的组件,可以用于遍历和操作 AST 。开发者可以编写自定义的 Visitors,通过遍历 AST 来访问特定类型的节点,执行代码分析、重构、生成等任务。
com.github.javaparser.ast.visitor.GenericVisitor
和com.github.javaparser.ast.visitor.VoidVisitor
这两个访问器提供了默认实现,如果需要自定义访问器,可以继承它们来实现自己的业务逻辑。
5. Printer(打印器):这个也很好理解,Printer 用于将 AST 转换回 Java 源代码的字符串表示形式。它可以将修改后的 AST 打印回原始源代码文件,或将 AST 打印为格式化的代码字符串。在上一章节的最后提到的 CompilationUnit 重写的 toString 方法,实际上就是使用了 Printer 来完成 AST 到源码字符串的转换。
上面的一些组件是 javaParser 中比较核心且常用的部分,当然 javaParser 还提供了一些便捷的工具类以及用法,这些内容笔者也没有接触过,有需要的读者可以自行翻阅文档。
第一章提到的 jar 包和源码均已上传私服,可以直接通过 maven 插件的方式运行。
<dependency>
<groupId>com.jd.omni.opdd</groupId>
<artifactId>lombok-replace</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.jd.omni.opdd.tools.lombok.LombokConverter</mainClass>
<arguments>
<argument>../../pop-jingme-customs</argument>
</arguments>
</configuration>
</plugin>
插件中的 argument 节点需要替换成工程的路径,可以是绝对路径也可以是相对路径
执行 mvn exec:java
可以在控制台看到:
使用工具处理完成后,一定要 build 的一下检查是否有编译错误,虽然在删除操作时做了较为严苛的校验,但是有些特殊的变量名可能没有考虑到,这部分问题是可以通过编译检查出来的。
另外,对于没有引用 lombok 的模块,需要手动添加依赖。
代码重构应该像手术刀一样,快、准、狠,正所谓君子生非异也,善假于物也。本文主要起一个抛砖引玉的作用,重点在于 JavaParser 的介绍,笔者写的这个小工具非常简单,之前也写过 B-PaaS 一键生成 matrix.json,一键根据错误码定义生成 i18n 文件,大都不难。
如果发散性思考一下,可以通过 JavaPaser 结合其他平台提供的 open api 做一些有趣的事情,例如结合 JSF、UMP、pfinder 的 API,实现对无调用方的方法清理。
除了代码重构,拿到了语法树后也可以做一些代码可视化的工具,参考之前发布的谢骁同学写的《浅谈代码可视化》。
作者:京东零售 谭磊
来源:京东云开发者社区 转载请注明来源