深入理解Java注解
依赖注入是一种较流行的设计模式,在 Android开发中也有很多实用的依赖注入框架,可以帮助我们少写一些样板代码,达到各个类之间解耦的目的
一 注解
从 JDK 5 开始, Java 增加了注解,注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、 开发工具和部署工具可以通过这些补充信息进行验证、处理或者进行部署。
1.1 注解分类
注解分为标准注解和元注解,下面分别介绍它们。
1. 标准注解,标准注解有以下4种。
•@Override:对覆盖超类中的方法进行标记,如果被标记的方法并没有实际覆盖超类中的方法,则编
译器会发出错误警告。
•@Deprecated:对不鼓励使用或者已过时的方法添加注解,当编程人员使用这些方法时,将会在编译
时显示提示信息。
•@SuppressWarnings:指示编译器去忽略注解中声明的警告。
•@SafeVarargs: JDK 7 新增,用来声明使用了可变长度参数的方法,其在与泛型类一起使用时不会出现
类型安全问题。
2. 元注解,除了标准注解,还有元注解,它用来注解其他注解,从而创建新的注解。元注解有以下几种。
•@Targe:注解所修饰的对象范围。
•@Inherited:标记这个注解是继承于哪个注解类
•@Documented:表示这个注解应该被JavaDoc工具记录(即该注解包含在用户文档中)。
•@Retention:用来声明注解的保留策略。
•@Repeatable:JDK 8 新增,允许一个注解在同一声明类型(类、属性或方法)上多次使用。
其中@Targe注解取值是一个ElementType 类型的数组,其中有以下几种取值,对应不同的对象范围。
• ElementType.TYPE:能修饰类、接口或枚举类型。
• ElementType.FIELD:能修饰成员变量。
• ElementType.METHOD:能修饰方法。
• ElementType.PARAMETER:能修饰参数。
• ElementType.CONSTRUCTOR:能修饰构造方法。
• ElementType.LOCAL_VARIABLE:能修饰局部变量。
• ElementType.ANNOTATION_TYPE:能修饰注解。
• ElementType.PACKAGE:能修饰包。
• ElementType.TYPE_PARAMETER:类型参数声明。
• ElementType.TYPE_USE:使用类型。
其中 @Retention 注解有 3 种类型,分别表示不同级别的保留策略。
• RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在.java源码中,源码在编译后,注解信息
被丢弃,不会保留在.class中。
• RetentionPolicy.CLASS:编译时注解。注解信息会保留在.java 源码以及.class 中。当运行 Java 程序时,
JVM 会丢弃该注解信息,不会保留在 JVM 中。
• RetentionPolicy.RUNTIME:运行时注解。当运行 Java 程序时, JVM 也会保留该注解信息,可以通过反 射获取该注解信息。
1.2 定义注解
1.基本定义
定义新的注解类型使用@interface关键字,这与定义一个接口很像,如下所示:
public@interfaceAnnotationDemo{
......
}
定义完注解后,就可以在程序中使用该注解:
@AnnotationDemo
publicclassAnnotationTest{
......
}
2. 定义成员变量
注解只有成员变量,没有方法。注解的成员变量在注解定义中以“无形参的方法”形式来声明,其“方法名”定义了该成员变量的名字,其返回值定义了该成员变量的类型:
public@interfaceAnnotationDemo{
Stringname();
intlength();
}
上面的代码定义了两个成员变量,定义了成员变量后,使用该注解时就应该为该注解的成员变量指定值:
publicclassAnnotationTest{
@AnnotationDemo(naem = "我是注解名字",length = 6)
publicvoidtest(){
......
}
}
也可以在定义注解的成员变量时,使用 default 关键字为其指定默认值,如下所示:
public@interfaceAnnotationDemo{
Stringname()default"我是注解名字";
intlength()default6;
}
因为注解定义了默认值,所以使用时可以不为这些成员变量指定值,而是直接使用默认值:
publicclassAnnotationTest{
@AnnotationDemo
publicvoidtest(){
......
}
}
3. 定义运行时注解
可以用 @Retention 来设定注解的保留策略,这 3 个策略的生命周期长度为 SOURCE < CLASS < RUNTIME。生命周期短的能起作用的地方生命周期长的一定也能起作用。一般如果需要在运行时去动态 获取注解信息,那只能用RetentionPolicy.RUNTIME;如果要在编译时进行一些预处理操作,比如生成一些辅助代码,就用 RetentionPolicy.CLASS;如果只是做一些检查性的操作,比如@Override 和 @SuppressWarnings,则可选用 RetentionPolicy.SOURCE。当设定为 RetentionPolicy.RUNTIME时,这个注解
就是运行时注解,如下所示:
@Retention(RetentionPolicy.RUNTIME)
public@interfaceAnnotationDemo {
Stringname()default"我是注解名字";
intlength()default6;
}
4. 定义编译时注解
同样地,如果将 @Retention 的保留策略设定为 RetentionPolicy.CLASS,这个注解就是编译时注解,如下 所示:
@Retention(RetentionPolicy.CLASS)
public@interfaceAnnotationDemo {
Stringname()default"我是注解名字";
intlength()default6;
}
5. 定义源码时注解
同样地,如果将@Retention的保留策略设定为RetentionPolicy.SOURCE,这个注解就是编译时注解,如下 所示:
@Retention(RetentionPolicy.SOURCE)
public@interfaceAnnotationDemo {
Stringname()default"我是注解名字";
intlength()default6;
}
1.3 注解处理器
如果没有处理注解的工具,那么注解也不会有什么大的作用。对于不同的注解有不同的注解处理器。 虽然注解处理器的编写会千变万化,但是其也有处理标准,比如:针对运行时注解会采用反射机制处理, 针对编译时注解会采用 AbstractProcessor 来处理。本篇就针对前面讲到的运行时注解和编译时注解来编写注解处理器。
1. 运行时注解处理器
处理运行时注解需要用到反射机制。首先我们要定义运行时注解,如下所示:
@Documented
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interfaceGet {
String value()default"";
}
上面的代码定了@Get注解,使用了@Target(ElementType.METHOD),意味着Get注解应用于方法。接下来应用该注解,如下所示:
publicclassAnnotationTest{
@Get(value ="欢迎学习Java注解")
publicString getMessage() {
return"";
}
}
上面的代码为 @Get 的成员变量赋值。接下来写一个简单的注解处理器,如下所示:
publicclassAnnotationProcessor{
publicstaticvoidprocessor(){
Method[] methods = AnnotationTest.class.getDeclaredMethods();
for(Method method:methods) {
Getget= method.getAnnotation(Get.class);
if(get!=null){
System.out.println("get.value() = " +get.value());
}
}
}
}
上面的代码用到了两个反射方法: getDeclaredMethods 和 getAnnotation, 调用 getAnnotation 方法返回指定类型的注解对象,也就是Get 。最后调用 Get 的 value 方法返回从 Get对象中提取元素的值。在MainActivity中调用AnnotationProcessor 的processor()方法,代码如下:
publicclassMainActivityextendsAppCompatActivity{
@Override
protectedvoid onCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AnnotationProcessor.processor();
}
}
输出结果如下:
2. 编译时注解处理器
( 1 )定义注解 这里首先在项目中新建一个Java Library 来专门存放注解,这个 Library 名为 annotations 。接下来定义注
解,如下所示:
@Documented
@Target(value = ElementType.FIELD)
@Retention(value = RetentionPolicy.CLASS)
public@interfaceBindView {
intvalue()default1;
}
上面代码中定义的注解类似于 ButterKnife 的 @BindView 注解。
( 2 )编写注解处理器
我们在项目中再新建一个Java Library 来存放注解处理器,这个 Library 名为 processor 。我们来配置 processor库的 build.gradle:
apply plugin:'java-library'
dependencies{
implementation fileTree(dir:'libs',include: ['*.jar'])
implementationproject(path:':annotaions')
}
sourceCompatibility="1.7"
targetCompatibility ="1.7"
接下来编写注解处理器 ClassProcessor ,它继承 AbstractProcessor ,如下所示:
publicclassClassProcessorextendsAbstractProcessor{
@Override
publicsynchronizedvoidinit(ProcessingEnvironment processingEnvironment){
super.init(processingEnvironment);
}
@Override
publicbooleanprocess(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){
...
returnfalse;
}
@Override
publicSetgetSupportedAnnotationTypes(){
Set annotations =newLinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
returnannotations;
}
@Override
publicSourceVersiongetSupportedSourceVersion(){
returnSourceVersion.latestSupported();
}
}
process 方法的实现会在后文讲到,这里首先分别介绍这 4 个方法的作用。
• init:被注解处理工具调用,并输入 ProcessingEnviroment 参数。 ProcessingEnviroment 提供很多有用的 工具类,比如Elements 、 Types 、 Filer 和 Messager 等。
• process:相当于每个处理器的主函数 main (),在这里写你的扫描、评估和处理注解的代码,以及生成Java 文件。输入参数 RoundEnviroment ,可以让你查询出包含特定注解的被注解元素。
• getSupportedAnnotationTypes:这是必须指定的方法,指定这个注解处理器是注册给哪个注解的。注 意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
• getSupportedSourceVersion:用来指定你使用的 Java 版本,通常这里返回 SourceVersion.latestSupported()。
在 Java 7 以后,也可以使用注解来代替 getSupportedAnnotationTypes 方法和 getSupportedSourceVersion 方 法,如下所示:
@SupportedAnnotationTypes(value ="com.lx.annotaions.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
publicclassClassProcessorextendsAbstractProcessor{
@Override
public synchronized void init(ProcessingEnvironmentprocessingEnvironment) {
super.init(processingEnvironment);
}
@Override
public boolean process(Set set,RoundEnvironmentroundEnvironment) {
...
returnfalse;
}
}
但是考虑到 Android 兼容性的问题,这里不建议采用这种注解的方式。接下来编写还未实现的 process 方法,如下所示:
@Override
publicboolean process(Setset, RoundEnvironment roundEnvironment) {
Messager messager = processingEnv.getMessager();
for(Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
if(element.getKind() == ElementKind.FIELD) {
messager.printMessage(Diagnostic.Kind.NOTE,"printMessage:"
+ element.toString());
}
}
returnfalse;
}
这里用到 Messager 的 printMessage 方法来打印出注解修饰的成员变量的名称,这个名称会在 Android Studio的 Gradle Console 窗口中打印出来。
( 3 )注册注解处理器
为了能使用注解处理器,需要用一个服务文件来注册它,我们 可以使用 Google 开源的 AutoService ,它用来帮助开发者注册注解处理器。首先我们添加该开源库,在processor 的 build.gradle中 直接添加如下代码:
dependencies{
......
implementation('com.google.auto.service:auto-service:1.0-rc2')
}
最后在注解处理器 ClassProcessor 中添加 @AutoService ( Processor.class )就可以了:
@AutoService(Processor.class)
publicclassClassProcessorextendsAbstractProcessor{
......
}
( 4 )应用注解
接下来在我们的主工程项目( app )中引用注解。首先要在主工程项目的 build.graldle中引用 annotations 和processor 这两个库:
dependencies{
......
implementationproject(path:':processor')
implementationproject(path:':annotaions')
}
接下来在MainActivity中应用注解,如下所示:
publicclassMainActivityextendsAppCompatActivity{
@BindView(value =R.id.tv_text)
TextViewtv_text;
@Override
protectedvoid onCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
最后,我们先 Clean Project 再 Make Project ,在 Gradle Console 窗口中打印的结果如图 所示。
可以发现编译时会打印出 @BindView 注解修饰的成员变量名: tv_text 。
这里通过ButterKinfe的@BindView的使用做讲解,
1. 使用运行时注解的方式
(1)定义@BindView注解,代码如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public@interfaceBindView {
intvalue();
}
(2)应用注解,在MainActivity中使用注解,代码如下
publicclassMainActivityextendsAppCompatActivity{
@BindView(value =R.id.tv_text)
TextViewtv_text;
@Override
protectedvoid onCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
(3)定义注解处理器,代码如下
publicclassButterKnife{
publicstaticvoidbind(@Nullable Activity activity){
Field[] fields = activity.getClass().getDeclaredFields();
for(inti =0; i < fields.length; i++) {
Field field = fields[i];
if(field.isAnnotationPresent(BindView.class)){
BindView bindView = field.getAnnotation(BindView.class);
intresId = bindView.value();
View view = activity.findViewById(resId);
field.setAccessible(true);
Class targetType = field.getType();
Class viewType = view.getClass();
if(!targetType.isAssignableFrom(viewType)) {
continue;
}
try{
field.set(activity, view);
}catch(IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
(4)使用,在MainActivity中调用注解处理器完成View的绑定,代码如下
@Override
protectedvoidonCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
tv_text.setText("欢迎学习Java注解");
}
运行程序之后,屏幕显示“欢迎学习Java注解”,并没有报tv_text的空指针异常,说明绑定成功。
由于是运行时处理注解,而且需要用到反射,导致效率不高,因此建议使用编译时注解,下面讲解如果使用编译时注解完成View的绑定。
2. 使用编译时注解的方式,
此处代码接着第一节中第三小节中“2.编译时注解处理器”的代码接着往下研究,
(1)在annotations module 中添加ViewBinder接口,代码如下
publicinterfaceViewBinder {
voidbind(T target);
}
(2)在processor module 中修改ClassProcessor类中的process方法,代码如下
@Override
publicbooleanprocess(Set<?extendsTypeElement> set, RoundEnvironment roundEnvironment) {
// 输出打印工具
Messager messager = processingEnv.getMessager();
// 拿到整个模块中(app)用到BindView的注解的节点
Set<?extendsElement> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
// key -- activity名字
Map> map =newHashMap<>();
for(Element element : elementsAnnotatedWith) {
if(element.getKind() == ElementKind.FIELD) {
// 获取到成员变量的节点,也就是控件
VariableElement variableElement = (VariableElement) element;
// 获取到activity名字
StringactivityName = variableElement.getEnclosingElement().getSimpleName().toString();
messager.printMessage(Diagnostic.Kind.NOTE,"printMessage:"+ activityName);
List variableElements = map.get(activityName);
if(variableElements ==null) {
variableElements =newArrayList<>();
map.put(activityName, variableElements);
}
variableElements.add(variableElement);
}
}
if(map.size() >0) {
Writer writer =null;
Iteratoriterator= map.keySet().iterator();
while(iterator.hasNext()) {
StringactivityName =iterator.next();
// 得到的是activity对应的控件
List variableElements = map.get(activityName);
// 通过空间的成员变量节点,获取到他的上一个节点,也就是类节点
Element enclosingElement = variableElements.get(0).getEnclosingElement();
// 通过类节点获取到包名
StringpackageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
try{
// 生成文件的对象
Filer filer = processingEnv.getFiler();
// 生成java文件
JavaFileObject sourceFile = filer.createSourceFile(packageName +"."
+ activityName +"_ViewBinding");
writer = sourceFile.openWriter();
writer.write("package "+ packageName +";\n");
writer.write("import "+ packageName +"."+ activityName +";\n");
writer.write("import "+ ViewBinder.class.getName() +";\n");
writer.write("public class "+ activityName +"_ViewBinding<T extends "+ activityName +
"> implements ViewBinder<"+ activityName +">{\n");
writer.write(" public void bind("+ activityName +" target){\n");
// 遍历所有成员变量 添加代码
for(VariableElement variableElement : variableElements) {
// 获取到控件的名字
StringvariableName = variableElement.getSimpleName().toString();
// 获取到控件的id
intresId = variableElement.getAnnotation(BindView.class).value();
// 获取到这个控件的类型
TypeMirror typeMirror = variableElement.asType();
writer.write(" target."+ variableName +"=("+ typeMirror +")target.findViewById("+ resId +");\n");
}
writer.write(" }\n}\n");
}catch(IOException e) {
e.printStackTrace();
}finally{
if(writer !=null) {
try{
writer.flush();
writer.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
}
}
returnfalse;
}
然后,我们先Clean Project再Make Project,会生成MainAcitivty_ViewBinding.java文件,内容如下图
(3)在app module中创建ButterKnife.java 文件,调用我们生成的MainActivity_ViewBinding中的bind方法,完成View的绑定,代码如下
publicclassButterKnife{
publicstaticvoidbind(@NullableActivity activity){
String name = activity.getClass().getName() +"_ViewBinding";
try{
Class<?> clazz = Class.forName(name);
ViewBinder binder = (ViewBinder)clazz.newInstance();
binder.bind(activity);
}catch(ClassNotFoundException e) {
e.printStackTrace();
}catch(IllegalAccessException e) {
e.printStackTrace();
}catch(InstantiationException e) {
e.printStackTrace();
}
}
}
(4)最后在MainActivity中使用@BindView注解,然后调用ButterKnife的bind方法,代码如下
publicclassMainActivityextendsAppCompatActivity{
@BindView(value =R.id.tv_text)
TextViewtv_text;
@Override
protectedvoid onCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
tv_text.setText("欢迎学习Java注解");
}
}
运行程序之后,屏幕显示“欢迎学习Java注解”,并没有报tv_text的空指针异常,说明绑定成功。
在这个过程中,我们使用的Filer通过拼接的形式创建的MainActivity_ViewBinding.java文件,这里很容易出错,所以我们接下来使用Javapoet来完成MainActivity_ViewBinding.java文件的生成。
3. 使用Javapoet完成代码生成
JavaPoet是一个动态生成代码的开源项目,在某些情况下具有特殊用处。Github地址:https://github.com/square/javapoet
在Github上有JavaPoet的官方教程,权威且全面,因为太好了
Javapoet的优点
JavaPoet是一款可以自动生成Java文件的第三方依赖
简洁易懂的API,上手快
让繁杂、重复的Java文件,自动化生成,提高工作效率,简化流程,不容易出错
(1) Javapoet的使用,在processor module的build.gradle中添加依赖,代码如下
dependencies{
implementation fileTree(dir:'libs',include: ['*.jar'])
implementationproject(path:':annotaions')
implementation('com.google.auto.service:auto-service:1.0-rc2')
// 引入生成代码的库
implementation'com.squareup:javapoet:1.11.1'
}
(2)在processor module 中修改ClassProcessor类中的process方法,代码如下
@Override
publicbooleanprocess(Set<?extendsTypeElement> set, RoundEnvironment roundEnvironment) {
// 输出打印工具
Messager messager = processingEnv.getMessager();
// 拿到整个模块中(app)用到BindView的注解的节点
Set<?extendsElement> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
// key -- activity名字
Map> map =newHashMap<>();
for(Element element : elementsAnnotatedWith) {
if(element.getKind() == ElementKind.FIELD) {
// 获取到成员变量的节点,也就是控件
VariableElement variableElement = (VariableElement) element;
// 获取到activity名字
StringactivityName = variableElement.getEnclosingElement().getSimpleName().toString();
messager.printMessage(Diagnostic.Kind.NOTE,"printMessage:"+ activityName);
List variableElements = map.get(activityName);
if(variableElements ==null) {
variableElements =newArrayList<>();
map.put(activityName, variableElements);
}
variableElements.add(variableElement);
}
}
if(map.size() >0) {
Writer writer =null;
Iteratoriterator= map.keySet().iterator();
while(iterator.hasNext()) {
StringactivityName =iterator.next();
// 得到的是activity对应的控件
List variableElements = map.get(activityName);
// 通过空间的成员变量节点,获取到他的上一个节点,也就是类节点
Element enclosingElement = variableElements.get(0).getEnclosingElement();
// 通过类节点获取到包名
StringpackageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
ClassName activityClassName = ClassName.bestGuess(activityName);
ClassName viewBuild = ClassName.get(ViewBinder.class.getPackage().getName(), ViewBinder.class.getSimpleName());
//构造一个方法
MethodSpec.Builder method = MethodSpec.methodBuilder("bind")//名称
.addModifiers(Modifier.PUBLIC)//修饰
.returns(void.class) //返回
.addAnnotation(Override.class)
.addParameter(activityClassName,"target", Modifier.FINAL);//参数
// 遍历所有成员变量 添加方法体
for(VariableElement variableElement : variableElements) {
// 获取到控件的名字
StringvariableName = variableElement.getSimpleName().toString();
// 获取到控件的id
intresId = variableElement.getAnnotation(BindView.class).value();
// 获取到这个控件的类型
TypeMirror typeMirror = variableElement.asType();
method.addStatement("target.$L=($L)target.findViewById($L)",variableName,typeMirror,resId);
}
//构造一个类
TypeSpec Class_ViewBinding = TypeSpec.classBuilder(activityName +"_ViewBinding")//名称
.addModifiers(Modifier.PUBLIC)//修饰
.addMethod(method.build())//方法
.addTypeVariable(TypeVariableName.get("T",activityClassName))
.addSuperinterface(ParameterizedTypeName.get(viewBuild,activityClassName))
.build();
//生成一个Java文件
JavaFile javaFile = JavaFile.builder(packageName, Class_ViewBinding)
.build();
try{
//将java写到当前项目中
javaFile.writeTo(System.out);//打印到命令行中
javaFile.writeTo(processingEnv.getFiler());
}catch(IOException e) {
e.printStackTrace();
}
}
}
returnfalse;
}
然后,我们先Clean Project再Make Project,会生成MainAcitivty_ViewBinding.java文件,内容如下图
后续操作跟用Filer生成MainAcitivty_ViewBinding.java文件一样了,这里就不在说明了。
参考
链接:https://blog.csdn.net/lixiong0713/article/details/106549704?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control