在开发Android项目中,经常会用到AbstractProcessor来构建注解处理器。在编译的时候生成代码,从而减少工作量。使用非常广泛的butterknife就是如此,但是最新版的github貌似是使用gradle插件来尽心生成,不过原理应该都类似,编译的时候通过对应的注解生成一些辅助类,或者主动注入一些方法。Groovy的AST就是类似的用法,由于Groovy的动态性,在AST中能做的事情就更多了。
一、AST的简单使用
在intellij中新建一个gradle项目,然后建立两个文件。
ASTtest.groovy
class ASTTextContent {
def a
def fun(){
println 'str'
}
}
println()
TestASTTransformation.groovy
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
/*
* Created by TY on 2018/4/20.
*/
@GroovyASTTransformation
class TestASTTransformation implements ASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
nodes.each {
println it
}
println source.AST
println source.source.reader.text
}
}
接着新建一个META-INF/services目录,在目录下新建一个org.codehaus.groovy.transform.ASTTransformation文件,并在文件中添加下面一行:
TestASTTransformation
最终目录如图:
其中classes,以及resource两个目录可以不管,这是为了方便打包jar建立的目录。按照我之前的建立方式可以不需要额外打包jar来进行测试。
接下来我们运行ASTtest.groovy,就会得到结果:
org.codehaus.groovy.ast.ModuleNode@709ba3fb
org.codehaus.groovy.ast.ModuleNode@709ba3fb
/*
* Created by TY on 2018/4/20.
*/
class ASTTextContent {
def a
def fun(){
println 'str'
}
}
println()
分析一下,node和AST是同一个对象,ModuleNode,source直接就把源码读取出来了。至于ModuleNode是什么东西,我们先看一看ASTNode的继承关系。
既然是编译时候的处理器,自然能获取到什么类的节点,方法节点,成员变量节点之类的。具体的我也没有太仔细查看,常用的也不多。至于ModuleNode通过查看注释,基本就可以理解为script了。Groovy直接执行代码的时候产生的module。
修改一下TestASTTransformation.groovy的代码
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.ConstructorNode
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.GroovyClassVisitor
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.PropertyNode
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
/*
* Created by TY on 2018/4/20.
*/
@GroovyASTTransformation
class TestASTTransformation implements ASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
source.AST.classes.each {
it.visitContents(new GroovyClassVisitor() {
@Override
void visitClass(ClassNode node) {
println 'visit class'
}
@Override
void visitConstructor(ConstructorNode node) {
println node.name
}
@Override
void visitMethod(MethodNode node) {
println node.name
}
@Override
void visitField(FieldNode node) {
println node.name
}
@Override
void visitProperty(PropertyNode node) {
println node.name
}
})
}
}
}
这里的source.AST.classes有两个类,一个是脚本script的子类ASTtest另一个就是ASTTextContent。然而戳进源码发现visitClass方法根本没有调用,其实也对,it就是classNode了,调不调用也无所谓了。结果:
<init>
<init>
main
run
a
a
fun
二、使用AST修改方法
首先我们先运行一下groovy的consolo,来分析ast的结构。
运行win10的powershell,当然cmd也行,执行
groovyconsole
出现以下界面
然后我们就可以在窗口中分析类的ast结构。然后我们新建一个module,并创建ASTtest.groovy文件。
class ASTTextContent {
def a
def fun(){
println 'str'
}
}
new ASTTextContent().fun()
我们将代码粘贴到groovyconsole窗口中,按下ctrl+T ,就可以分析结构了。
图中的BlockStatement可以理解为语句块,也就是一行代码吧。分析一下得知我们只需要在BlockStatement 加入自己的statement就行了。
接下来创建我们的AST,注意下面使用了两种方法来添加方法的语句块。
InjectAST.groovy
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
/**
* Created by Administrator on 2017/6/23 0023.
*/
@GroovyASTTransformation
class InjectAST implements ASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
source.AST.classes.find{
it.name.contains("ASTTextContent")
}?.methods?.find{
it.name=='fun'
}?.with {
BlockStatement block = code
def methods = new AstBuilder().buildFromSpec {
expression {
methodCall {
variable('this')
constant('println')
argumentList {
constant('replace')
}
}
}
}
block.statements.addAll(methods)
methods = new AstBuilder().buildFromCode {
println('third')
}
block.statements.addAll(methods[0].statements)
}
}
}
}
最终目录结构
然后我们运行ASTtest.groovy,会输出两行字符串。
str
replace
third
那么我们修改方法就成功了。
三、使用AST绑定注解
同样在新的module中我们新建几个类。
AnnotationAST
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
/**
* Created by Administrator on 2017/6/23 0023.
*/
@GroovyASTTransformation
class AnnotationAST implements ASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
if(nodes.size()==2){
println nodes[0]
println nodes[1]
}
}
}
ASTtest.groovy
class ASTTextContent {
@BindMethod
def fun(){
sleep(2_000)
}
}
new ASTTextContent().fun()
BindMethod
import org.codehaus.groovy.transform.GroovyASTTransformationClass
import java.lang.annotation.ElementType
import java.lang.annotation.Target
/*
* Created by TY on 2018/4/20.
*/
@Target(ElementType.METHOD)
@GroovyASTTransformationClass('AnnotationAST')
@interface BindMethod {
}
最终目录结构:
然后我们运行ASTtest,观察之前我们普通执行的结果,知道visit中的nodes只有一个ModuleNode,而使用注解绑定,会将注解的方法以及注解一并传过来,所以这里的结果:
org.codehaus.groovy.ast.AnnotationNode@17695df3
MethodNode@878274034[java.lang.Object fun()]
那么我们就可以不用很容易的处理一些东西了。我们把AnnotationAST的visit方法修改一下。目的是将类处理成这样的。
使用ast分析
与只有单行代码 sleep(2_000)的进行对比
那么我们知道了缺失的表达式,创建好填充进去就可以了。
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
def methodNodes = nodes.findAll { it instanceof MethodNode }
methodNodes.each {
MethodNode node ->
//单行语句会生成ReturnStatement
def startStatement = new AstBuilder().buildFromCode {
def start = System.nanoTime()
}
//多行代码生成ExpressionStatement
def endStatement = new AstBuilder().buildFromCode {
def use = System.nanoTime()-start
println("use:${use/1.0e9}")
}
BlockStatement blockStatement = node.code
ReturnStatement returnStatement = startStatement[0].statements[0]
//这里添加需要ExpressionStatement在·
blockStatement.statements.add(0,new ExpressionStatement(returnStatement.expression))
blockStatement.statements.addAll(endStatement[0].statements)
}
}
那么最终结果就是:
use:2.000883712
关于groovy语言的使用已经写了十篇文章了,不能说总结的十分全面,但是基本上和java的区别用法都总结差不多了,如果你熟悉java,应该对groovy的与语法来说不成问题了。接下来准备研究android的gradle插件了。