Java 注解

https://rainmonth.github.io/posts/J191115.html

摘要

在分析ARouter源码的时候,看到ARouter-compiler模块里面涉及到了大量的注解处理,在看看现在流行比较广的框架,如ButterKnife、Dagger2都大量的用到了注解,还有AOP编程,也离不开注解的使用,所以本文主要介绍下Java注解的特性,从源码实现角度来了解注解处理的一般流程。本文主要从三个部分来阐述,分别是注解的基本概念及使用、运行时注解、编译时注解。

基本概念及使用

啥是注解

注解时一种元数据(即要来描述数据的的数据)(和文档注释类似,但又有不同于文档注释的功能,通过添加一些处理,能自动生成Java代码)

元注解

  • @Retention,注解的保留策略,即注解在什么阶段存在,支持三种阶段:源码阶段、编译阶段和运行阶段,分别对应RetentionPolicy.SOURCE、RetentionPolicy.CLASS和RetentionPolicy.RUNTIME(三者范围从小到大);
  • @Target,注解的作用目标,即注解用在什么地方,支持一下地方:
作用目标 含义
@Target(ElementType.TYPE) 用于接口(注解本质上也是接口),类,枚举
@Target(ElementType.FIELD) 用于字段,枚举常量
@Target(ElementType.METHOD) 用于方法
@Target(ElementType.PARAMETER) 用于方法参数
@Target(ElementType.CONSTRUCTOR) 用于构造参数
@Target(ElementType.LOCAL_VARIABLE) 用于局部变量
@Target(ElementType.ANNOTATION_TYPE) 用于注解
@Target(ElementType.PACKAGE) 用于包
  • @Inherited,默认情况下用在父类上的注解不会被子类所继承,但如果自定义注解时使用了该注解,则父类上的注解会被子类集成;
  • @Documented,使用该注解时,表明其他注解要本文档化;
  • @interface,Java中声明注解的关键字,使用后被声明的对象将自动集成java.lang.annotation.Annotation类;

看看Java自带的注解@Override:

1
2
3
4
@Target(ElementType.METHOD)                 // 作用范围为方法
@Retention(RetentionPolicy.SOURCE) // 保留策略为在源码时保留,其他阶段会去掉
public @interface Override {
}

系统注解

  • @Override,表明子类重载了父类的方法;
  • @Deprecated,表明方法已废弃;
  • @SuppressWarnnings,忽略编译警告;

自定义注解

自定义注解一般如下:

1
2
3
4
5
@Retention(RetentionPolicy.CLASS)
@Target(value = {ElementType.FIELD, ElementType.METHOD})
public @interface UserMeta {
public int id() default 0;
}

上面的UserMeta为注解名,或括号为注解的定义提,里面可以定义注解方法,如果定义提为空的话,则表明该注解是个标记注解;关于定义体需要作如下说明:

  • 只能使用public和default两个修饰符;
  • 配置参数的类型只能使用基本类型(byte,boolean,char,short,int,long,float,double)和String,Enum,Class,annotation;
  • 一个参数的注解,参数名称建议设置为value,即定义的方法名为value;
  • 配置参数一旦定义就必须有值,要么是设置的值,要么是定义时有default指定的默认值,非基本类型的配置参数,其值不能为null;

下面给出一个完整注解定义实例:

1
2
3
4
5
6
7
8
// 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface UserMeta {
public int id() default 0;
public String name() default "";
public int age() default 0;
}

注解处理器

注解处理器按注解处理的时机可以分为两种,运行时注解处理器和编译时注解处理器,前者主要依赖于反射实现,后者主要依赖于ADT实现。两种处理方式基本思想都一样就是读取代码中定义的注解并对不同的注解做处理

注解处理流程

  1. 定义注解;2. 编写注解处理器;3. 注册注解处理器;4. 使用注解。

运行时注解处理

基本介绍

主要处理注解保留策略为RetentionPolicy.RUNTIME的注解。主要是在运行时通过Java的反射机制来解析处理注解。这种方式依赖于Java的反射,效率较编译时注解来说低。

Java注解API位于java.lang.annotation中,Java反射API位于java.lang.reflect中,而通过反射来处理注解主要通过java.lang.reflect.AnnotatedElement,AnnotatedElement是所有程序元素的父接口,可以通过反射获取某个类的AnnotatedElement对象,然后通过该对象访问其Annotation信息。

