插件化注解处理API(Pluggable Annotation Processing API)
Java奇技淫巧-插件化注解处理API(Pluggable Annotation Processing API)
参考资料
- JDK6的新特性之六:插入式注解处理API(Pluggable Annotation Processing API)
- Java Annotation Processing and Creating a Builder
简介
插件化注解处理(Pluggable Annotation Processing)APIJSR 269提供一套标准API来处理AnnotationsJSR 175,实际上JSR 269不仅仅用来处理Annotation,我觉得更强大的功能是它建立了Java 语言本身的一个模型,它把method、package、constructor、type、variable、enum、annotation等Java语言元素映射为Types和Elements,从而将Java语言的语义映射成为对象,我们可以在javax.lang.model包下面可以看到这些类。所以我们可以利用JSR 269提供的API来构建一个功能丰富的元编程(metaprogramming)环境。JSR 269用Annotation Processor在编译期间而不是运行期间处理Annotation, Annotation Processor相当于编译器的一个插件,所以称为插入式注解处理.如果Annotation Processor处理Annotation时(执行process方法)产生了新的Java代码,编译器会再调用一次Annotation Processor,如果第二次处理还有新代码产生,就会接着调用Annotation Processor,直到没有新代码产生为止。每执行一次process()方法被称为一个"round",这样整个Annotation processing过程可以看作是一个round的序列。JSR 269主要被设计成为针对Tools或者容器的API。这个特性虽然在JavaSE 6已经存在,但是很少人知道它的存在。下一篇介绍的Java奇技淫巧-lombok就是使用这个特性实现编译期的代码插入的。另外,如果没有猜错,像IDEA在编写代码时候的标记语法错误的红色下划线也是通过这个特性实现的。KAPT(Annotation Processing for Kotlin),也就是Kotlin的编译也是通过此特性的。
Pluggable Annotation Processing API的核心是Annotation Processor即注解处理器,一般需要继承抽象类javax.annotation.processing.AbstractProcessor
。注意,与运行时注解RetentionPolicy.RUNTIME
不同,注解处理器只会处理编译期注解,也就是RetentionPolicy.SOURCE
的注解类型,处理的阶段位于Java代码编译期间。
使用步骤
插件化注解处理API的使用步骤大概如下:
- 1、自定义一个Annotation Processor,需要继承
javax.annotation.processing.AbstractProcessor
,并覆写process方法。 - 2、自定义一个注解,注解的元注解需要指定
@Retention(RetentionPolicy.SOURCE)
。 - 3、需要在声明的自定义Annotation Processor中使用
javax.annotation.processing.SupportedAnnotationTypes
指定在第2步创建的注解类型的名称(注意需要全类名,"包名.注解类型名称",否则会不生效)。 - 4、需要在声明的自定义Annotation Processor中使用
javax.annotation.processing.SupportedSourceVersion
指定编译版本。 - 5、可选操作,可以通在声明的自定义Annotation Processor中使用
javax.annotation.processing.SupportedOptions
指定编译参数。
实战例子
基础
下面我们模仿一下测试框架Junit里面的@Test注解,在运行时通过Annotation Processor获取到使用了自定义的@Test注解对应的方法的信息。因为如果想要动态修改一个类或者方法的代码内容,需要使用到字节码修改工具例如ASM等,这些操作过于深入,日后再谈。先定义一个注解:
package club.throwable.processor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author throwable
* @version v1.0
* @description
* @since 2018/5/27 11:18
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Test {
}
定义一个注解处理器:
@SupportedAnnotationTypes(value = {"club.throwable.processor.Test"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class AnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("Log in AnnotationProcessor.process");
for (TypeElement typeElement : annotations) {
System.out.println(typeElement);
}
System.out.println(roundEnv);
return true;
}
}
编写一个主类:
public class Main {
public static void main(String[] args) throws Exception{
System.out.println("success");
test();
}
@Test(value = "method is test")
public static void test()throws Exception{
}
}
接着需要指定Processor,如果使用IDEA的话,Compiler->Annotation Processors中的Enable annotation processing必须勾选。然后可以通过下面几种方式指定指定Processor。
- 1、直接使用编译参数指定,例如:javac -processor club.throwable.processor.AnnotationProcessor Main.java。
- 2、通过服务注册指定,就是META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.AnnotationProcessor。
- 3、通过Maven的编译插件的配置指定如下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessors>
<annotationProcessor>
club.throwable.processor.AnnotationProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
值得注意的是,以上三点生效的前提是club.throwable.processor.AnnotationProcessor已经被编译过,否则编译的时候就会报错:
[ERROR] Bad service configuration file, or exception thrown while
constructing Processor object: javax.annotation.processing.Processor:
Provider club.throwable.processor.AnnotationProcessor not found
解决方法有两种,第一种是提前使用命令或者IDEA右键club.throwable.processor.AnnotationProcessor对它进行编译;第二种是把club.throwable.processor.AnnotationProcessor放到一个独立的Jar包引入。我在这里使用第一种方式解决。
最后,使用Maven命令mvn compile进行编译。输出如下:
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[club.throwable.processor.Test,club.throwable.processor.Main, club.throwable.processor.AnnotationProcessor, processingOver=false]
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[], processingOver=false]
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[], processingOver=true]
可见编译期间AnnotationProcessor生效了。
进阶
下面是一个例子直接修改类的代码,为实体类的Setter方法对应的属性生成一个Builder类,也就是原来的类如下:
public class Person {
private Integer age;
private String name;
public Integer getAge() {
return age;
}
@Builder
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
@Builder
public void setName(String name) {
this.name = name;
}
}
生成的Builder类如下:
public class PersonBuilder {
private Person object = new Person();
public Person build() {
return object;
}
public PersonBuilder setName(java.lang.String value) {
object.setName(value);
return this;
}
public PersonBuilder setAge(int value) {
object.setAge(value);
return this;
}
}
自定义的注解如下:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
}
自定义的注解处理器如下:
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author throwable
* @version v1.0
* @description
* @since 2018/5/27 11:21
*/
@SupportedAnnotationTypes(value = {"club.throwable.processor.builder.Builder"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement typeElement : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(typeElement);
Map<Boolean, List<Element>> annotatedMethods
= annotatedElements.stream().collect(Collectors.partitioningBy(
element -> ((ExecutableType) element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")));
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Builder must be applied to a setXxx method "
+ "with a single argument", element));
Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));
String className = ((TypeElement) setters.get(0)
.getEnclosingElement()).getQualifiedName().toString();
try {
writeBuilderFile(className, setterMap);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private void writeBuilderFile(
String className, Map<String, String> setterMap)
throws IOException {
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String builderClassName = className + "Builder";
String builderSimpleClassName = builderClassName
.substring(lastDot + 1);
JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
setterMap.forEach((methodName, argumentType) -> {
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});
out.println("}");
}
}
}
主类如下:
public class Main {
public static void main(String[] args) throws Exception{
//PersonBuilder在编译之后才会生成,这里需要编译后才能这样写
Person person = new PersonBuilder().setAge(25).setName("doge").build();
}
}
先手动编译BuilderProcessor,然后在META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.builder.BuilderProcessor
,最后执行Maven命令mvn compile进行编译。
编译后控制台输出:
[errorRaised=false, rootElements=[club.throwable.processor.builder.PersonBuilder], processingOver=false]
编译成功之后,target/classes包下面的club.throwable.processor.builder子包路径中会新增了一个类PersonBuilder
:
package club.throwable.processor.builder;
public class PersonBuilder {
private Person object = new Person();
public PersonBuilder() {
}
public Person build() {
return this.object;
}
public PersonBuilder setName(String value) {
this.object.setName(value);
return this;
}
public PersonBuilder setAge(Integer value) {
this.object.setAge(value);
return this;
}
}
这个类就是编译期新增的。在这个例子中,编译期新增的类貌似没有什么作用。但是,如果像lombok那样对原来的实体类添加新的方法,那样的话就比较有用了。因为些类或者方法是编译期添加的,因此在代码中直接使用会标红。因此,lombok提供了IDEA或者eclipse的插件,插件的功能的实现估计也是用了插件式注解处理API。
小结
我在了解Pluggable Annotation Processing API的时候,通过搜索引擎搜索到的几乎都是安卓开发通过插件式注解处理API编译期动态添加代码等等的内容,可见此功能的使用还是比较广泛的。可能在文中的实战例子并不能体现Pluggable Annotation Processing API功能的强大,因此有时间可以基于此功能编写一些代码生成插件,例如下一篇将要介绍的lombok。
(本文完)
插件化注解处理API(Pluggable Annotation Processing API)的更多相关文章
- 【转】Java奇技淫巧-插件化注解处理API(Pluggable Annotation Processing API)
参考资料 JDK6的新特性之六:插入式注解处理API(Pluggable Annotation Processing API) Java Annotation Processing and Creat ...
- ANNOTATION PROCESSING 101 by Hannes Dorfmann — 10 Jan 2015
原文地址:http://hannesdorfmann.com/annotation-processing/annotationprocessing101 In this blog entry I wo ...
- 《Android插件化开发指南》面世
本书在京东购买地址:https://item.jd.com/31178047689.html 本书Q群:389329264 (一)这是一本什么书 如果只把本书当作纯粹介绍Android插件化技术的书籍 ...
- Android 插件化开发(三):资源插件化
在前面的文章中我们成功的加载了外部的Dex(Apk)并执行了插件的Bean代码.这时我们会想,能不能加载并运行插件Apk的Activity.答案当然是能,否则后续我们的研究就没意义了,但是想实现Act ...
- java 编程基础:注解(Annotation Processing Tool)注解处理器 利用注解解读类属性生成XML文件
APT的介绍: APT(Annotation Processing Tool)是一种注解处理工具,它对源代码文件进行检测,并找出源文件所包含的注解信息,然后针对注解信息进行额外的处理. 使用APT工具 ...
- Android插件化技术简介
https://blog.csdn.net/io_field/article/details/79084630 可以通过反射 事先定义统一接口的方式,访问插件中的类和方法 还可以在AndroidMan ...
- Lombok Requires Annotation Processing Annotation processing seems to be disabled for the project "HelloWorld". For plugin to function correctly, please enable it under "Settings > Build > Compiler >
更多精彩详见微信公众号 在网上查找说是插件的问题,但是我安装类插件父级项目没有开启注解处理Annotation Processor,子项目都有开启,如图,顶级项目是demo,下面的都是子项目,把第一 ...
- Android 插件化 动态升级
最新内容请见原文:Android 插件化 动态升级 不少朋友私信以及 Android开源交流几个 QQ 群 中都问到这个问题,这里简单介绍下 1.作用 大多数朋友开始接触这个问题是因为 App 爆棚了 ...
- Android的Proxy/Delegate Application框架 (主要介绍插件化开发)
1. 插件化的原理 是 Java ClassLoader 的原理:Java ClassLoader基础 常用的其他解决方法还包括:Google Multidex,用 H5 代替部分逻辑,删无用代码,买 ...
随机推荐
- Selenium2+python自动化24-js处理富文本(带iframe)【转载】
前言 上一篇Selenium2+python自动化23-富文本(自动发帖)解决了富文本上iframe问题,其实没什么特别之处,主要是iframe的切换,本篇讲解通过js的方法处理富文本上iframe的 ...
- python算法:嵌套数组转变成一维数组
比如,输入是:[2, 1, [3, [4, 5], 6], 7, [8]] 则,输出是:[2, 1, 3, 4, 5, 6, 7, 8] def list_flatten(l, a=None): a ...
- Yii2中如何访问controller的二级目录下的控制器
Yii2默认的控制器下只有一层目录放在controller目录下,当开发的需要的时候需要在该目录下面新增一层目录也就是在controller下增加二级目录,比如controller/api/Membe ...
- AC日记——Dynamic Problem Scoring codeforces 807d
Dynamic Problem Scoring 思路: 水题: 代码: #include <cstdio> #include <cstring> #include <io ...
- BZOJ 3223: Tyvj 1729 文艺平衡树-Splay树(区间翻转)模板题
3223: Tyvj 1729 文艺平衡树 Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 6881 Solved: 4213[Submit][Sta ...
- 线段树+扫描线【HDU1542】Atlantis
Description 给定一些二维空间上的矩形,求它们的面积并. 一道线段树+扫描线的板子题 然而即使我会打了,也不能灵活运用这种算法.QAQ 遇到题还是不太会. 但是这种板子题还是随随便便切的. ...
- 转:mysql group by 用法解析(详细)
group by 用法解析 group by语法可以根据给定数据列的每个成员对查询结果进行分组统计,最终得到一个分组汇总表. SELECT子句中的列名必须为分组列或列函数.列函数对于GROUP BY子 ...
- HDOJ 5693 D Game
众所周知,度度熊喜欢的字符只有两个:B 和D. 今天,它发明了一个游戏:D游戏. 度度熊的英文并不是很高明,所以这里的D,没什么高深的含义,只是代指等差数列[(等差数列百科)](http://baik ...
- 1.6(java学习笔记)static关键字
static关键字 1.static修饰变量也称静态变量,静态变量存放在静态区被该类的所有对象共享. 例如,定义了一个类class User{static Sring city = "a城& ...
- Problem Q: 多项式求和x+(x^2)/2!+(x^3)/3!+...
#include<stdio.h> #include<math.h> int main() { float x,i; scanf("%f",&x); ...