随便唠唠 编译时注解 这个技术
1 => 缘起
公司部门负责人突然有一天聊起了
编译时注解
这么个名词,说实话,编译时
、注解
这两个名词非常的耳熟,连在一起对我就有点陌生了。使我不得不坐下来研究一番。
经常写过注解,配合着反射,AOP等操作,能写出各种方便、通用的程序。但是这里用的注解说我们常说的
运行时注解
,它会跟随着程序在运行时对程序进行处理。如下就是我们常用的一个运行时注解@RestController
:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
@RestController
@RequestMapping({"/user"})
public class UserController {
}
它会伴随程序直到运行期,也就是随程序一起进入JVM,在运行时可以获取他们。而
编译时注解
不一样的地方在于他们只伴随程序直到编译期,不会被编入class文件,当然,更不会随程序进入JVM,那么,它有什么作用呢?
2 => 缘及Lombok
这不得不提起大名鼎鼎的
Lombok
,我不知道是否全世界的Java程序员都在用它,至少我目前我待过的公司和我所写过的程序都在用它。毋庸置疑,它最大的好处就是帮我们生成setter、getter、全参构造、hashCode()、toString()等方法函数,使我们的实体类看上去非常的清爽、简洁。它的使用方法如下:
@Setter
public class User {
/**
* 名称
*/
private String name;
}
可是为什么使用它
@Setter
这么一个小小的注解就不用再为字段写 set 方法了呢?我们可以康康生成的 class 文件长什么样,用此来验证它的工作方式。class如下:
public class User {
private String name;
public User() {
}
public void setName(final String name) {
this.name = name;
}
}
我们可以发现,生成的 User 没有了 @Setter 注解,多了无参构造函数和 name 的 Setter 方法。无参构造是编译期给类默认生成的,和 @Setter 注解无关。@Setter 只是为类生成了 setName() 方法。
所以,我们可以粗糙的认为,Lombok 通过识别 @Setter 注解在编译的过程中为我们实体类程序加入了 setter 方法的代码。而@Setter 注解本身并不会随程序到JVM去执行。它符合编译时注解的特点,因此
@Setter
是一个合格的编译时注解
。
了解了它大致的工作内容后,我们便可以开始了解它的实现方式了。
3 => 缘深 APT
在我的多方打听下(也就是百度,bing,搜狗等),找到了一个叫
APT
的家伙,他的全称是AbstractProcessor
,他是在javax.annotation.processing
包下,也就是 JDK 包含的拓展包 javax 下。他是一个抽象类,结构如下:
public abstract class AbstractProcessor implements Processor {
/**
* Processing environment providing by the tool framework.
*/
protected ProcessingEnvironment processingEnv;
private boolean initialized = false;
/**
* Constructor for subclasses to call.
*/
protected AbstractProcessor() {}
....
}
先说说他的接口
Process
,它被官方定义为注解处理器,在编译期会 有一个单独且完整的JVM来运行所有被实现的APT对象,这个JVM和你的程序最后运行的在的JVM区别开,他只是用于编译阶段的。
也就是说,我们可以借助于这个技术来自定义我们自己的编译过程,比如为我们的实体类生成Settter、Getter等。那么我们如何自己实现这个抽象类来生成我们自己需要的代码呢?
4 => 缘生@Setter
这里我们先通过实现生成Setter的方式来做例子,方便引申出更多的玩法。
新建一个
Springboot
项目吧,在pom.xml
中导入如下依赖:
<dependencies>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc2</version>
</dependency>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.9.0</version>
</dependency>
<!-- ${java.home} 为 JRE 的安装目录 -->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
接下来我们先定义一个
@Setter
的注解,注意哦,我们将它定义为编译时的注解。它大概先长这个样子吧。
@Retention(RetentionPolicy.SOURCE) // SOURCE 表示该注解只在源码中保留
@Target(ElementType.TYPE)
public @interface Setter {
String value() default "";
}
接下里我们定义一个
SetterProcessor
来实现AbstractProcessor
。
@AutoService(Processor.class)
public class SetterProcessor extends AbstractProcessor {
// 语法树操作工具
private JavacTrees javacTrees;
private TreeMaker treeMaker;
private Names names;
private Messager messager;
// 初始化,获取一些工具
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
this.javacTrees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
// 处理方法
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 获取被添加了 @Setter 的元素(可能是类、也可能是方法函数)
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Setter.class);
elementsAnnotatedWith.forEach(e -> {
// 从元素中获取语法树
JCTree tree = javacTrees.getTree(e);
// 语法树追加生成的Setter代码
tree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
// 生成空变量链表
List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
// 在抽象树中找出所有待生成Setter的变量
for (JCTree jcTree : jcClassDecl.defs) {
if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
}
}
// 对于变量进行生成Setter方法的操作
jcVariableDeclList.forEach(jcVariableDecl -> {
// 控制台打印信息
messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " 已生成Setter。");
jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
});
return true;
}
// 为变量生成Setter
private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
// 生成表达式 例如 this.a = a;
JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
statements.append(aThis);
JCTree.JCBlock block = treeMaker.Block(0, statements.toList());
// 生成入参
JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null);
List<JCTree.JCVariableDecl> parameters = List.of(param);
// 生成返回对象
JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), methodType, List.nil(), parameters, List.nil(), block, null);
}
private Name getNewMethodName(Name name) {
String s = name.toString();
return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}
// 表达式赋值,获取语句流
private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
return treeMaker.Exec(
treeMaker.Assign(
lhs,
rhs
)
);
}
// 支持的注解类型,这里把我们自己定义的 @Setter 给他
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> typeSet = new LinkedHashSet<>();
typeSet.add(Setter.class.getCanonicalName());
return typeSet;
}
// JDK 版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
}
}
这里突然出现的大段代码,大家大可不必惊慌。主要包含了四个 重载的
@Override
方法:
<1>synchronized void init(processingEnv)
:初始化方法,可以获取一些工具类
<2>boolean process(annotations, roundEnv)
:处理方法
<3>Set<String> getSupportedAnnotationTypes()
:支持的注解类型
<4>SourceVersion getSupportedSourceVersion()
:编译的JDK版本
这里核心的就是
init()
和process()
这两个方法。先说init()
,它在初始化时获取了几个奇怪的对象:
JavacTrees
TreeMaker
Names
Messager
Messager
:最简单的Messager
是用于编译时控制台信息打印的。
JavacTrees
:而其他几个在说之前大家需要先参照以前的知识明白一个概念,对于前端的html
代码,在浏览器运行时会有DOM树
的概念,而Java的代码,对于编译器来说,一层一层的,有循环嵌套和方法调用,也就像树一样,它被称为语法树
,因其抽象性,也被称为抽象语法树
。编译器在操作语法树时就需要用到语法树操作工具,也就是JavacTrees
。
TreeMaker
:对于语法树上的每个节点,比如代码块 {}、IF 判断、变量定义、变量赋值、Switch、Return等如果都用 new 的方法不免有些麻烦,于是TreeMaker
提供了静态方法,方便快速创建和拼接这些语法节点。
Names
:加入我们需要创建一个 List 变量时,不能直接写 List list;而需要指定命令空间,因为执行 process 这已经是在编译时了,不允许出现编译不通过的语法了,所以除了写成java.util.List list
,还必须确保java.util.List
的的存在,通过 Names 工具类如果能将java.util.List
转为一个Name对象
,则证明这个类是存在的,这个命名空间是可用的,否则编译会失败。你在编程时可能会想为啥不直接传个"java.util.List"
字符串呢,哈哈,强大的javac
编译器会告诉你他不是好忽悠的!不信你试试呢。
需要
重点
关注的是,这些语法树节点都源于一个叫JCTree
的家伙,他在 pom.xml 中依赖的第三个包中,也就是 tools.jar 中,提供他的 API 文档,可以学习他提供的节点用法:API文档,这个网页有点小卡,忍忍就好了。
接下来,需要说说
@AutoService(Processor.class)
这个玩意,在我们的 SetterProcessor 类的头上,他有什么作用呢?
我们不妨思考下,只定义一个类,编译器怎么会知道要执行他呢,让我们看看 Lombok是怎么告诉编译器的:
原来他是在项目的
Resources
下的META-INF
下的services
下生成了一个javax.annotation.processing.Processor
文件,在里面告诉编译器需要执行的 Process ,而@AutoService(Processor.class)
这个小玩意正是帮我们干了这件事。他是在pom.xml
文件引入的第一个依赖包中,如果您想自己写这个文件也可以不需要依赖它哦!
那么,最后的问题来了,为什么没起作用呢?
这里的方式都是正常操作的,为什么没有对 User 的 name 参数生成 Setter 呢?这里就是需要注意的了,
实现了 Process 的类不能和使用它的类一起编译,那样就无法使用 Process 了,只有把 实现有 Process 的项目编译成 jar 放进 maven 仓库,作为依赖使用,才可以起作用。
新建一个项目,导入依赖,使用@Setter,如下成功生成了:
这两个小demo就没传gitee , 需要参考的可以下载哈 下载
4 => 缘远
其实,APT这个技术还是很厉害的,只是大家平时用的少,都比较喜欢运行时注解,都不太喜欢去看接近底层、JVM、编译等东西,包括我自己,对这些晦涩的东西有一种敬而远之的主动,主要是有点怕,对未知的东西的害怕,但这足以说明自己的浅薄。
编译时注解本质可以做到和运行时注解一样的事儿,甚至可以做到更多更好,但是运行的效率比运行时注解更快,因为他不需要在运行时为我们的代码生成代理类然后编译成class等操作,这无疑是略微提高了效率的,虽然大部分时候对这点效率无足轻重。本文并非建议您一定使用编译时注解,只是说明了有另外一种方式而已,并且这种方式有他的局限性,一则是学习成本较大,二来使用它需要对每种情况具有预见性,稍有不慎,便会使得程序无法通过编译,这在作为框架使用时是不被喜欢的。
比如缓存这件事,如果您用了 redis ,不免要在 service 的方法最前面中加入从 redis 获取缓存和方法最后从 redis 存入缓存的逻辑,您当然想使自己的代码更加优美,于是使用 AOP 切面 + 运行时注解来搞定,这样不至于使业务代码看起来臃肿。而使用编译时注解则避免了代理类的使用,将直接在源码中生成缓存逻辑,一个简简单单的注解确实充满了魔力。
如下,是我依照自己的想法写了一版自动生成缓存的,不足以称为框架的东西吧!
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Cache {
/**
* 是否开启缓存
*/
boolean enable() default true;
/**
* 缓存的 key | you'd better to set this param
*/
String key() default "";
/**
* 失效时间【默认30分钟】
*/
long expire() default 30 * 60l;
/**
* 失效时间单位【默认为秒】
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 缓存类型【默认是 REDIS 】
*/
CacheType cacheType() default CacheType.REDIS;
}
@AutoService(Processor.class)
// 生成 Java 8 的源码
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 支持的注解
@SupportedAnnotationTypes({
"com.mabang.cache.annotation.Cache"
})
@SuppressWarnings("all")
public class CacheProcessor extends AbstractProcessor {
// 获取语法树的工具
private JavacTrees javacTrees;
// 创建语法树节点的工具
private TreeMaker treeMaker;
// 名字工具
private Names names;
// 打印信息的工具类
private Messager messager;
// 每个方法中缓存对象的名称
private static final String cacheObjectName = "cacheObject$$";
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
javacTrees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
treeMaker = TreeMaker.instance(context);
names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Cache.class);
elements.stream().forEach(element -> {
if (element.getKind().equals(ElementKind.METHOD)) {
// 如果是方法
handleMethodCache(element, element.getAnnotation(Cache.class));
} else if (element.getKind().equals(ElementKind.CLASS)) {
// 如果是类
handleClassCache(element);
} else {
messager.printMessage(Diagnostic.Kind.WARNING,
new StringBuilder()
.append("对于 ")
.append(element.getEnclosingElement().getSimpleName()).append(".")
.append(element.getSimpleName())
.append(" 该元素无法完成缓存,暂时只支持对于方法的缓存。"));
}
});
return true;
}
/**
* 处理方法的缓存
*
* @param methodElement
* @param cache
*/
private void handleMethodCache(Element methodElement, Cache cache) {
// 获取语法树
JCTree.JCMethodDecl methodDecl = (JCTree.JCMethodDecl) javacTrees.getTree(methodElement);
// 获取返回类型,如果是 void 则不做缓存
JCTree.JCExpression resType = methodDecl.restype;
if (resType.type instanceof Type.JCVoidType) {
waringMessage(
new StringBuilder()
.append(methodElement.getEnclosingElement().getSimpleName()).append(".")
.append(methodElement.getSimpleName())
.append(" 返回类型为 void , 跳过缓存织入。")
.toString());
return;
}
// 非 void 方法
if (!cache.enable()) return;
// 获取具体的缓存处理实现
CacheHandler cacheHandler$$ = CacheLoadService.get(cache.cacheType());
if (cacheHandler$$ == null) {
if (CacheType.REDIS == cache.cacheType()) {
waringMessage(
new StringBuilder()
.append(methodElement.getEnclosingElement().getSimpleName()).append(".")
.append(methodElement.getSimpleName())
.append(" 缓存织入失败,原因可能是")
.append(cache.cacheType().desc())
.append(" 未导入相应的依赖!")
.toString());
} else {
waringMessage(
new StringBuilder()
.append(methodElement.getEnclosingElement().getSimpleName()).append(".")
.append(methodElement.getSimpleName())
.append(" 缓存织入失败,原因可能是")
.append(cache.cacheType().desc())
.append(" 暂不被支持!")
.toString());
}
return;
}
// 织入逻辑
weavingCacheToMethod(cacheHandler$$, methodDecl, methodElement, cache);
}
/**
* 对方法织入缓存
*
* @param cacheHandler$$ 缓存处理器
* @param methodDecl 需要缓存的方法
* @param methodElement 需要缓存的方法元素
* @param cache 缓存注解
*/
private void weavingCacheToMethod(CacheHandler cacheHandler$$,
JCTree.JCMethodDecl methodDecl,
Element methodElement,
Cache cache) {
// 给他的类引入相应的 cacheHandler$$
processImportCacheHandler(cacheHandler$$, methodElement.getEnclosingElement());
// 创建相应的 cacheHandler$$ 属性
processCreateCacheHandlerField(cacheHandler$$, methodElement.getEnclosingElement());
// 1、生成缓存变量
JCTree.JCVariableDecl cacheObject$$ = makeCacheObject(methodDecl);
// 2、查缓存
JCTree.JCIf getFromCacheIf = makeGetFromCacheIf(cacheHandler$$, methodElement, methodDecl, cache);
// 3、对方法主体的改造和存储缓存
JCTree.JCBlock body = makeBodyTryCacheAndSetCache(cacheHandler$$, methodElement, methodDecl, cache);
// 设置新 body
methodDecl.body = treeMaker.Block(0, com.sun.tools.javac.util.List.of(
cacheObject$$, // 定义 cacheObject$$ 变量
getFromCacheIf, // 判断缓存是否存在,存在则返回
// 判断是否值得缓存
body
));
infoMessage("asjdfh");
}
.......
}
这里只提供了一部分,需要看全部源码的请移驾 cache-processor,如果您有一些 idea 的话,也欢迎一起探讨呀。