注意:这里的程序元素指的是Class、Package、Constructor、Method、Parameter等,具体可以看看哪些类实现了AnnotatedElement接口。
AnnotatedElement常用的方法及解释如下:

  • isAnnotationPresent,指定类型的注解是否存在;
  • getAnnotation,获取程序元素上指定类型的注解;
  • getAnnotations,获取程序元素上的所有注解;
  • getAnnotationsByType,根据type来获取一类注解;
  • getDeclaredAnnotation,获取直接声明的注解(非继承的);
  • getDeclaredAnnotationsByType,根据type来获取直接声明的注解(非继承的);
  • getDeclaredAnnotations,获取所有的声明的注解。

使用示例

示例1:利用上面的UserMeta注解,自动为UserBean赋值;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// UserBean.java
public class UserBean {
private int id;
private int age;
private String name;

@UserMeta(id = 1, name = "Randy", age = 22)
public UserBean() {

}

public UserBean(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "UserBean{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}

// RuntimeAnnotationProcess.java
public class RuntimeAnnotationProcess {

public static void init(Object o) {
if (!(o instanceof UserBean)) {
throw new IllegalArgumentException("[" + o.getClass().getSimpleName() + "] is not type of UserBean");
}
Constructor[] constructors = o.getClass().getDeclaredConstructors();
for (Constructor constructor : constructors) {
if (constructor.isAnnotationPresent(UserMeta.class)) {
UserMeta userMeta = (UserMeta) constructor.getAnnotation(UserMeta.class);
int id = userMeta.id();
int age = userMeta.age();
String name = userMeta.name();
((UserBean) o).setId(id);
((UserBean) o).setAge(age);
((UserBean) o).setName(name);

}
}
}

public static void main(String[] args) {
UserBean userBean = new UserBean();
RuntimeAnnotationProcess.init(userBean);
System.out.println(userBean.toString());
}

}

出处结果为:

1
UserBean{id=1, age=22, name='Randy'}

可见我们成功的实现了利用注解赋值。

示例2
利用运行时处理注解来实现一个简单的ButterKnife(实际上现在的ButterKnife是基于编译时注解+JavaPoet来实现的)。新建一个AnnotationDemo项目
先定义要使用的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ContentView.java,即用来指定布局资源文件的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ContentView {
int value();
}
// ViewInject.java,即用来指定View id的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ViewInject {
int id() default -1;

boolean clickable() default false;

}

其次定义注解处理器,这里姑且教ButterKnife

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ButterKnife.java
public class ButterKnife {
private static final String TAG = ButterKnife.class.getSimpleName();

public static void inject(Activity activity) {
if (activity == null || activity.getWindow() == null) {
return;
}

initLayout(activity);
initViews(activity, activity.getWindow().getDecorView());

}


private static void initLayout(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView != null) {
int layoutId = contentView.value();
try {
Method method = clazz.getMethod("setContentView", int.class);
method.invoke(activity, layoutId);
} catch (Exception e) {
Log.d(TAG, "Exception Happens->" + e.getMessage());
}
}
}

private static void initViews(Object o, View sourceView) {
Field[] fields = o.getClass().getDeclaredFields();
for (Field field : fields) {
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
int viewId = viewInject.id();
boolean clickable = viewInject.clickable();

if (viewId != -1) {
try {
field.setAccessible(true);
field.set(o, sourceView.findViewById(viewId));
if (clickable) {
sourceView.findViewById(viewId).setOnClickListener((View.OnClickListener) o);
}
} catch (Exception e) {
Log.d(TAG, "Exception Happens->" + e.getMessage());
}
}
}
}
}

}

这样我们只要在Activity所在类上使用ContentView注解,然后再其onCreate方法中ButterKnife.inject()方法,Activity如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@ViewInject(id = R.id.cl_container, clickable = true)
ConstraintLayout clContainer;
@ViewInject(id = R.id.tv_test, clickable = true)
TextView tvTest;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.inject(this);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.cl_container:
Toast.makeText(this, "container clicked", Toast.LENGTH_SHORT).show();
break;
case R.id.tv_test:
Toast.makeText(this, "test clicked", Toast.LENGTH_SHORT).show();
break;
default:
// do nothing
break;
}
}
}

