前言

如果有应用过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;}
  1. flags:拜访标记
  2. 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;}
  1. mods:拜访标记,能够通过 TreeMaker.Modifiers 来创立
  2. name:类名
  3. typarams:泛型参数列表
  4. extending:父类
  5. implementing:实现的接口
  6. 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);}
  1. mods:拜访标记
  2. name:办法名
  3. restype:返回类型
  4. typarams:泛型参数列表
  5. params:参数列表
  6. thrown:异样申明列表
  7. body:办法体
  8. defaultValue:默认办法(可能是 interface 中的哪个 default)
  9. m:办法符号
  10. 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);}
  1. mods:拜访标记
  2. name:参数名称
  3. vartype:类型
  4. init:初始化语句
  5. 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);}
  1. selected:. 运算符右边的表达式
  2. 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;}
  1. encl:不太明确此参数的含意,我看很多例子中此参数都设置为 null
  2. typeargs:参数类型列表
  3. clazz:待创建对象的类型
  4. args:参数列表
  5. 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;}
  1. typeargs:参数类型列表
  2. fn:调用语句
  3. args:参数列表

TreeMaker.Assign
TreeMaker.Assign 用户创立赋值语句语法树节点(JCAssign),源码如下:

public JCAssign Assign(JCExpression lhs,    JCExpression rhs) {        JCAssign tree = new JCAssign(lhs, rhs);        tree.pos = pos;        return tree;}
  1. lhs:赋值语句右边表达式
  2. 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;}
  1. flags:拜访标记
  2. 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)@Documentedpublic @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