背景
Java开发过程中经常有大量重复代码的组件需要编写,Ctrl + c/v 做法有点坑,毕竟这种代码没有手写的必要,举个例子,在使用Spring Data Jpa 时,需要为每个实体@Entity编写对应的@Repository组件,而这@Repository组件基本就不会经常变动,如果去手写没什么意思,就是Ctrl +c/v。还有如果在前后端分离项目中,需要编写数据传输组件,DTO(Data Transfer Obejct),那么一个DTO对象的所有字段基本都来自于对应的@Entity。如果我们可以编写一个解析Java源码的工具,预先设置生成模板,根据解析得到的数据自动生成@Reository和DTO组件,那不爽了。
基础
经过一番搜索和尝试,现在三种解析Java源码的方案:
(1)使用JDK自带的Java语法树遍历接口解析源码
(2)使用JDK自带的Javadoc接口解析源码
(3)使用第三方语法解析工具,例如antlr4,但需要去找合适的语法定义文件
经过考虑,方案三直接被弃用,尽量不使用第三方依赖,JDK有对应接口,那就用它吧!
使用Java语法树遍历接口
一、首先,自定义Visitor,访问者模式是源码解析中常用模式,可以百度了解一下
package io.github.okay6.demo.javac;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreeScanner;
/**
* @author Okay6
* 继承com.sun.source.util.TreeScanner,构建遍历抽象语法树的Visitor
* 重写部分方法 visitImport visitClass visitVariable 打印节点信息
*/
public class SourceVisitor extends TreeScanner<String, String> {
@Override
public String visitImport(ImportTree importTree, String s) {
System.out.println("Find Import");
System.out.println(importTree.getQualifiedIdentifier());
return super.visitImport(importTree, s);
}
@Override
public String visitClass(ClassTree classTree, String s) {
System.out.println("Find Class Definition");
System.out.println(classTree.getModifiers());
System.out.println(classTree.getSimpleName());
return super.visitClass(classTree, s);
}
@Override
public String visitVariable(VariableTree variableTree, String s) {
System.out.println("Find Variable Definition");
System.out.println(variableTree.getType());
System.out.println(variableTree.getName());
return super.visitVariable(variableTree, s);
}
}
二 、定义源码解析流程
package io.github.okay6.demo.javac;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.JavacTask;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.util.Context;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.nio.charset.Charset;
/**
* @author Okay6
* 实现Java源码解析流程构建
*/
class ParserByJavac {
private JavacFileManager fileManager;
private JavacTool javacTool;
ParserByJavac() {
Context context = new Context();
fileManager = new JavacFileManager(context, true, Charset.defaultCharset());
javacTool = JavacTool.create();
}
@SuppressWarnings("all")
void parseJava(String sourcePath) {
// Java源码文件解析为JavaFileObject
Iterable<? extends JavaFileObject> files = fileManager.getJavaFileObjects(sourcePath);
// 创建JavacTask
JavacTask javacTask = javacTool.getTask(null, fileManager, null, null, null, files);
try {
// 构建抽象语法树
Iterable<? extends CompilationUnitTree> result = javacTask.parse();
// 使用自定义Visitor遍历抽象语法树
for (CompilationUnitTree tree : result) {
tree.accept(new SourceVisitor(), null);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用Javadoc工具解析源码
package io.github.okay6.demo.javadoc;
import com.sun.javadoc.*;
/**
* @author Okay6
* 继承Docler,重写start languageVersion方法
*/
public class ParserByJavaDoc extends Doclet {
private static RootDoc rootDoc;
public static class Doclet {
private Doclet() {
}
@SuppressWarnings("all")
public static boolean start(RootDoc rootDoc) {
ParserByJavaDoc.rootDoc = rootDoc;
return true;
}
// 返回Java1.5 因为泛型是在Java1.5后加入的特性,获取变量类型时要获取泛型就需要重写此方法
@SuppressWarnings("all")
public static LanguageVersion languageVersion() {
return LanguageVersion.JAVA_1_5;
}
}
private static void parse() {
ClassDoc[] classes = rootDoc.classes();
ClassDoc classDoc = classes[0];
System.out.println("Find Class Definition");
System.out.println(classDoc.getRawCommentText());
System.out.println(classDoc.qualifiedName());
System.out.println("Find Imports");
for (PackageDoc packageDoc : classDoc.importedPackages()) {
System.out.println(packageDoc.name());
}
System.out.println("Find Field Definition");
for (FieldDoc fieldDoc : classDoc.fields(false)) {
System.out.println(fieldDoc.getRawCommentText());
System.out.println(fieldDoc.type());
System.out.println(fieldDoc.name());
}
}
public void parseJava(String sourceFile) {
String[] params = new String[]{"-doclet",
Doclet.class.getName(),
"-encoding", "utf-8", "-quiet", sourceFile};
com.sun.tools.javadoc.Main.execute(params);
parse();
}
}
后记
在使用过程中可以发现,使用Javadoc解析工具可以获取比遍历语法树更加详细的源码信息,包括注释,这样我们甚至可以把所需要的注释,例如@author 字段说明等同时提取出来,并在新生成的组件中使用。如果文中有错误之处,欢迎指出。源码:JavaParserDemo