这样一个简单的基于运行时注解的ButterKnife就完成了。接下来看看编译时注解。

编译时注解处理

APT,Annotation Processing Tool,编译时注解处理工具。可以将java源文件或编译后的.class文件作为输入,然后输出另一些文件(可以是.java文件,也可以是.class文件,但通常是.java文件),然后这些生成的.java文件和源文件一起被javac编译。下面先对编译时注解要用到的一些管家类作说明,然后再给出编译时注解的一般处理流程。

基本介绍

Processor

即javax.annotation.processing.Processor,接口中的init方法接收实现ProcessingEnvironment接口的对象,接口中process方法接收实现RoundEnvironment接口的对象,同时还定义了支持处理的选项(通过getSupportedOptions方法)、支持处理的注解类型(通过getSupportedAnnotationTypes方法)、支持处理的最新source version (通过getSupportedSourceVersion方法),同时还提供了一个获取包含Completions的迭代器。

  • init(ProcessingEnvironment processingEnv),接收实现ProcessingEnvironment接口的实例,来初始化注解处理器;
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv),需要注意处理annotations为空的情况;
  • getSupportedOptions(),获取注解处理器支持的选项;
  • getSupportedAnnotationTypes(),获取注解处理器支持的注解类型;
  • getSupportedSourceVersion(),获取注解处理器支持的最新source version;
  • getCompletions(),给ADT返回某个注解建议的Completions集合。

注意,Processor的实现类必须提供一个public的无参数的构造函数,它被ADT用来实例化自定义的Processor。ADT和Processor的实现类按如下方式交互:

  1. 首先ADT在Processor未实例化时,调用Processor实现类的无参数构造函数实例化;
  2. 实例化后,ADT调用实例化对象的init方法,用合适的注解处理环境来完成注解处理的准备工作;
  3. 然后调用getSupportedAnnotationTypesgetSupportedOptionsgetSupportedSourceVersion这三个方法,他们只会在最初调用一次,不是每轮处理都调用;
  4. 在这之后,调用Processor实例的process方法,每一轮注解处理并不会创建新的Processor对象。

上面是标准的Processor接口规范。

[Java SE 6中文文档说明]http://www.cjsdn.net/Doc/JDK60/javax/annotation/processing/package-summary.html

AbstractProcessor

即javax.annotation.processing.AbstractProcessor,设计它的目的就是为了能更好的定义自己的注解处理器,所以在自定义注解处理器的时候,可以继承该类。

RoundEnvironment

即javax.annotation.processing.RoundEnvironment,注解处理工具框架之一,用来提供了每轮注解处理的反馈信息,如是否处理完成processingOver、是否报错errorRaised、注解处理的根元素集getRootElements、获取某种注解类型的元素getElementsAnnotatedWith

ProcessingEnvironment

即javax.annotation.processing.ProcessingEnvironment,注解处理工具框架之一,提供了很多工具类,用来在编译时辅助助理的处理工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface ProcessingEnvironment {

// 获取传给工具框架的置顶处理器选项
Map<String,String> getOptions();

// 获取用来处理错误、警告和通知的Messager
Messager getMessager();

// 获取用来创建源文件、class文件、辅助文件的Filer
Filer getFiler();

// 获取提供操作Element工具的Elements对象
Elements getElementUtils();

// 获取提供操作TypeMirror工具方法的Types对象
Types getTypeUtils();

SourceVersion getSourceVersion();

Locale getLocale();
}
Element

Element指的是静态的、语言级别的构建。一个简单的Java文件,其包含的Elements即包、类、方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.rainmonth;                               // PackageElement

public class UserBean { // TypeElement
private int id; // VariableElement
private int age;
private String name;

@UserMeta(id = 1, name = "Randy", age = 22)
public UserBean() { // ExecutableElement

}

public UserBean(int id, int age, String name) { // 参数name对应于TypeElement
this.id = id;
this.age = age;
this.name = name;
}

...
}
TypeMirror

