共计 15930 个字符,预计需要花费 40 分钟才能阅读完成。
前言
如果有应用过 spring aop 性能的小伙伴,应该都会晓得 spring aop 次要是通过动静代理在运行时,对业务进行切面拦挡操作。明天咱们就来实现一下如何通过 APT+AST 在编译期时实现 AOP 性能。不过在此之前先科普一下 APT 和 AST 相干内容
APT(注解处理器)
apt 能够查看我之前写过的文章聊聊如何使用 JAVA 注解处理器(APT)
AST(形象语法树)
什么是 AST
形象语法树(Abstract Syntax Tree,AST),是源代码语法结构的一种形象示意。它以树状的模式体现编程语言的语法结构,树上的每个节点都示意源代码中的一种构造。比方包、类型、修饰符、运算符、接口、返回值都能够是一个语法结构。
示例:
package com.example.adams.astdemo;
public class TestClass {
int x = 0;
int y = 1;
public int testMethod(){
int z = x + y;
return z;
}
}
对应的形象语法树如下:
java 的编译过程
重点关注步骤一和步骤二生成 AST 的过程
步骤一:词法剖析,将源代码的字符流转变为 Token 列表。
通过词法分析器剖析源文件中的所有字符,将所有的单词或字符都转化成符合规范的 Token
规范化的 token 能够分成一下三种类型:
java 关键字 :public, static, final, String, int 等等;
自定义的名称 :包名,类名,办法名和变量名;
运算符或者逻辑运算符等符号:+、-、*、/、&&,|| 等等。
步骤二: 语法分析,依据 Token 流来结构树形表达式也就是 AST。
语法树的每一个节点都代表着程序代码中的一个语法结构,如类型、修饰符、运算符等。通过这个步骤后,编译器就根本不会再对源码文件进行操作了,后续的操作都建设在形象语法树之上。
AST 的利用场景
AST 定义了代码的构造,通过操作 AST,咱们能够精准地定位到申明语句、赋值语句、运算语句等,实现对源代码的剖析、优化、变更等操作。
注: AST 操作属于编译器级别,对程序运行齐全没有影响,效率绝对其余 AOP 更高
java 形象语法树罕用 API 类介绍
JCTree
JCTree 是语法树元素的基类,蕴含一个重要的字段 pos,该字段用于指明以后语法树节点(JCTree)在语法树中的地位,因而咱们不能间接用 new 关键字来创立语法树节点,即便创立了也没有意义。
重点介绍几个 JCTree 的子类:
1、JCStatement:申明语法树节点,常见的子类如下
- JCBlock:语句块语法树节点
- JCReturn:return 语句语法树节点
- JCClassDecl:类定义语法树节点
- JCVariableDecl:字段 / 变量定义语法树节点
2、JCMethodDecl:办法定义语法树节点
3、JCModifiers:拜访标记语法树节点
4、JCExpression:表达式语法树节点,常见的子类如下
- JCAssign:赋值语句
- JCAssignOp:+=
- JCIdent:标识符,能够是变量,类型,关键字等等
- JCLiteral: 字面量表达式,如 123,“string”等
- JCBinary:二元操作符
JCTrees 更多 API 的介绍能够查看如下链接
https://blog.csdn.net/u013998373/article/details/90050810
TreeMaker
TreeMaker 用于创立一系列的语法树节点,咱们下面说了创立 JCTree 不能间接应用 new 关键字来创立,所以 Java 为咱们提供了一个工具,就是 TreeMaker,它会在创立时为咱们创立的 JCTree 对象设置 pos 字段,所以必须应用上下文相干的 TreeMaker 对象来创立语法树节点。
着重介绍一下罕用的几个办法
TreeMaker.Modifiers
TreeMaker.Modifiers 办法用于创立拜访标记语法树节点(JCModifiers),源码如下
public JCModifiers Modifiers(long flags) {return Modifiers(flags, List.< JCAnnotation >nil());
}
public JCModifiers Modifiers(long flags,
List<JCAnnotation> annotations) {JCModifiers tree = new JCModifiers(flags, annotations);
boolean noFlags = (flags & (Flags.ModifierFlags | Flags.ANNOTATION)) == 0;
tree.pos = (noFlags && annotations.isEmpty()) ? Position.NOPOS : pos;
return tree;
}
- flags:拜访标记
- annotations:注解列表
其中 flags 能够应用枚举类 com.sun.tools.javac.code.Flags 来示意,例如咱们能够这样用,就生成了上面的拜访标记了。
示例:
创立拜访修饰符 public
treeMaker.Modifiers(Flags.PUBLIC);
TreeMaker.ClassDef
TreeMaker.ClassDef 用于创立类定义语法树节点(JCClassDecl), 源码如下:
public JCClassDecl ClassDef(JCModifiers mods,
Name name,
List<JCTypeParameter> typarams,
JCExpression extending,
List<JCExpression> implementing,
List<JCTree> defs) {
JCClassDecl tree = new JCClassDecl(mods,
name,
typarams,
extending,
implementing,
defs,
null);
tree.pos = pos;
return tree;
}
- mods:拜访标记,能够通过 TreeMaker.Modifiers 来创立
- name:类名
- typarams:泛型参数列表
- extending:父类
- implementing:实现的接口
- defs:类定义的具体语句,包含字段、办法的定义等等
TreeMaker.MethodDef
TreeMaker.MethodDef 用于创立办法定义语法树节点(JCMethodDecl),源码如下
public JCMethodDecl MethodDef(JCModifiers mods,
Name name,
JCExpression restype,
List<JCTypeParameter> typarams,
List<JCVariableDecl> params,
List<JCExpression> thrown,
JCBlock body,
JCExpression defaultValue) {
JCMethodDecl tree = new JCMethodDecl(mods,
name,
restype,
typarams,
params,
thrown,
body,
defaultValue,
null);
tree.pos = pos;
return tree;
}
public JCMethodDecl MethodDef(MethodSymbol m,
Type mtype,
JCBlock body) {return (JCMethodDecl)
new JCMethodDecl(Modifiers(m.flags(), Annotations(m.getAnnotationMirrors())),
m.name,
Type(mtype.getReturnType()),
TypeParams(mtype.getTypeArguments()),
Params(mtype.getParameterTypes(), m),
Types(mtype.getThrownTypes()),
body,
null,
m).setPos(pos).setType(mtype);
}
- mods:拜访标记
- name:办法名
- restype:返回类型
- typarams:泛型参数列表
- params:参数列表
- thrown:异样申明列表
- body:办法体
- defaultValue:默认办法(可能是 interface 中的哪个 default)
- m:办法符号
- mtype:办法类型。蕴含多种类型,泛型参数类型、办法参数类型、异样参数类型、返回参数类型。
注: 返回类型 restype 填写 null 或者 treeMaker.TypeIdent(TypeTag.VOID) 都代表返回 void 类型
示例
创立办法
public String getUserName(String userName){return userName;}
ListBuffer<JCTree.JCStatement> usernameStatement = new ListBuffer<>();
usernameStatement.append(treeMaker.Return(treeMaker.Ident(names.fromString("userName"))));
JCTree.JCBlock usernameBody = treeMaker.Block(0, usernameStatement .toList());
// 生成入参
JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), names.fromString("userName"),treeMaker.Ident(names.fromString("String")), null);
com.sun.tools.javac.util.List<JCTree.JCVariableDecl> parameters = com.sun.tools.javac.util.List.of(param);
JCTree.JCMethodDecl username = treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),
names.fromString("getUserName"), // 办法名
treeMaker.Ident(names.fromString("String")), // 返回类型
com.sun.tools.javac.util.List.nil(),
parameters, // 入参
com.sun.tools.javac.util.List.nil(),
usernameBody ,
null
);
TreeMaker.VarDef
TreeMaker.VarDef 用于创立字段 / 变量定义语法树节点(JCVariableDecl),源码如下
public JCVariableDecl VarDef(JCModifiers mods,
Name name,
JCExpression vartype,
JCExpression init) {JCVariableDecl tree = new JCVariableDecl(mods, name, vartype, init, null);
tree.pos = pos;
return tree;
}
public JCVariableDecl VarDef(VarSymbol v,
JCExpression init) {return (JCVariableDecl)
new JCVariableDecl(Modifiers(v.flags(), Annotations(v.getAnnotationMirrors())),
v.name,
Type(v.type),
init,
v).setPos(pos).setType(v.type);
}
- mods:拜访标记
- name:参数名称
- vartype:类型
- init:初始化语句
- v:变量符号
示例:
创立变量:private String username = “lyb-geek”;
treeMaker.VarDef(treeMaker.Modifiers(Flags.PRIVATE), names.fromString("useranme"), treeMaker.Ident(names.fromString("String"), treeMaker.Literal("lyb-geek");
TreeMaker.Ident
TreeMaker.Ident 用于创立标识符语法树节点(JCIdent)能够示意类、变量援用或者办法。源码如下
public JCIdent Ident(Name name) {JCIdent tree = new JCIdent(name, null);
tree.pos = pos;
return tree;
}
public JCIdent Ident(Symbol sym) {return (JCIdent)new JCIdent((sym.name != names.empty)
? sym.name
: sym.flatName(), sym)
.setPos(pos)
.setType(sym.type);
}
public JCExpression Ident(JCVariableDecl param) {return Ident(param.sym);
}
示例:
创立 username 的援用
treeMaker.Ident(names.fromString("username"))))
TreeMaker.Return
TreeMaker.Return 用于创立 return 语句(JCReturn),源码如下
public JCReturn Return(JCExpression expr) {JCReturn tree = new JCReturn(expr);
tree.pos = pos;
return tree;
}
示例
return this.username
treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")),names.fromString("useranme")));
TreeMaker.Select
TreeMaker.Select 用于创立域拜访 / 办法拜访(这里的办法拜访只是取到名字,办法的调用须要用 TreeMaker.Apply)语法树节点(JCFieldAccess),源码如下
public JCFieldAccess Select(JCExpression selected,
Name selector)
{JCFieldAccess tree = new JCFieldAccess(selected, selector, null);
tree.pos = pos;
return tree;
}
public JCExpression Select(JCExpression base,
Symbol sym) {return new JCFieldAccess(base, sym.name, sym).setPos(pos).setType(sym.type);
}
- selected:. 运算符右边的表达式
- selector:. 运算符左边的表达式
示例
获取办法 logDTO.setArgs()
treeMaker.Select(treeMaker.Ident(getNameFromString("logDTO")),
getNameFromString("setArgs")
TreeMaker.NewClass
TreeMaker.NewClass 用于创立 new 语句语法树节点(JCNewClass), 源码如下:
public JCNewClass NewClass(JCExpression encl,
List<JCExpression> typeargs,
JCExpression clazz,
List<JCExpression> args,
JCClassDecl def) {JCNewClass tree = new JCNewClass(encl, typeargs, clazz, args, def);
tree.pos = pos;
return tree;
}
- encl:不太明确此参数的含意,我看很多例子中此参数都设置为 null
- typeargs:参数类型列表
- clazz:待创建对象的类型
- args:参数列表
- def:类定义
示例:
创立 List args = new ArrayList();
JCTree.JCNewClass argsListclass = treeMaker.NewClass(null, null, memberAccess("java.util.ArrayList"), List.nil(), null);
JCTree.JCVariableDecl args = makeVarDef(treeMaker.Modifiers(0),
memberAccess("java.util.List"),
"args",
argsListclass
);
TreeMaker.Apply
TreeMaker.Apply 用于创立办法调用语法树节点(JCMethodInvocation),源码如下:
public JCMethodInvocation Apply(List<JCExpression> typeargs,
JCExpression fn,
List<JCExpression> args) {JCMethodInvocation tree = new JCMethodInvocation(typeargs, fn, args);
tree.pos = pos;
return tree;
}
- typeargs:参数类型列表
- fn:调用语句
- args:参数列表
TreeMaker.Assign
TreeMaker.Assign 用户创立赋值语句语法树节点(JCAssign),源码如下:
public JCAssign Assign(JCExpression lhs,
JCExpression rhs) {JCAssign tree = new JCAssign(lhs, rhs);
tree.pos = pos;
return tree;
}
- lhs:赋值语句右边表达式
- rhs:赋值语句左边表达式
示例
创立 username = “lyb-geek”
treeMaker.Assign(treeMaker.Ident(names.fromString("username")))), treeMaker.Literal("lyb-geek"))
TreeMaker.Exec
TreeMaker.Exec 用于创立可执行语句语法树节点(JCExpressionStatement),源码如下:
public JCExpressionStatement Exec(JCExpression expr) {JCExpressionStatement tree = new JCExpressionStatement(expr);
tree.pos = pos;
return tree;
}
注: TreeMaker.Apply 以及 TreeMaker.Assign 就须要外面包一层 TreeMaker.Exec 来取得一个 JCExpressionStatement
示例:
username =“lyb-geek”
treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(names.fromString("username")),treeMaker.Binary(JCTree.Tag.PLUS,treeMaker.Literal("lyb"),treeMaker.Literal("-geek"))))
TreeMaker.Block
TreeMaker.Block 用于创立组合语句的语法树节点(JCBlock),源码如下:
public JCBlock Block(long flags,
List<JCStatement> stats) {JCBlock tree = new JCBlock(flags, stats);
tree.pos = pos;
return tree;
}
- flags:拜访标记
- stats:语句列表
示例
创立代码块
List<JCTree.JCStatement> jcStatementList = List.nil();
treeMaker.Block(0, jcStatementList);
TreeMaker 更多具体 API 能够查看如下链接
http://www.docjar.com/docs/api/com/sun/tools/javac/tree/TreeMaker.html
Names
Names 封装了操作标识符的办法, 类、办法、参数的名称都能够通过 names 来获取
大家如果对 AST 感兴趣,能够通过 https://astexplorer.net/ 在线体验一下
实战
示例次要通过 APT+AST 实现一个统计办法调用耗时以及记录日志的性能
注: 大家能够通过 JavaParserJavaParser 来简化对 AST 的操作。
本示例通过 jdk 自带的 tools.jar 工具类进行操作
1、在 pom 引入 tools.jar gav
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
2、自定义注解 CostTimeRecoder
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface CostTimeRecoder {
}
3、编写注解处理器
@AutoService(Processor.class)
@SupportedOptions("debug")
public class CostTimeRecordProcessor extends AbstractComponentProcessor {
/**
* 元素辅助类
*/
private Elements elementUtils;
/**
* 日志输入工具类
*/
private Messager meessager;
/**
* 形象语法树
*/
private JavacTrees trees;
/**
* 封装了创立或者批改 AST 节点的一些办法
*/
private TreeMaker treeMaker;
/**
* 封装了操作标识符的办法
*/
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
meessager = processingEnv.getMessager();
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment)processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public Set<String> getSupportedAnnotationTypes() {return Collections.singleton(CostTimeRecoder.class.getName());
}
@Override
protected boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {if (annotations == null || annotations.isEmpty()) {return false;}
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(CostTimeRecoder.class);
if (elements == null || elements.isEmpty()){return false;}
if (!roundEnv.processingOver()) {elements.stream() .filter(element -> element instanceof ExecutableElement)
.map(element -> (ExecutableElement) element)
.forEach(method -> {TypeElement typeElement = (TypeElement)method.getEnclosingElement();
JCTree.JCClassDecl tree = trees.getTree(typeElement);
JCTree.JCMethodDecl methodDecl = trees.getTree(method);
CostTimeRecordAstTranslator costTimeRecordAstTranslator = new CostTimeRecordAstTranslator(treeMaker,names,meessager,tree,methodDecl);
costTimeRecordAstTranslator.setTrees(trees);
// 导入援用类, 如果不配置 import,则办法调用,需配置全类门路,// 比方 LogFactory.getLogger(), 如果没导入 LogFactory,则办法需写成 com.github.lybgeek.log.factory.LogFactory.getLogger
// 配置后,仅需写成 LogFactory.getLogger 即可
costTimeRecordAstTranslator.addImportInfo(typeElement, LogFactory.class.getPackage().getName(),LogFactory.class.getSimpleName());
costTimeRecordAstTranslator.addImportInfo(typeElement,LogDTO.class.getPackage().getName(),LogDTO.class.getSimpleName());
// costTimeRecordAstTranslator.addImportInfo(typeElement, LogService.class.getPackage().getName(),LogService.class.getSimpleName());
tree.accept(costTimeRecordAstTranslator);
});
}
return false;
}
private String getPackageName(TypeElement typeElement) {return elementUtils.getPackageOf(typeElement).getQualifiedName()
.toString();}
}
3、编写 AST TreeTranslator
注:省略业务的 TreeTranslator,就列出基类,可能对大家比拟有用,须要业务的实现办法,间接见下方 demo 链接
public abstract class AbstractTreeTranslator extends TreeTranslator {
/**
* 封装了创立或者批改 AST 节点的一些办法
*/
protected TreeMaker treeMaker;
/**
* 封装了操作标识符的办法
*/
protected Names names;
/**
* 日志输入工具类
*/
protected Messager meessager;
/**
* 形象语法树
*/
private JavacTrees trees;
public AbstractTreeTranslator(TreeMaker treeMaker, Names names, Messager meessager) {
this.treeMaker = treeMaker;
this.names = names;
this.meessager = meessager;
}
/**
* 依据字符串获取 Name
* @param s
* @return
*/
public Name getNameFromString(String s) {return names.fromString(s); }
/**
* 创立变量语句
* @param modifiers 拜访修饰符
* @param name 参数名称
* @param varType 参数类型
* @param init 初始化赋值语句
* 示例
* JCTree.JCVariableDecl var = makeVarDef(treeMaker.Modifiers(0), "xiao", memberAccess("java.lang.String"), treeMaker.Literal("methodName"));
* 生成语句为:String xiao = "methodName";
* @return
*/
public JCTree.JCVariableDecl makeVarDef(JCTree.JCModifiers modifiers, JCTree.JCExpression varType,String name, JCTree.JCExpression init) {
return treeMaker.VarDef(
modifiers,
getNameFromString(name),
varType,
init
);
}
/**
* 创立 域 / 办法 的多级拜访, 办法的标识只能是最初一个
* @param components 比方 java.lang.System.out.println
* @return
*/
public JCTree.JCExpression memberAccess(String components) {String[] componentArray = components.split("\\.");
JCTree.JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0]));
for (int i = 1; i < componentArray.length; i++) {expr = treeMaker.Select(expr, getNameFromString(componentArray[i]));
}
return expr;
}
/**
* 给变量赋值
* @param lhs
* @param rhs
* @return
* 示例:makeAssignment(treeMaker.Ident(getNameFromString("xiao")), treeMaker.Literal("assignment test"));
* 生成的赋值语句为:xiao = "assignment test";
*/
public JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
return treeMaker.Exec(
treeMaker.Assign(
lhs,
rhs
)
);
}
/**
* 导入办法依赖的 package 包
* @param packageName
* @param className
* @return
*/
public JCTree.JCImport buildImport(String packageName, String className) {JCTree.JCIdent ident = treeMaker.Ident(names.fromString(packageName));
JCTree.JCImport jcImport = treeMaker.Import(treeMaker.Select(ident, names.fromString(className)), false);
meessager.printMessage(Diagnostic.Kind.NOTE,jcImport.toString());
return jcImport;
}
/**
* 导入办法依赖的 package 包
* @param element class
* @param packageName
* @param className
* @return
*/
public void addImportInfo(TypeElement element, String packageName, String className) {TreePath treePath = getTrees().getPath(element);
Tree leaf = treePath.getLeaf();
if (treePath.getCompilationUnit() instanceof JCTree.JCCompilationUnit && leaf instanceof JCTree) {JCTree.JCCompilationUnit jccu = (JCTree.JCCompilationUnit) treePath.getCompilationUnit();
for (JCTree jcTree : jccu.getImports()) {if (jcTree != null && jcTree instanceof JCTree.JCImport) {JCTree.JCImport jcImport = (JCTree.JCImport) jcTree;
if (jcImport.qualid != null && jcImport.qualid instanceof JCTree.JCFieldAccess) {JCTree.JCFieldAccess jcFieldAccess = (JCTree.JCFieldAccess) jcImport.qualid;
try {if (packageName.equals(jcFieldAccess.selected.toString()) && className.equals(jcFieldAccess.name.toString())) {return;}
} catch (NullPointerException e) {e.printStackTrace();
}
}
}
}
java.util.List<JCTree> trees = new ArrayList<>();
trees.addAll(jccu.defs);
JCTree.JCImport jcImport = buildImport(packageName,className);
if (!trees.contains(jcImport)) {trees.add(0, jcImport);
}
jccu.defs = List.from(trees);
}
}
public JavacTrees getTrees() {return trees;}
public void setTrees(JavacTrees trees) {this.trees = trees;}
}
4、测试
编写测试类
public class HelloService {
@CostTimeRecoder
public String sayHello(String username){
try {TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {e.printStackTrace();
}
return "hello :" + username;
}
}
测试主类
public class AptAstMainTest {public static void main(String[] args) {System.out.println(new HelloService().sayHello("zhangsan"));
}
}
运行查看控制台
会发现多了耗时,以及日志打印。咱们查看 HelloService .class 文件,会发现多了如下内容
public class HelloService {public HelloService() { }
public String sayHello(String username) {Long startTime = System.currentTimeMillis();
try {TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException var7) {var7.printStackTrace();
}
Long endTime = System.currentTimeMillis();
Long costTime = endTime - startTime;
String msg = String.format("costTime = %s(ms)", costTime);
System.out.println(msg);
List args = new ArrayList();
args.add(username);
this.saveLog(costTime, args);
return "hello :" + username;
}
private void saveLog(Long costTime, List args) {LogDTO logDTO = new LogDTO();
logDTO.setMethodName("sayHello");
logDTO.setClassName("com.github.lybgeek.test.service.HelloService");
logDTO.setCostTime(costTime);
logDTO.setArgs(args);
LogService logService = LogFactory.getLogger();
logService.save(logDTO);
}
}
总结
本文次要重点介绍 AST 的用法,对 AOP 的实现基本上是一笔带过。起因次要是平时除非是对性能有特地要求,咱们实现 AOP 通常会在运行期实现,而非在编译期实现。其次 AST 比拟偏底层,如果出问题,排查难度会比拟高。当然如果团队有对 AST 很相熟的话,能兼顾性能是最好的。
demo 链接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-apt-ast
参考链接
https://my.oschina.net/u/4030990/blog/3211858
https://blog.csdn.net/a_zhenzhen/article/details/86065063
https://www.jianshu.com/p/ff8ec920f5b9