1. 什么是注解
注解(annotation
)以一种形式化的方式在代码中添加信息,如我们最常见的@Overide
,表明当前的方法将覆盖超类中的方法。
由于注解是与源代码结合在一起,并不是像文档一样脱离代码,所以这使得编译器来测试和验证,并存储额外的信息。
另外呢,通过注解处理器我们可以生成一些描述符文件或者新的类的定义,有助于在项目中减少重复样板代码的编写。
除此之外,注解使得代码更加简洁易读以及在编译器进行类型检查等特点。
通过以上的介绍,肯定迫不及待的想进一步了解注解了吧。
2. 注解的基本知识
在我们自定义注解之前,先要了解这四种注解,这四种注解又称为元注解,主要作用就是负责创建新的注解。这四种注解分别为@Target
, @Retention
,@Documented
,@Inherited
2. 1 @Target
@Target
: 表示该注解可以用于什么地方。
可选参数 | 说明 |
---|---|
ElementType.CONSTRUCTOR |
构造函数的声明 |
ElementType.FIELD |
成名变量的声明(包含enum ) |
LOCAL_VARIABLE |
局部变量的声明 |
METHOD |
方法的声明 |
PACKAGE |
包声明 |
PARAMETER |
参数声明 |
TYPE |
类、接口(包括注解类型) 或enum 声明 |
像我们熟知的@Override
的定义:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
就是使用的是ElementType.METHOD
, 我们看一下@Target
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
我们在定义@Target(ElementType.METHOD)
,其实是一种简写,真正的写法@Target(value = ElementType.METHOD
. 原因是因为Target
类中有一个value()
元素,如果定义成别的变量,则必须要写全了。
2.2 @Retention
@Retention
: 表示该注解信息保存到什么级别
可选参数 | 说明 |
---|---|
RetentionPolicy.SOURCE |
注解将被编译器丢弃 |
RetentionPolicy.CLASS |
注解在class 文件中可用, 但会被VM丢弃 |
RetentionPolicy.RUNTIME |
VM将运行期也保留注解信息,因此可用通过反射机制来读取注解的信息 |
可见这三种策略对应的生命周期文件为:
java
源文件 -> .class
-> 内存字节码
对于这三种保存策略,我们可以分别用在这三种场景下:
@Retention(RetentionPolicy.SOURCE)
: 比如@Override
只对一个方法进行检查,或者定一个枚举变量来表明这个变量的取值范围。
@Keep
@Retention(RetentionPolicy.SOURCE)
@IntDef({INIT, PLAYING, STOP, PAUSE})
public @interface PlayState {
int INIT = 0;
int PLAYING = 1;
int STOP = 2;
int PAUSE = 3;
}
比如定义一个音乐播放器的状态只能为这四种情况,我们可以定义保存级别为RetentionPolicy.SOURCE
. 因为该注解只是用来在进行一个状态的检查,并不需要处理器的。
@Retention(RetentionPolicy.CLASS)
: 编译时注解,比如我们想在编译时期生成一些页面的路由信息,像Arouter
库,注解在class
文件中可用, 这也是默认的注解生命周期。这种情况最多的就是使用apt 工具实现注解处理器,在编译器期间生成相应的代码。当然如果只是仅仅本地用,直接使用RetentionPolicy.SOURCE
即可,但往往有的时候需要以库的方式(.jar, .aar)的提供给其它人使用,这时候就需要在.class
文件中,也需要能够访问这些注解,所以对于编译时注解我们用这种生命周期。
如 Arouter
库中的Route
注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
* Path of route
*/
String path();
/**
* Used to merger routes, the group name MUST BE USE THE COMMON WORDS !!!
*/
String group() default "";
/**
* Name of route, used to generate javadoc.
*/
String name() default "undefined";
/**
* Extra data, can be set by user.
* Ps. U should use the integer num sign the switch, by bits. 10001010101010
*/
int extras() default Integer.MIN_VALUE;
/**
* The priority of route.
*/
int priority() default -1;
}
@Retention(RetentionPolicy.RUNTIME)
: 运行时注解,这个就不难理解了,这是为了在运行期间能够以反射的方式获取类或方法,字段,然后找到注解。如weex sdk
中自定义module
模块下的@JsMethod
这个注解
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target(ElementType.METHOD)
public @interface JSMethod {
boolean uiThread() default true;
String alias() default NOT_SET;
String NOT_SET = "_";
}
private void generateMethodMap() {
...
HashMap<String, Invoker> methodMap = new HashMap<>();
try {
for (Method method : mClazz.getMethods()) {
for (Annotation anno : method.getDeclaredAnnotations()) {
if (anno != null) {
if(anno instanceof JSMethod) {
JSMethod methodAnnotation = (JSMethod) anno;
String name = JSMethod.NOT_SET.equals(methodAnnotation.alias())? method.getName():methodAnnotation.alias();
methodMap.put(name, new MethodInvoker(method, methodAnnotation.uiThread()));
break;
}else if(anno instanceof WXModuleAnno) {
WXModuleAnno methodAnnotation = (WXModuleAnno)anno;
methodMap.put(method.getName(), new MethodInvoker(method,methodAnnotation.runOnUIThread()));
break;
}
}
}
}
} catch (Throwable e) {
WXLogUtils.e("[WXModuleManager] extractMethodNames:", e);
}
}
2.3 @Documented @Inherited
这两个注解就比较好理解了,@Documented
的注解会包含在JavaDoc
中。而@Inherited
表明该注解允许子类继承父类中的注解.
@Inherited
public @interface anno {
}
@anno
public class Parent {
}
public class Child extends Parent {
}
那么类child
也是被一个被@anno
注解的类。
好了,我们介绍完了创建新注解的四大武器之后,就摩拳擦掌创建自定义注解吧。
3. apt介绍
apt(annotation processing tool)
注解处理工具,就是操作Java
源文件,当处理完源文件后编译它们,在系统创建的过程中会自动创建一些新的源文件,这些新文件会在新的一轮中的注解处理器中接受检查,直到不再有新的源文件产生为止。这个过程中是发生在编译期间(compile time
),而非运行期间,所有apt
产生为自定义注解性能的提高做出了大大的贡献。
3.1 AbstractProcessor
接下来,我们了解一下注解处理器的相关api
. 任何一个自定义处理器都需要继承AbstractProcessor
.
public class BuilderProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEvn) {
super.init(processingEvn);
}
@Override
public Set<String> getSupportedAnnotationTypes() {
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){ return true;
}
}
这是一个自定义注解处理器基本的代码框架。
init(ProcessingEnvironment processingEnv)
, 这个方法在整个编译期间仅仅被调用一次,作用就是初始化参数ProcessingEnvironment
。
public interface ProcessingEnvironment {
Map<String, String> getOptions();
Messager getMessager();
Filer getFiler();
Elements getElementUtils();
Types getTypeUtils();
SourceVersion getSourceVersion();
Locale getLocale();
}
这个接口可以提供这些变量,getOptions
这里的map
指的是在编译期间,app
传给注解处理器的值,我们这里还拿Arouter
库来看:
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// Attempt to get user configuration [moduleName]
Map<String, String> options = processingEnv.getOptions();
if (MapUtils.isNotEmpty(options)) {
moduleName = options.get(KEY_MODULE_NAME);
generateDoc = VALUE_ENABLE.equals(options.get(KEY_GENERATE_DOC_NAME));
}
...
}
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]
}
}
Messager
: 在注解处理器处理注解生成新的源代码过程中,我们可用Messager
来将一些错误信息打印到控制台上。
Filer
: 我们可以通过这个类来创建新的文件。
Elements
: 它其实是一个工具类,用来处理所有的Element
元素,而我们可以把生成代码的类中所有的元素都可以成为Element
元素,如包就是PackageElement
, 类/接口为TypeElement
, 变量为VariableElement
, 方法为ExecutableElement
Type
: 它其实也是一个工具类,只是用来处理TypeMirror
. 也就是一个类的父类。TypeMirror superClassType = currentClass.getSuperclass();
process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
这个方法是最重要的,注解处理器新生成出来的类就是在这个方法生成的。之前我们说过ProcessingEnvironment
是包含了注解处理器相关的工具类和编译器配置的参数,而RoundEnvironment
则是指在每一轮的扫描和处理源代码中获取被注解的Element
.为了方便后面例子的叙述,我们这里再先了解一些api
.
- 获取被注解的
Element
List<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Builder.class)
- 判断某个
Element
的类型
element.getKind() == ElementKind.CLASS
- 获取父类
TypeMirror superClassType = typeElement.getSuperclass();
typeElement = (TypeElement) types.asElement(superClassType);
- 获取类中内部元素
List<? extends Element> closedElements = typeElement.getEnclosedElements()
等等。
4. 自定义build注解
我们在项目中往往喜欢写xxxBuilder.setXXX().setXXX().build()
方法来构造一个对象。比如我们有一个Student
类。
public class Student {
public String mName;
public int mAge;
public int mGender;
public int mEnglishScore;
public int mMathScore;
}
我们要为它写一个Builder
类,类似于这样:
public class StudentBuilder {
private String mName;
private int mAge;
private int mGender;
private int mEnglishScore;
private int mMathScore;
public StudentBuilder setName(String name) {
mName = name;
return this;
}
public StudentBuilder setAge(int age) {
mAge = age;
return this;
}
public StudentBuilder setGender(int gender) {
mGender = gender;
return this;
}
public StudentBuilder setEnglishScore(int englishScore) {
mEnglishScore = englishScore;
return this;
}
public StudentBuilder setMathScore(int mathScore) {
mMathScore = mathScore;
return this;
}
public Student build() {
Student student = new Student();
student.mName = mName;
student.mAge = mAge;
student.mGender = mGender;
student.mEnglishScore = mEnglishScore;
student.mMathScore = mMathScore;
return student;
}
这样的代码经常格式是一样的,我们是否可以用自定义注解来自动帮我们生成这些样板代码呢,答案是可以的,那我们开始吧。
首先定义两个注解,一个注解表示哪个类需要构建,一个注解表示这个类哪些字段可以通过setXXX
方法来设置。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
String value();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderField {
String value();
}
接下来我们定一个被注解的类:
@Builder("AptStudent")
public class AptStudent {
@BuilderField("name")
public String name;
@BuilderField("age")
public int age;
@BuilderField("gender")
public int gender;
@BuilderField("englishScore")
public int englishScore;
@BuilderField("mathScore")
public int mathScore;
}
接下来我们定义一个注解处理器:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
try {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Builder .class);
for (Element element : elements) {
if (!ValidCheckUtil.isValidClass(element)) {
continue;
}
List<? extends Element> memberFields = mElementUtils.getAllMembers((TypeElement)
element);
List<VariableElement> annoFields = new ArrayList<>();
if (memberFields == null) {
return false;
}
for (Element member : memberFields) {
if (ValidCheckUtil.isValidField(member)) {
annoFields.add((VariableElement) member);
}
}
mCodeGenerator.generatorCode((TypeElement) element, annoFields);
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
这里,我们主要看一下process
方法,其它的代码具体见文末的链接地址。这里我们是遍历出用Builder
和BuilderField
注解的类和变量,然后用工具类BuilderCodeGenerator
来生成代码,接下来我们看一下BuilderCodeGenerator
这个类。
private MethodSpec generateBuildMethodCode(TypeElement typeElement, List<VariableElement> fields) {
String methodName = "build";
TypeName typeName = TypeName.get(typeElement.asType());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PUBLIC)
.returns(typeName);
String code = "{0} obj = new {1}();\n";
methodBuilder.addCode(MessageFormat.format(code, typeName, typeName));
for (VariableElement field : fields) {
BuilderField fieldAnno = field.getAnnotation(BuilderField.class);
String codeTxt = "obj.{0} = {1};\n";
methodBuilder.addCode(MessageFormat.format(codeTxt, fieldAnno.value(), fieldAnno.value()));
}
methodBuilder.addStatement("return obj");
mBuildMethodSpec = methodBuilder.build();
return mBuildMethodSpec;
}
这里,我们就看build()
这个方法是如何生成的。生成代码我们用的是JavaPoet
这个库。可以很方便的生成代码。生成一个类就是构建一个TypeSpec
,生成一个方法就是构建一个MethodSpec
,生成一个变量就是用FieldSpec
. 我们可以直接使用addStatement()
来生成一段代码,也可以使用`addCode`来生成,addCode
里面我们要注意里面的几个通配符。
* $T: 类型替换 addStatement("$T student", Student.class) => Student student
* $L: 字面量替换 addStatement("$L = null", "student") => student = null
* $S: 字符串 addStatement("student = new Student($S)", "amy") => student = new Student("amy")
* $N: 名称替换 MethodSpec methodSpec = MethodSpec.methodBuilder("Student").build();
* ("$N", methodSpec) => Student.
好了,这样我们的自定义注解就讲完了,具体的代码细节还请看地址:
https://github.com/thh0613/BuildPatternDemo.git