自动化工具 代码手术刀 - 自定义你的代码重构工具

京东云开发者 · February 06, 2024 · Last by simonpatrick replied at February 09, 2024 · 4010 hits

前言

笔者近日在做代码仓库的存量代码缩减工作,首先考虑的是基于静态扫描的缩减,尝试使用了很多工具来对代码进行优化,例如 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;
}


使用注解替换 getter/setter

在拿到所有文件的列表之后,需要对其进行处理。

1.过滤掉无字段的类。

2.过滤掉已经使用 lombok 注解的类。

3.判断是否有显式 getter/setter(这里需要注意,boolean 类型的字段需要特殊处理)

4.判断 getter/setter 是否为简单的返回和赋值操作。

5.删除 getter/setter。

6.添加@Data注解。

7.添加 lombok 包的引入。

这里使用 github 上开源的工具 javaParser 来对类进行解析、代码提取、删除以及内容新增,javaParser 会在下一章节进行介绍。

这里简单粗暴了一些,直接使用 equals 判断方法体,其实 javaParser 提供了更完善的 api 来分析语义。

更新 java 文件

在完成对代码的清理之后,需要将内容更新到 java 文件,CompilationUnit 重写了 toString 方法,可以支持直接将代码转换成字符串的形式。

JavaParser 介绍

JavaParser 是什么?

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.GenericVisitorcom.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>


添加 maven 插件

<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,实现对无调用方的方法清理。

除了代码重构,拿到了语法树后也可以做一些代码可视化的工具,参考之前发布的谢骁同学写的《浅谈代码可视化》。

作者:京东零售 谭磊

来源:京东云开发者社区 转载请注明来源

共收到 2 条回复 时间 点赞

“判断 getter/setter 是否为简单的返回和赋值操作。” 这段部分对我还挺有用的,感谢分享

做完这些应为不太确定是不是完全正确,然后需要测试再全量回归一下,有时想想测试确实痛苦,这本来就是不存在的事情。及不重要又不紧急的事情,最后被推到了又紧张,又紧急的事情。关键是万一问题还要背锅,没出问题都是应该的。测试开发何苦折腾测试呢。 就这个事情,和代码重构真的不在一个维度上的,代码根本就没有重构, 重构什么了?那个 Bean 还是那个 Bean,只不过是编译的时候把 GET/SET 方法给自己生成了而已,对代码没有做任何形式的重构。

提高效率是好事,但是提高到代码重构工具,这肯定上不合适的,这肯定说不上重构,哪怕一点都说不上。

@Data和用 IDE 直接生成 GET/SET 没大区别,就是看着烦了点,但是 Lombak 说不定还存在点坑,然后不只不觉的就引入了风险。

一切都是矛盾呀,一面说管控变更,一面一动手就用代码改代码了, 代码改代码风险并不会小。

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up