TypeMirror,代表Java里面的类型,可以是基本类型(如boolean、byte、char、double、float等、声明类型(类类型或接口类型),数组,类型变量和空类型,也代表通配类型参数,可执行文件的签名和返回类型等,TypeMirror值的比较依赖于工具类中的Types提供的方法。可以通过getKind()返回的TypeKind对象来查看,下面看看TypeKind.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public enum TypeKind {
BOOLEAN, // boolean
BYTE, // byte
SHORT, // short
INT, // int
LONG, // long
CHAR, // char
FLOAT, // float
DOUBLE, // double
VOID, // void ->NoType
NONE, // ->NoType
NULL, // null ->NoType
ARRAY, // []
DECLARED, // class or interface
ERROR, // A class or interface type that could not be resolved.
TYPEVAR, // A type variable.
WILDCARD, // A wildcard type argument.
PACKAGE, // A pseudo-type corresponding to a package element.->NoType
EXECUTABLE, // A method, constructor, or initializer.
OTHER, // An implementation-reserved type.(找不到想查找的类型时返回这个值)
UNION, // A union type.(Java 7新增)
INTERSECTION; // An intersection type.(Java 8新增)

/**
* 判断是否基本类型
*/
public boolean isPrimitive() {
switch(this) {
case BOOLEAN:
case BYTE:
case SHORT:
case INT:
case LONG:
case CHAR:
case FLOAT:
case DOUBLE:
return true;

default:
return false;
}
}
}
小结

Element表示构成Java源文件的元素(如包、类、方法等),TypeElement代表源码中的类型元素。

Filer

Filer,从名字上看,在File后加个r,肯定是和文件处理有关的类,其实它就是注解处理器用来创建文件的。

javax.annotation.processing包中的注解

内部定义的注解主要有三个:SupportedAnnotationTypesSupportedOptionsSupportedSourceVersion。显然这三个注解都是为Processor服务的,只有采用SupportedAnnotationTypes才能作为getSupportedAnnotationTypes返回值,其他两个同理。

javax.annotation包中的注解

主要有五个:GeneratedPostConstructPreDestroyResourceResources

  • Generated,表明代码有编译器生成,非用户编写;
  • PostConstruct,依赖注入初始化工作完成后需要调用的方法可以采用该注解;
  • PreDestroy,在注入对象销毁前做释放资源的操作;
  • Resource,表示应用程序所必须的资源;
  • Resources,表示应用程序所必须的一组资源;

注解处理流程

  1. 实现自己Processor,可以继承AbstractProcessor,也可以直接实现Processor;
  2. 打包并注册自己实现的Processor;

下面详细讲解第二部的实现。

打包并注册

要想是自己实现Processor能生效,我们必须让Java编译器能够找到它,这就需要我们手动将其打包并注册:将自定义的处理器生成一个jar文件,并在jar包的META-INF/services路径下创建一个固定的文件javax.annotation.processing.Processor,在javax.annotation.processing.Processor文件中需要填写自定义处理器的完整路径名,有几个处理器就需要填写几个。

注意,从java 6之后,我们只需要将打出的jar防止到项目的buildpath下即可,javac在运行的过程会自动检查javax.annotation.processing.Processor注册的注解处理器,并将其注册上。而java 5需要单独使用apt工具来将其注册。

使用示例

定义两个注解@Print@Code,一个用于打印备注接的类、方法、和Field,另一个用来生成代码,输出作者创建日期,并自动生成文件。

定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Print.java
package com.rainmonth.apt.annotation;
...
/**
* 打印备注接的类、Field和方法
*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface Print {
}
// Code.java
package com.rainmonth.apt.annotation;
...
/**
* 生成代码
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Code {
String author();

String date() default "";
}

编写注解处理器

由于上面存在两个注解,所以要定义两个注解处理器,分别是:PrintProcessorCodeProcessor,内容如下:
PrintProcessor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class PrintProcessor extends AbstractProcessor {
private Messager mMessager;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
// 从ProcessingEnvironment获取Messager
mMessager = processingEnvironment.getMessager();
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (TypeElement te : set) {
for (Element e : roundEnvironment.getElementsAnnotatedWith(te)) {
// 找到注解后执行特定的方法
print(e.toString());
}
}
return true;
}

@Override
public Set<String> getSupportedAnnotationTypes() {
// 支持处理哪些注解
LinkedHashSet<String> annotations = new LinkedHashSet<>();
annotations.add(Print.class.getCanonicalName());
return annotations;
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

private void print(String msg) {
mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
}
}

CodeProcessor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class CodeProcessor extends AbstractProcessor {
private final String SUFFIX = "$RandyInfo";
private Messager mMessager;
private Filer mFiler;
private Types mTypeUtils;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
// 通过ProcessingEnvironment来初始化变量
mMessager = processingEnvironment.getMessager();
mFiler = processingEnvironment.getFiler();
mTypeUtils = processingEnvironment.getTypeUtils();
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (Element e : roundEnvironment.getElementsAnnotatedWith(Code.class)) {
Code code = e.getAnnotation(Code.class);
TypeElement clazz = (TypeElement) e.getEnclosingElement();
try {
generateCode(e, code, clazz);
} catch (IOException ioe) {
mMessager.printMessage(Diagnostic.Kind.ERROR, ioe.toString());
return false;
}
}
return true;
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
LinkedHashSet<String> annotations = new LinkedHashSet<>();
annotations.add(Code.class.getCanonicalName());
return annotations;
}

// 生成代码的关键方法,其实就是字符串拼接
private void generateCode(Element e, Code code, TypeElement clazz) throws IOException {
JavaFileObject f = mFiler.createSourceFile(clazz.getQualifiedName() + SUFFIX);
mMessager.printMessage(Diagnostic.Kind.NOTE, "Creating " + f.toUri());
try (Writer writer = f.openWriter()) {
String pack = clazz.getQualifiedName().toString();
PrintWriter pw = new PrintWriter(writer);
pw.println("package " + pack.substring(0, pack.lastIndexOf('.')) + ";"); //create package element
pw.println("\n class " + clazz.getSimpleName() + "Autogenerate {");//create class element
pw.println("\n protected " + clazz.getSimpleName() + "Autogenerate() {}");//create class construction
pw.println(" protected final void message() {");//create method
pw.println("\n//" + e);
pw.println("//" + code);
pw.println("\n System.out.println(\"author:" + code.author() + "\");");
pw.println("\n System.out.println(\"date:" + code.date() + "\");");
pw.println(" }");
pw.println("}");
pw.flush();
}
}
}

生成jar包并注册

在Android Studio 中很简单,在仙剑的Java Module下的main目录下创建resources目录(有的话就不用创建了),然后再resources目录下创建META-INF.services目录,在里面新建一个名为javax.annotation.processing.Processor文件,在里面加上我们自定义的注解处理器的全路径,如:

1
2
com.rainmonth.apt.processor.PrintProcessor
com.rainmonth.apt.processor.CodeProcessor

加好后,由于我们是利用Android Studio 新建的Java module,可以直接运行gradle的Jar任务来打包,打完包后将在对应module的build文件夹下的libs文件夹下生成jar文件,我们将这个jar文件拷贝到我们主module下的libs目录中,注意,这里我们要采用annotationProcessor的方式来引用这个jar包,即在主module的dependencies闭包中添加:annotationProcessor files('libs/apt.jar'),因为新版本的Android Studio要求必需显示的指定注解处理器。

使用生成的注解

上面的都完事后,就可以在Activity中使用定义的注解了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CompileTimeAnnotationActivity extends AppCompatActivity {

@Override
@Print
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_compile_time_annotation);
process();
}

@Code(author = "Randy", date = "2019/11/15")
private void process() {

}
}

添加好后,直接运行build任务,这个时候可以看到console中会有一些红色的输出,这些输出一般就是我们注解编译时的处理结果了。而且还会发现在我们自定义生成的java源文件

1
2
3
4
// 输出内容(@Pring注解处理结果)
注: onCreate(android.os.Bundle)
// @Code 输出结果
注: Creating file:/Users/randy/AndroidStudioProjects/RandyDemos/AnnotationDemo/app/build/generated/source/apt/debug/com/rainmonth/annotationdemo/compile/CompileTimeAnnotationActivity$RandyInfo.java

总结

重点介绍了编译时注解和运行时注解使用的一般方法,并简单介绍了注解的一些基本概念,给出了两种注解的使用demo,详细阅读之后再去看那些基于注解的框架就不会那么吃力了,而且应该还能灵活运用,利用注解来提高工作效率,体验注解之美。

参考文章