使用最短路径算法检查项目循环依赖
最近项目组让我做一个自研的小工具,用来检查代码里的循环依赖,这里做下记录。
思路
由于工作是网络算路的,第一个想法就是通过路径计算来实现这个功能:把项目里test,resource等文件夹排除,剩下的每一个java文件可以算是对应一个类,把每个类看做是网络/路网里的节点,把类与类之间的依赖关系具象成网络里的链路或者路网里的道路,再把网络节点和链路抽象成图,这样,就可以把查找循环依赖变成查找图里每个节点两两之间是否有路径可达,如果两两之间都有路径可达,那么这两个节点代表的类就存在互相依赖。
1. 遍历项目文件
第一步,先将项目里所有java文件找出来,这里就用正常的迭代遍历文件的方法:
public static List<File> getAllJavaClassFile(String rootPath) throws IOException {
File project = new File(rootPath);
return getJavaFiles(project);
}
private static List<File> getJavaFiles(File parentFile) throws IOException {
List<File> javaFiles = new ArrayList<>();
if (parentFile == null || ConfigConstant.EXCLUDE_SCAN_DIR.contains(parentFile.getName())) {
return Lists.newArrayList();
}
File[] subFiles = parentFile.listFiles();
if (subFiles == null || subFiles.length == 0) return Lists.newArrayList();
for (File subFile : subFiles) {
BasicFileAttributes basicFileAttributes = Files.readAttributes(subFile.toPath(), BasicFileAttributes.class);
if (basicFileAttributes.isDirectory()) {
javaFiles.addAll(getJavaFiles(subFile));
} else if (basicFileAttributes.isRegularFile()) {
String mimeType = Files.probeContentType(subFile.toPath());
if (Objects.equals(ConfigConstant.JAVA_TYPE, mimeType)) {
javaFiles.add(subFile);
}
}
}
return javaFiles;
}
2. 构造节点和依赖关系
这里需要做的操作如下:
- 按行读取Java文件,将第一行“package ...”读出来,将类和每一层父级包依次构造成节点(父级包也需要构造出来,有可能不是类与类的循环依赖,而是存在包与包之间的循环依赖);
- 将java文件中每一行"import ..."读出来,构造成节点,即此类存在依赖的类,同时构造成依赖关系。
节点结构:
@Getter
@Setter
public class ProjectItem {
Integer id;
String name;
String fullName;
Integer parent;
}
依赖关系结构:
@Getter
@Setter
public class DependNode {
/**
* 唯一标识,和ProjectNode里id一一对应
*/
Integer id;
/**
* 标识是否是类
*/
boolean isClass;
/**
* 当前节点依赖的节点
*/
List<Integer> dependIds = new ArrayList<>();
/**
* 记录最下层类关系
*/
List<Pair<Integer, Integer>> dependChildIds = new ArrayList<>();
}
转化实现:
protected void convertFile2Item(File file, BiConsumer<String, ProjectItem> consumer) {
ConfigParam config = ConfigParam.getIns();
// 一行行读取,根据package和import导入语句确定依赖关系
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String lineCode;
ProjectItem classItem = null;
while ((lineCode = br.readLine()) != null) {
if (!lineCode.contains(config.getRootItem()) || config.getExclude().stream().anyMatch(lineCode::contains)) {
// 这里过滤下不需要的文件,例如.git目录,test目录,resource目录以及自定义的排除目录,提供效率
continue;
}
if (lineCode.startsWith(ContextConstant.JAVA_PACKAGE_PREFIX)) {
// 这里根据package语句可以自上而下把包和自身类的node节点创建出来
classItem = createProjectItems(lineCode, file.getName().substring(0, file.getName().length() - 5));
} else if (lineCode.startsWith(ContextConstant.JAVA_IMPORT_PREFIX)) {
// 这里根据import语句创建依赖的节点和包node,全量和增量进行不同处理
consumer.accept(lineCode, classItem);
}
// 读到具体的代码了,退出
if (lineCode.contains(ContextConstant.JAVA_CLASS_START)) break;
}
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
注:这里import语句的解析因为项目组实际要求,需要能够读取项目全量文件和增量文件两种方式,所以我通过函数式接口的方式分开实现的。
3. 计算
这里就对抽象好的图进行计算了,由于需要对每个节点进行计算,所以使用更适合矩阵运算的Floyd算法,而不是效率更高的迪杰斯特拉算法(详见Floyd算法),具体步骤如下:
- 构建矩阵;
- Floyd计算;
- 对计算出来的矩阵进行过滤,将两两之间存在路径的节点找到,得到类和依赖路径;
- 向上浮动一层进行计算(例如第一次计算的都是类的循环依赖,然后开始算类的上层包之间的循环依赖,再算更上层的包的循环依赖,直至到根目录);
- 导出结果。
实现代码:
public void cycleCalc() {
List<PathResult> results = new ArrayList<>();
// 从类开始寻找循环依赖,找完之后整个往上一层,到包继续寻找循环依赖,直到不存在有依赖关系的包或者向上找到了根节点
while (!curDependNodes.isEmpty() && !curDependNodes.keySet().stream().allMatch(index ->
Objects.equals(-1, allItems.get(index).getParent()))) {
// 构建矩阵
MatrixNode[][] matrix = buildMatrix();
// 计算矩阵
floyd(matrix);
// 解析矩阵
results.addAll(analyseMatrix(matrix));
// 矩阵节点向上层迭代
upIterate();
}
ExportResult.exportJson(results, allItems);
}
注:这里就不展示每一步的具体实现了,仅提供思路。