java注解处理器学习在编译期修改语法树教程
作者:烦啦
从需求说起
由于相关政策,最近公司安全部要求各系统在rpc接口调用的交互过程中把相应的参数及结果以相应的格式发送到安全部统一记录,例如参数或结果含手机号和邮箱则格式如:“mail:axxx@126.com,phone:183xxxx1967”,其它系统信息等先忽略。
以便在数据泄露时可据此分析出数据的泄露源头,以及若有黑客攻克有些接口时公司能有迹可循。
总体架构是各个接口把入参和结果打印日志,然后由统一的日志收集器收集日志通过mq发送到安全部。这样每个系统只用在接口中添加参数和结果的打日志代码。
添加打印日志代码的方案
第一种方案,硬编码
即直接在接口中编写打印日志的代码。这种工作量太大,公司各个部门,以往积累了众多的项目,这样改造的工作量太大。
第二种方案,AOP
利用aop框架,在切面类中打印日志。可以使用spring 支持的aop功能或其他aop框架。
这个方案应该来说改动及工作量都大大降低,公司也是采用的这种方案。但是其弊端也很明显,
一、是对框架的依赖(如用spring aop的话则非spring项目则不适用)
二、就是不同的项目或接口,入参或结果变量名不同,如手机号:有的叫mobilePhone, 有的叫telephone等;但打印日志时要统一打印,如:phone:183xxxx1967; 所以要在参数上加注解,以表明打印日志时的名称。这个重复工作量也不小。
第三种方案,修改class文件
针对第二种方案的弊端,我设计出这第三种方案。
利用相关技术,直接修改class文件,在接口中添加打印日志的字节码。例如Javassist,asm等技术。
通过调研,在编译期通过修改语法树来达到修改class文件的效果,这种对用户来说完全透明,不依赖任何框架。针对弊端二则发明名称分析模块,让程序自动分析出参数的含义,从而避免了手工添加注解的麻烦。
下面就具体说明第三种方案的实现:
利用JDK的注解处理器,可在编译期间对注解处理,可以读取、修改、添加抽象语法树中的任意元素。
注解处理器是JDK1.6开始提供的功能,利用注解处理器可以干涉编译器的行为,只要有足够的创意,可以利用注解处理器实现许多原本只能在编码中完成的事情。
注解处理器的用法:
1、实现AbstractProcessor
实现init和process方法
顾名思义,init是完成一些初始化工作;process完成具体的逻辑处理。后边会有具体的例子说明。
2、添加注解
@SupportedAnnotationTypes 指定此注解处理器支持的注解,可用*指定所有注解
@SupportedSourceVersion 指定支持的java的版本
注解实例:
在process方法中可获取到注解有@Safety的类和方法。
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Safety.class);
然后可遍历方法,获取到方法的入参,分析入参,给方法的语法树添加打印参数日志的代码。对于方法的结果是同样的道理。
关于对语法树的操作,网上资料相对较少及较为片段,介绍起来篇幅略长,故放在最后面进行介绍。
名称分析模块的思想及设计
剩下的一个关键问题是如何把不同的参数名统一成打印日志时的名称,例如参数名为mobilePhone或telephone,但要打印的是phone。如果在注解属性中指定的话,通过注解可以获取到,但是当接口或参数很多的情况下也是一件重复性的力气活。
故我设计出一种不让开发人员手动指定名称的方案,既对老的项目修改的少,又减轻开发人员的工作量,对新项目的应用也是高效率的。
如图:
词库存储(可用类的静态字段存储)需要打印的日志名称及其对应的词根及单词。如:
上图红框为打印日志时要打印的名称。绿框中为词根及单词:如若业务参数有用postbox作为邮箱变量名的则也可把postbox加入到mail的词库中。
这样当业务参数为mobilePhone或telephone时,名称分析模块能够分析出参数名包含phone词根,从而得到对应的打印日志名“phone”;这就要求业务参数的名义要有具体的含义,不能随便字母组合没有含义的词语,这应该也是每个公司开发时的基本要求。
这里只是举例一个简单的可行性方案,名称分析模块也可利用AI技术,根据输入的变量名利用智能技术分析出此变量名的含义。
语法树的操作:
下面对语法树的操作进行详细的说明,这里需要提到三个类:
JavacTrees 提供了待处理的抽象语法树TreeMaker 封装了创建AST节点的一些方法Names 提供了创建标识符的方法
可在init方法中对这三个类初始化,以便在process方法中利用它们对语法树进行操作。如图:
AST(抽象语法树)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值,甚至代码注释等都可以是一个语法结构。
JCTree的介绍
JCTree是语法树元素的基类。
如上图,它包含两个属性,
字段type表示语法结构的类型
字段pos用于指明当前语法树节点(JCTree)在语法树中的位置,因此我们不能直接用new关键字来创建语法树节点,即使创建了也没有意义,而要用TreeMaker来进行操作。
重点介绍几个JCTree的子类:
JCStatement:声明语法树节点,常见的子类如下 JCBlock:语句块JCReturn:return语句JCClassDecl:类定义JCVariableDecl:字段/变量定义JCIf: if语句
2.JCMethodDecl:方法定义语法树节点
3.JCModifiers:访问标志语法树节点
4.JCExpression:表达式语法树节点,常见的子类如下
JCAssign:赋值语句JCAssignOp:+=JCIdent:标识符,可以是变量,类型,关键字等等JCLiteral: 字面量表达式,如123, “string”等JCBinary:二元操作符
JCTree的子类很多,大部分可以从字面上看出其意义
如上图,在jdk1.8.0_65里JCTree有58个子类。
下面具体介绍对语法树的操作。
TreeMaker
TreeMaker创建语法树节点的所有方法,创建时会为创建出来的JCTree设置pos字段,所以必须用上下文相关的TreeMaker对象来创建语法树节点,而不能直接new语法树节点。
TreeMaker.Modifiers
该方法用于创建访问标志语法树节点(JCModifiers),源码如下:
public JCModifiers Modifiers(long flags, List<JCAnnotation> annotations) { JCModifiers tree = new JCModifiers(flags, annotations); boolean noFlags = (flags & Flags.StandardFlags) == 0; tree.pos = (noFlags && annotations.isEmpty()) ? Position.NOPOS : pos; return tree; } public JCModifiers Modifiers(long flags) { return Modifiers(flags, List.<JCAnnotation>nil()); }
参数解释:
flags:访问标志
annotations:注解列表
其中flags可以用枚举类型com.sun.tools.javac.code.Flags,且支持相加(Flags的值按二进制设计),如图:
用法示例:treeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC + Flags.FINAL);
TreeMaker.ClassDef
该方法用于创建类定义语法树节点(JCClassDef),源码如下:
public JCClassDecl ClassDef(JCModifiers mods, Name name, List<JCTypeParameter> typarams, JCTree extending, List<JCExpression> implementing, List<JCTree> defs) { JCClassDecl tree = new JCClassDecl(mods, name, typarams, extending, implementing, defs, null); tree.pos = pos; return tree; }
参数解释:
mods:访问标志
name:方法名
restype:返回类型
typarams:泛型参数列表
params:参数列表
thrown:异常声明列表
body:方法体
defaultValue:默认方法(可能是interface中的那个default)
m:方法符号
mtype:方法类型。包含多种类型,泛型参数类型、方法参数类型,异常参数类型、返回参数类型
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; }
参数解释:
mods:访问标志
name:方法名
restype:返回类型
typarams:泛型参数列表
params:参数列表
thrown:异常声明列表
body:方法体
defaultValue:默认方法(可能是interface中的那个default)
m:方法符号
mtype:方法类型。包含多种类型,泛型参数类型、方法参数类型,异常参数类型、返回参数类型
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; }
参数解释:
mods:访问标志
vartype:类型
init:初始化语句
v:变量符号
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); }
TreeMaker.Return
用于创建return语句语法树节点(JCReturn)
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:不太明白此参数含义
typeargs:参数类型列表
clazz:待创建对象的类型
args:参数列表
def:类定义
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:.运算符右边的名字
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
用于创建赋值语句语法树节点(JCAssign),源码如下:
public JCAssign Assign(JCExpression lhs, JCExpression rhs) { JCAssign tree = new JCAssign(lhs, rhs); tree.pos = pos; return tree; }
参数解释:
lhs:赋值语句左边表达式
rhs:赋值语句右边表达式
TreeMaker.Exec
用于创建可执行语句语法树节点(JCExpressionStatement),源码如下:
public JCExpressionStatement Exec(JCExpression expr) { JCExpressionStatement tree = new JCExpressionStatement(expr); tree.pos = pos; return tree; }
TreeMaker.Block
用于创建组合语句语法树节点(JCBlock),源码如下:
public JCBlock Block(long flags, List<JCStatement> stats) { JCBlock tree = new JCBlock(flags, stats); tree.pos = pos; return tree; }
参数解释:
flags:访问标志
stats:语句列表
用法实例:
下面来介绍一下实例来加深对API用法的理解:
1、根据字符串获取Name,(利用Names的fromString静态方法)
private Name getNameFromString(String s) { return names.fromString(s); }
2、创建变量语句
private JCTree.JCVariableDecl makeVarDef(JCTree.JCModifiers modifiers, String name, JCTree.JCExpression vartype, JCTree.JCExpression init) { return treeMaker.VarDef( modifiers, getNameFromString(name), //名字 vartype, //类型 init //初始化语句 ); }
3、创建 域/方法 的多级访问, 方法的标识只能是最后一个
例如: java.lang.System.out.println
private 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; }
4、声明变量并赋值(利用以上包装的方法)
JCTree.JCVariableDecl var = makeVarDef(treeMaker.Modifiers(0), "xiao", memberAccess("java.lang.String"), treeMaker.Literal("methodName"));
生成语句为:String xiao = "methodName";
5、给变量赋值
private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) { return treeMaker.Exec( treeMaker.Assign( lhs, rhs ) ); } makeAssignment(treeMaker.Ident(getNameFromString("xiao")), treeMaker.Literal("assignment test"));
生成的赋值语句为:xiao = "assignment test";
6、两个字符串字面量相加并赋值
treeMaker.Exec( treeMaker.Assign(treeMaker.Ident(getNameFromString("xiao")), treeMaker.Binary( JCTree.Tag.PLUS, treeMaker.Literal("-Binary operator one"), treeMaker.Literal("-Binary operator two") )) );
生成语句为:xiao = "-Binary operator one" + "-Binary operator two";
编译器会对此语句进行优化,因为两个字面量在编译器即能确定,所以编译器会把此语句优化为:“xiao = "-Binary operator one-Binary operator two";”
7、+=语句
treeMaker.Exec( treeMaker.Assignop( JCTree.Tag.PLUS_ASG, treeMaker.Ident(getNameFromString("xiao")), treeMaker.Literal("-Assignop test") ) );
生成语句为:xiao += "-Assignop test";
8、声明整型变量并赋值
makeVarDef(treeMaker.Modifiers(0), "zhen", memberAccess("java.lang.Integer"), treeMaker.Literal(1));
生成语句为:Integer zhen = 1;
9、++语句
treeMaker.Exec( treeMaker.Unary( JCTree.Tag.PREINC, treeMaker.Ident(getNameFromString("zhen")) ) );
生成语句:zhen++;
10、加法语句
treeMaker.Exec( treeMaker.Unary( JCTree.Tag.PREINC, treeMaker.Ident(getNameFromString("zhen")) ) );
生成语句:zhen = zhen + 10;
11、方法调用(以输出语句举例)
treeMaker.Exec( treeMaker.Assign( treeMaker.Ident(getNameFromString("zhen")), treeMaker.Binary( JCTree.Tag.PLUS, treeMaker.Ident(getNameFromString("zhen")), treeMaker.Literal(10) )) );
生成语句:System.out.println(xiao);
12、方法调用,输出字符串
JCTree.JCExpressionStatement printVar = treeMaker.Exec(treeMaker.Apply( List.of(memberAccess("java.lang.String")),//参数类型 memberAccess("java.lang.System.out.println"), List.of(treeMaker.Ident(getNameFromString("xiao"))) ) );
生成语句:System.out.println("xiao test zhen");
13、if语句
treeMaker.If( treeMaker.Binary( JCTree.Tag.LT, treeMaker.Ident(getNameFromString("zhen")), treeMaker.Literal(10) ), printVar, printLiteral );
生成语句:
if (zhen < 10) {
System.out.println(xiao);
} else {
System.out.println("xiao test zhen");
}
14、if语句(null判断)
treeMaker.If( treeMaker.Parens( treeMaker.Binary( JCTree.Tag.NE, treeMaker.Ident(getNameFromString("xiao")), treeMaker.Literal(TypeTag.BOT, null)) ), printVar, printLiteral )
生成语句:
if (xiao != null) {
System.out.println(xiao);
} else {
System.out.println("xiao test zhen");
}
以上列出了一下常用的语句的语法树操作方法,希望对理解操作语法树有帮助。笔者也正在研究中,对编译原理了解的话,学起来也就容易多了。
推荐几本书,看完的话定定会受益匪浅:《编译原理(高清龙书中文版)》、《两周自制脚本语言》、《现代编译器的Java实现(第二版)》。
更多关于java编译期修改语法树的资料请关注脚本之家其它相关文章!