JavaParser是一种强大的代码分析工具,用户帮助开发者优化代码质量,
通过使用JavaParser,开发者可以轻松的剔除项目中无用类、方法等冗余代码,从而改善代码的可读性和维护性。
JavaParser具有较高的灵活性和准确性,它能够解析java代码,并构建一个标识该代码结构的抽象语法树(AST)。通过对AST的分析,JavaParser可以识别出来未使用的类、方法等,准确地定位到项目中冗余代码。
实现思路
- 使用静态分析工具(JavaParser)解析代码,构建代码的抽象语法树(AST)
- 遍历AST,识别所有的类申明和类的引用
- 建立一个类的引用关系图,记录每个类被哪些其他类引用
- 对于每个类,检查它是否被其他类引用,如果没有被引用,可以认为它是未被使用
- 3.1、一键发现无用类
- [3.1、一键发现无用方法 导航:一键发现无用方法
1、添加依赖
<!-- javaParser -->
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>3.25.8</version>
</dependency>
<!-- hutool -->
<dependency>
<groupId>org.dromara.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>6.0.0-M10</version>
</dependency>
2、实现思路,JavaParser简单介绍
2.1、构建代码的抽象语法树
// 创建解析代码对象
JavaParser javaParser = new JavaParser();
// 解析分析代码,构建AST
CompilationUnit compilationUnit = javaParser.parse(file).getResult().orElse(null);
2.2、遍历AST识别所有的类申明和类的引用
// 自定义visitor,用户访问所有节点并遍历AST
ClassVisitor visitor = new ClassVisitor(stripFileName);
visitor.visit(compilationUnit, referencedClassesList);
2.3、类的引用关系图
FieldDeclaration表示类中的字段申明和定义的变量,MethodDeclaration表示方法;以下是一个简单的示例代码
// 检查当前类的成员
for (BodyDeclaration < ? > member : n.getMembers()) {
// 检查当前类中的字段申明和定义的变量如:private final CaptureExceptionService captureExceptionService;
if (member instanceof FieldDeclaration) {
FieldDeclaration field = (FieldDeclaration) member;
// 检查字段的类型
String fieldType = field.getCommonType().asString();
if (CharSequenceUtil.equals(fieldType, className)) {
referencedClasses.add(className);
}
else if (CharSequenceUtil.contains(fieldType, className)) {
// 字段申明和定义的变量可能包含泛型,如:private List< Class > xxxList
// 【缺失逻辑】:还需要判断referencedName是否被引用,如果该类未被引用则className也未被引用
String referencedName = n.getName().asString();
referencedClasses.add(className);
}
}
else if (member instanceof MethodDeclaration) {
MethodDeclaration method = (MethodDeclaration) member;
String returnType = method.getType().asString();
// 返回值匹配对应的类型也算引用【包含泛型】:PageSerializable < CollSimDTO >
if (CharSequenceUtil.equals(returnType, className)
|| CharSequenceUtil.contains(returnType, className)) {
referencedClasses.add(className);
}
// 检查方法的参数类型【包含泛型】:PageSerializable < CollSimDTO >
for (Parameter parameter : method.getParameters()) {
String paramType = parameter.getTypeAsString();
if (CharSequenceUtil.equals(paramType, className)
|| CharSequenceUtil.contains(paramType, className)) {
referencedClasses.add(className);
}
}
// 检查方法中的对象创建表达式
method.accept(new ObjectCreationVisitor(className), referencedClasses);
}
}
2.4、检查它是否被其他类引用
// 输出引用了目标类的其他类
Map < String, Integer > unusedMap = MapUtil.filter(referencedMap, entry -> entry.getValue() == 0);
List < String > unusedClassList = Lists.newArrayList(unusedMap.keySet());
System.out.println("Unused Classes:" + unusedClassList.size());
for (String unusedClass : unusedClassList) {
System.out.println(unusedClass);
}
3、代码质量的利器
3.1、一键发现无用类
3.1.1 分析未使用类代码如下
import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.BodyDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.google.common.collect.Lists;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.date.StopWatch;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.map.MapUtil;
import org.dromara.hutool.core.text.CharSequenceUtil;
import org.dromara.hutool.core.util.ObjUtil;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 分析未使用类
*
* @author 管理员
* @since 2024/2/19
*/
public final class AnalyzerUnusedClassUtil {
/**
* 排除目录
*/
private static final List < String > EXCLUDE_DIRECTORY = Lists.newArrayList("advice", "config", "init",
"listener", "controller", "impl", "task");
/**
* 私有化构造器
*/
private AnalyzerUnusedClassUtil() {
}
/**
* 分析未使用类
*
* @author 管理员
* @date 2024/2/19
* @param args 启动参数
*/
@SuppressWarnings("checkstyle:UncommentedMain")
public static void main(String[] args) {
String scanPath = args[0];
if (CharSequenceUtil.isEmpty(scanPath)) {
System.out.println("路径为空,扫描结束");
return;
}
File projectDir = new File(scanPath);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 所有java文件及引用文件
List < String > fileNameList = listFileNames(projectDir);
List < File > fileList = FileUtil.loopFiles(projectDir);
Map < String, Integer > referencedMap = new ConcurrentHashMap <>();
fileNameList.parallelStream().forEach(fileName -> {
List < String > referencedClassesList = new ArrayList <>();
System.out.print("已检查" + referencedMap.size());
// 遍历项目目录下的所有Java文件
fileList.forEach(file -> {
// 如果该类已存在引用,则无须在排查是否存在其他引用
if (ObjUtil.isNotNull(referencedMap.get(fileName)) && referencedMap.get(fileName) > 0) {
// System.out.println(fileName + "已存在引用,检查下一个类");
return;
}
try {
JavaParser javaParser = new JavaParser();
ParseResult < CompilationUnit > parseResult = javaParser.parse(file);
// 文件解析成功
if (parseResult.isSuccessful()) {
CompilationUnit compilationUnit = parseResult.getResult()
.orElseThrow(() -> new IllegalStateException("Failed to parse file: " + file));
String stripFileName = CharSequenceUtil.strip(fileName, ".java");
ClassVisitor visitor = new ClassVisitor(stripFileName);
visitor.visit(compilationUnit, referencedClassesList);
// 如果最后类中的字段申明、方法中还是发现获取当前类所有导包则使用最后杀招循环所有导入
if (CollUtil.isEmpty(referencedClassesList)) {
NodeList < ImportDeclaration > importList = compilationUnit.getImports();
importList.forEach(importClass -> {
// 如果导包中存在该类则被使用【注意】:不考虑未清理的无效导包
if (CharSequenceUtil.contains(importClass.getNameAsString(), stripFileName)) {
referencedClassesList.add(stripFileName);
return;
}
});
}
referencedMap.put(fileName, referencedClassesList.size());
}
else {
// 打印失败文件
System.out.println("Parse file: " + file);
}
}
catch (FileNotFoundException exception) {
throw new RuntimeException(exception);
}
});
});
stopWatch.stop();
System.out.println();
System.out.println("获取所有任务的总花费时间(秒):" + stopWatch.getTotalTimeSeconds());
// 输出引用了目标类的其他类
Map < String, Integer > unusedMap = MapUtil.filter(referencedMap, entry -> entry.getValue() == 0);
List < String > unusedClassList = Lists.newArrayList(unusedMap.keySet());
System.out.println("Unused Classes:" + unusedClassList.size());
for (String unusedClass : unusedClassList) {
System.out.println(unusedClass);
}
}
/**
* 递归获取项目目录下的所有Java文件
*
* @author 管理员
* @date 2024/2/19
* @param directory 目录
* @return 所有Java文件
*/
private static List < String > listFileNames(File directory) {
List < String > javaFiles = new ArrayList <>();
File[] files = directory.listFiles();
// 目录下无对应文件直接返回
if (files == null) {
return javaFiles;
}
// 遍历文件或目录
for (File file : files) {
String fileName = file.getName();
// 如果是目录并且非排除目录(advice、config、init、listener等),递归获取对应文件
if (file.isDirectory() && !CollUtil.contains(EXCLUDE_DIRECTORY, fileName)) {
javaFiles.addAll(listFileNames(file));
}
else if (file.isFile() && CharSequenceUtil.endWith(fileName, "java")) {
// 添加对应java文件
javaFiles.add(file.getName());
}
}
return javaFiles;
}
/**
* 自定义Visitor,用于遍历AST并记录引用了目标类的其他类
*
* @author 管理员
* @date 2024/2/19
*/
private static class ClassVisitor extends VoidVisitorAdapter < List < String > > {
/**
* 类名
*/
private final String className;
/**
* 构造方法
*
* @param className 类名
*/
ClassVisitor(String className) {
this.className = className;
}
@Override
public void visit(ClassOrInterfaceDeclaration n, List < String > referencedClasses) {
// 检查当前类的成员
for (BodyDeclaration < ? > member : n.getMembers()) {
// 检查当前类中的字段申明和定义的变量如:private final CaptureExceptionService captureExceptionService;
if (member instanceof FieldDeclaration) {
FieldDeclaration field = (FieldDeclaration) member;
// 检查字段的类型
String fieldType = field.getCommonType().asString();
if (CharSequenceUtil.equals(fieldType, className)) {
referencedClasses.add(className);
}
else if (CharSequenceUtil.contains(fieldType, className)) {
// 字段申明和定义的变量可能包含泛型,如:private List< Class > xxxList
// 【缺失逻辑】:还需要判断referencedName是否被引用,如果该类未被引用则className也未被引用
String referencedName = n.getName().asString();
referencedClasses.add(className);
}
}
else if (member instanceof MethodDeclaration) {
MethodDeclaration method = (MethodDeclaration) member;
String returnType = method.getType().asString();
// 返回值匹配对应的类型也算引用【包含泛型】:PageSerializable < CollSimDTO >
if (CharSequenceUtil.equals(returnType, className)
|| CharSequenceUtil.contains(returnType, className)) {
referencedClasses.add(className);
}
// 检查方法的参数类型【包含泛型】:PageSerializable < CollSimDTO >
for (Parameter parameter : method.getParameters()) {
String paramType = parameter.getTypeAsString();
if (CharSequenceUtil.equals(paramType, className)
|| CharSequenceUtil.contains(paramType, className)) {
referencedClasses.add(className);
}
}
// 检查方法中的对象创建表达式
method.accept(new ObjectCreationVisitor(className), referencedClasses);
}
}
super.visit(n, referencedClasses);
}
}
/**
* 自定义Visitor,用于检查方法中的对象创建表达式
*
* @author 管理员
* @date 2024/2/19
*/
private static class ObjectCreationVisitor extends VoidVisitorAdapter < List < String > > {
/**
* 类名
*/
private final String className;
/**
* 构造方法
*
* @param className 类名
*/
ObjectCreationVisitor(String className) {
this.className = className;
}
@Override
public void visit(ObjectCreationExpr n, List < String > referencedClasses) {
String createdType = n.getType().asString();
if (CharSequenceUtil.equals(createdType, className)) {
referencedClasses.add(className);
}
super.visit(n, referencedClasses);
}
}
}
3.1.2、配置Maven插件
把以上编写源码打包成jar包上传私服供所有项目使用
<build>
<plugins>
<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.hssa.AnalyzerUnusedClassUtil</mainClass>
<arguments>
<argument>${basedir}/src/main/java/com/hssa/shrimp</argument>
</arguments>
</configuration>
<dependencies>
<dependency>
<groupId>com.hssa</groupId>
<artifactId>shrimp-paste</artifactId>
<version>0.0.1</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
【说明】shrimp-paste是自行打包在本地伺服中(好处:本地所有项目可使用),也可以把AnalyzerUnusedClassUtil
类放工程目录下,这样就不需要引入依赖,编译一下项目工程即可
3.1.3、运行插件
执行 mvn exec:java