Java 注解 (Annotation)
- 初步理解
注解即Annotation:英文翻译成中文的意思是注释、解释、说明的意思。在java中专业名词为注解。
注解可以用来修饰类、方法、属性等,从英文的解释不难看出,注解的作用就是对类、方法、属性的进一步解释说明。
既然是解释那到底是解释给谁的,开发者?编译器?还是运行时代码?
分别举个我们比较熟悉的例子:
- 解释给开发者:
@Override
这个注解应该是大家最熟悉的,子类复写父类方法时,往往会在复写的方法上添加此注解,用于提示开发者,该方法复写自父类。
- 解释给编译器:
熟悉Android的人,有一个开源项目是你一定熟悉的,那就是butterknife,其中最常用的注解就是
@BindView
该注解通过java编译时的注解处理器,自动为注解生成findViewById()代码,从而简化开发者大量书写重复代码的繁琐工作。
- 解释给运行时代码:
移动端通常处理大量的网络数据,而这些数据往往是使用json格式传输的,所以解析json就离不开大名鼎鼎的Google开源工程gson。
Gson中经常会用到的注解就是
@Expose
该字段就是用于标识是否需要将类中的字段打包到json格式中,这个工作就是在运行时gson框架完成的。
通过上面的三种应用场景的解析,相信你已经基本明白java注解的用途和作用。
- 注解的语法
因为平常开发少见,相信有不少的人员会认为注解的地位不高。其实同 classs 和 interface 一样,注解也属于一种类型。它是在 Java SE 5.0 版本中开始引入的概念。现在许多java框架的开发都是基于注解完成的,如butterknife,Dagger2,Eventbus,gson等。
2.1注解定义
注解通过 @interface 关键字进行定义。
public @interface TestAnnotation { } |
这样定义好的注解就可以添加到类上面了。
@TestAnnotation public class Test { } |
元注解
元注解是可以注解到注解上的注解,java内部定义好了5中元注解标签:
@Retention、@Documented、@Target、@Inherited、@Repeatable 。
我们可以把他们认为是用于定义注解的关键字。
@Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Retention 相当于注解盖了一张时间戳,时间戳指明了注解存活的时间周期。
@Retention(RetentionPolicy.RUNTIME) public @interface TestAnnotation { } |
上面的代码中,我们指定 TestAnnotation 可以在程序运行周期被获取到,因此它的生命周期非常的长。他可以在程序运行时通过java的反射机制获取到,而SOURCE和CLASS周期的注解就无法通过反射获取。
@Documented
顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。
@Target
Target 是目标的意思,@Target 指定了注解运用的地方。
当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。
比如只能应用到方法上、类上、方法参数上等等。@Target 有下面的取值
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR 可以给构造方法进行注解
ElementType.FIELD 可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
@Inherited
Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。
说的比较抽象。代码来解释。
@Inherited @Retention(RetentionPolicy.RUNTIME) @interface Test {}
@Test public class A {}
public class B extends A {} |
注解Test被 @Inherited 修饰,之后类A被 Test注解,类B继承A,类B也拥有Test这个注解。
@Repeatable
Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
什么样的注解会多次应用呢?通常是注解的值可以同时取多个。
举个例子,一个人他既是程序员又是产品经理,同时他还是个画家。
@interface Persons { Person[] value(); }
@Repeatable(Persons.class) @interface Person{ String role default ""; }
@Person(role="artist") @Person(role="coder") @Person(role="PM") public class SuperMan{}
|
注意上面的代码,@Repeatable 注解了 Person。而 @Repeatable 后面括号中的类相当于一个容器注解。
什么是容器注解呢?就是用来存放其它注解的地方。它本身也是一个注解。
我们再看看代码中的相关容器注解。
@interface Persons { Person[] value(); } |
按照规定,它里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable 注解过的注解数组,注意它是数组。
2.2注解的属性
注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TestAnnotation { int id(); String msg(); } |
上面代码定义了 TestAnnotation 这个注解中拥有 id 和 msg 两个属性。在使用的时候,我们应该给它们进行赋值。
赋值的方式是在注解的括号内以 value=”” 形式,多个属性之前用 ,隔开。
@TestAnnotation(id=3,msg="hello annotation") public class Test {} |
需要注意的是,在注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组。
注解中属性可以有默认值,默认值需要用 default 关键值指定。比如:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TestAnnotation { public int id() default -1; public String msg() default "Hi"; } |
TestAnnotation 中 id 属性默认值为 -1,msg 属性默认值为 Hi。
它可以这样应用。
@TestAnnotation() public class Test {} |
因为有默认值,所以也可以不在 @TestAnnotation 后面的括号里面进行赋值了。
另外,还有一种情况。如果一个注解内仅仅只有一个名字为 value 的属性时,应用这个注解时可以直接接属性值填写到括号内。
public @interface Check { String value(); } |
上面代码中,Check 这个注解只有 value 这个属性。所以可以这样应用。
@Check("hi") int a; |
这和下面的效果是一样的
@Check(value="hi") int a; |
最后,还需要注意的一种情况是一个注解没有任何属性。比如
public @interface Perform {} |
那么在应用这个注解的时候,括号都可以省略。
@Perform public void testMethod(){} |
需要强调一下,注解可以用来修饰注解:
@Retention(RUNTIME) @Target(ANNOTATION_TYPE)//定义了修饰注解的注解 public @interface ListenerClass {} |
@Retention(CLASS) @Target(METHOD) @ListenerClass public @interface OnLongClick { @IdRes int[] value() default { View.NO_ID }; } |
3.注解的用途
注解就相当于给被他修饰的类、方法或成员变量等添加了一个标签,根据这个标签的存在的周期(Retention)可以有不同的用途。
- RetentionPolicy.SOURCE源码阶段
这个级别的注解不会被加载到编译阶段,故只针对一些编辑器特性。
如@Deprecated:
public class Hero { @Deprecated public void say(){ } } |
这里标识say方法已经过时,如果强制使用,编辑器会有以下行为。
Hero hero = new Hero(); hero. |
其实这都是编辑器行为,是编辑器(Android Studio)本身针对java一些预设的注解做的一些行为定制。我们自己新增这个级别的注解是改变不了编辑器行为的,故这个级别的注解对我们开发意义不大。如果你想定义,其实也就只能给开发者自己看看,编辑器是不会理会你的。
- RetentionPolicy.CLASS编译阶段
这个级别后面详细讲,也是最难的地方,之所以说难,主要是因为平时开发对这一块开发较少,缺乏经验积累。
- RetentionPolicy.RUNTIME 运行阶段
这个界别注解可以被带到jvm中,你可以通过java的反射机制获取到这个注解,并根据注解做相应的处理。
举个例子:
在Gson序列化一个对象时,默认会使用字段的名字作为json中键值对的键,而当我们为对象中字段添加了@SerializedName("test")后,生成的json对应的字段的key就变成了test了。
下面是我自己的应用场景:
App启动时需要加载配置,并将配置文件中的信息加载到指定的指定对象中。
//类定义 public class AppInfo { @SerializedName("appId") public String appId = ""; @SerializedName("authKey") public String appKey = ""; } //通过反射加载对象,clazz传入的就是AppInfo.class //这个方法是伪代码,只为说明问题 public static <T> T loadObjectFromConfigFile(Class<T> clazz) { //创建反射类的对象实例 T object = clazz.newInstance(); //加载配置文件 Properties properties = new Properties(); properties.load(inputStream); //获取类中所有字段 Field[] fieldArray = clazz.getDeclaredFields(); for(Field field : fieldArray) { //获取字段上SerializedName类型的注解 SerializedName serializedName = field.getAnnotation(SerializedName.class); if(serializedName != null) { //存在注解,则以注解的值为配置文件的key,读取配置文件中的值 String value = properties.getProperty(serializedName.value(), null); if(value != null) { if(field.getType() == String.class) { field.setAccessible(true); //将值赋值给对象 field.set(object, value); } } } } return object; } |
上面例子中是从配置文件中读取值,然后赋值给对象,这个过程类似Gson从json数据中读取值,然后赋值给对象的过程。
4. 编译阶段注解详解
4.1 注解处理器简介
注解处理器(Annotation Processor)是javac内置的一个用于编译时扫描和处理注解(Annotation)的工具。简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件内注解(Annotation)相关内容。
由于注解处理器可以在程序编译阶段工作,所以我们可以在编译期间通过注解处理器进行我们需要的操作。比较常用的用法就是在编译期间获取相关注解数据,然后动态生成.java源文件(让机器帮我们写代码),通常是自动产生一些有规律性的重复代码,解决了手工编写重复代码的问题,大大提升编码效率。
比较典型的开源项目有:
butterknife,Dagger2,EventBus,Retrofit等
4.2注解处理器使用
前面我们已经懂得了注解的基本语法,下面介绍下注解配合注解处理器的用法。
1.创建一个android测试工程
打开android studio 3.+
创建一个android空的hello world工程。
2.创建注解java lib。
点击工程,右键创建一个java lib。
创建完成后,打开MyBindView.java文件,创建一个自己用于测试的注解类
@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface MyBindView { int value() default -1; } |
实际项目中你可能需要在当前模块的gradle文件中添加如下两个依赖,该测试项目中不需要。
compileOnly "com.google.android:android:4.1.1.4"//android jar 包
api "com.android.support:support-annotations:27.0.2"//android 注解包
3.创建注解处理器java lib
跟创建注解lib步骤相似,这里创建的模块名为annotation-processor,类名为MyProcessor。
创建完成后如下图:
在gradle文件中添加如下依赖
//注解解析器依赖注解包 implementation project(':annotation-lib') //注解解析用到的工具包 implementation "com.google.auto:auto-common:0.10" //一个开源工程,用于生成java代码 api "com.squareup:javapoet:1.10.0" //auto service注解包,用于向jvm注册注解处理器 compileOnly "com.google.auto.service:auto-service:1.0-rc4" //解析注解中R.id.xxx使用到的工具包 compileOnly files(org.gradle.internal.jvm.Jvm.current().getToolsJar()) |
下面实现MyProcessor.java
想要在编译时注册注解处理器,MyProcessor类必须要继承
javax.annotation.processing.AbstractProcessor
且要在该类上添加@AutoService(Processor.class)注解。
@AutoService(Processor.class) :向javac注册我们这个自定义的注解处理器,这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。
4. AbstractProcessor方法介绍
1.init(ProcessingEnvironment env):每个Annotation Processor必须有一个空的构造函数。编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnviroment参数,通过该参数可以获取到很多有用的工具类: Elements , Types , Filer **等等
2.process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv):Annotation Processor扫描出的结果会存储进roundEnv中,可以在这里获取到注解内容,编写你的操作逻辑。注意,process()函数中不能直接进行异常抛出,否则的话,运行Annotation Processor的进程会异常崩溃,然后弹出一大堆让人捉摸不清的堆栈调用日志显示.
3.getSupportedAnnotationTypes(): 该函数用于指定该自定义注解处理器(Annotation Processor)是注册给哪些注解的(Annotation),注解(Annotation)指定必须是完整的包名+类名(eg:com.example.MyAnnotation)
举个简单例子:
MyBindView.class.getCanonicalName()
这句代码获取的就是完成包名加类名。
注意,如果不通过此方法告诉jvm你的注解处理器处理哪些注解,则jvm不会回调process方法。
4.getSupportedSourceVersion():用于指定你的java版本,一般返回:SourceVersion.latestSupported()。当然,你也可以指定具体java版本:
return SourceVersion.RELEASE_7;
- 给demo添加依赖
要使用这个注解处理器,必须在demo的gragle文件中添加依赖。
普通库使用implementation,注解处理器要使用annotationProcessor关键字(之前是使用apt,现在google已经废弃第三方apt插件,改为统一使用annotationProcessor)。
dependencies { implementation project(':annotation-lib') annotationProcessor project(':annotation-processor') } |
在代码代码中使用自己定义的MyBindView注解
public class MainActivity extends AppCompatActivity { @MyBindView(123) TextView textView;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } |
4.3调试注解处理器
1.添加日志信息
注解处理是在编译阶段完成的,故无法使用logcat查看,想要添加log信息必须使用注解处理器提供的log类,Message,该类通过AbstractProcessor的init参数ProcessingEnvironment中获取。
日志方法使用:
private void log(String msg) { mMessager.printMessage(Diagnostic.Kind.NOTE, msg); } |
注意Error级别的日志会导致编译器编译失败。
通过上面的方法,再点击菜单栏Build ->Rebuild Preject重新编译程序,就可以在Message窗口看到日志输出了。
注意Android studio 3.1以上 把Message窗口 与 buid 合并了,
默认不会显示原来message的信息,如果想查看原来版本的message信息,
只需要 打开下图这个开关
2.断点调试
断点调试第一步,添加remote调试模块
在菜单栏中点击Run->Edit Configurations
名字自己起一个,这里使用的是test,其他参数都不要改动,注意将1处的内容拷贝下来,然后点击ok
断点调试第二步配置jvm参数,
这一步有两种方式可用,如果你遇到一种配置方式无效则可以尝试使用另一种方式
方式一:
从上图可以看出,我们项目中目前有3个模块,注解定义模块(annotation-lib)、注解处理模块(annotation-processor)和应用模块(app)。
注解实际是使用在app层面的,点击as右侧的gradle窗口,选择app->other->compileDebugJavaWithJavac->右键Create Configuration。
图中VM options填入上一步拷贝的参数。
方式二:
在工程根目录中找到gradle.properties文件,
在文件中添加如下配置,其中jvmargs就是上一步拷贝的内容
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 org.gradle.daemon=true |
注意,如果该配置文件中已经存在org.gradle.jvmargs配置,则将上面jvm参数添加到已有配置后面即可,注意使用空格隔开,如下图:
第三步,添加断点
跟普通java程序添加断点一样,点击代码左侧面板就能添加和取消断点了
断点调试第三步,开启远端调试端口
点击1处,然后选择第一步创建的remote调试模块2,然后点击3处的debug按钮。
此时4处状态如图,代表启动成功。
如下图则是启动成功
否则可能会看到如下错误:
Error running 'test': Unable to open debugger port (localhost:5005): java.net.ConnectException "Connection refused: connect"
遇到这个错误你需要检查如下几个地方:
1.在windows下使用netstat -ano|findstr 5005命令也没有看到被其他程序占用端口,
2. 检查一下android studio是否开启别的工程,并且在别的工程启用了此端口(就是别的工程有没有使用java的5005端口开启远程调试服务)
3. 如果1、2检查没有问题,那咨询一下公司IT是否禁用5005端口。
4.如果1、2、3都没有问题,那恭喜你,极有肯能是防火墙万岁。
打开防火墙高级设置,在入站规则中添加java.exe的入站权限,如下图,java.exe位于android studio安装目录\jre\bin\java.exe
新建规则->程序->选择android studio安装目录下的java.exe:
5.如果上面4中方法都试过了,那你只能祈祷一分钟,上帝保佑,重启一下电脑试试吧。
6.重启以后还不行,是的,换个端口再试试(比如将端口换成50005,注意步骤一和步骤二的端口要保持一致)。
断点调试第四步:重新编译程序
经过上面的步骤,远程调试模块创建好了,java虚拟机启动参数设置完成,调试服务启动成功,那最后一步就是编译程序了:
点击菜单栏Build->Rebuild Project就可以进入断点了。
如果还是不行,则点击Build->Clean Project先清空一下工程,然后再Rebuild Project。
如果遇到下图中GBK的不可映射字符错误,
则需要再annotation-processor模块的gradle文件中添加java编译字符集配置。
tasks.withType(JavaCompile) { options.encoding = "UTF-8" } |
5.实现一个简单demo
5.1.使用自定义注解demo代码
此处demo主要是在4.2注解处理器使用篇幅的基础上,在MyProcessor中实现解析MyBindView注解并生成绑定类,然后在app模块中实现MyBindViewUtils实现绑定。
下面是生成的绑定类MainActivity_MyViewBinding.java
package com.example.guozr.annotationtest;
import android.support.annotation.UiThread;
public class MainActivity_MyViewBinding { @UiThread public MainActivity_MyViewBinding(MainActivity target) { target.textView = (android.widget.TextView) target.findViewById(R.id.textView_hello_world); } } |
MyBindViewUtils.java代码
import android.app.Activity;
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException;
public class MyBindViewUtils { public static void bind(Activity activity) { Class clazz = activity.getClass(); //利用java反射反射生成的类 Class<?> bindingClass = clazz.getClassLoader().loadClass(clazz.getName() + "_MyViewBinding"); //反射构造方法并执行,构造方法中实现自动查找和赋值功能 Constructor<?> constructor = bindingClass.getConstructor(clazz); constructor.newInstance(activity); } |
在MainActivity中使用写好的工具类
public class MainActivity extends AppCompatActivity {
//添加自动绑定注解 @MyBindView(R.id.textView_hello_world) TextView textView;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //调用绑定方法 MyBindViewUtils.bind(this); //测试一下是否生效 textView.setText("bind view Sucess"); } } |
最后运行结果是ok的。
通过上面代码可以看出,反射调用和使用自定义注解都很简单,关键是自动生成的绑定类MainActivity_MyViewBinding.java的生成过程。
5.2绑定类生成
注解的解析主要是在我们自定义的MyProcessor中完成的,下面介绍下复写的部分方法
@Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); //Filer,java文件生成后写入的地方 mFiler = processingEnvironment.getFiler(); //gradle运行日志记录的帮助类 mMessager = processingEnvironment.getMessager(); //解析注解的辅助类 mElementUtils = processingEnvironment.getElementUtils(); //jvm编译环境的一些选项,可以自己添加 mOptions = processingEnvironment.getOptions(); try { //获取类编译时原始数据的类,比如@MyBindView(R.id.text) //我们正常只能获取id的整型值,要想获取原始R.id.text //这个字符串,就必须使用到该类 trees = Trees.instance(processingEnv); } catch (IllegalArgumentException ignored) { } logi("MyProcessor init ..."); } |
@Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotations = new LinkedHashSet<>(); //返回包含自定义注解的集合,否则jvm不会掉process方法 annotations.add(MyBindView.class.getCanonicalName()); return annotations; } |
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { for (Element element : roundEnvironment.getElementsAnnotatedWith(MyBindView.class)) { try { //所有注解转到parseBindView方法执行 parseBindView(element); } catch (Exception e) { mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage()); } } return false; } |
private void parseBindView(Element element) { //获取@Mybindview修饰的成员变量的修饰符 Set<Modifier> modifiers = element.getModifiers(); //如果是不可访问或不可改变的成员变量,则报错 if(modifiers.contains(Modifier.PRIVATE) or modifiers.contains(Modifier.FINAL)) { loge("不合法的修饰符"); } //获取拥有该成员的类元素 TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); //如果不是类的成员变量则报错 if(enclosingElement.getKind() != ElementKind.CLASS) { loge("不合法的成员"); } //如果类是私有的,则报错 if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) { loge("不合法的类访问属性"); } TypeMirror elementType = element.asType(); //TypeKind.DECLARED代表java语言中定义的类或接口类型 if(elementType.getKind() != TypeKind.DECLARED) { loge("MyBindView只修饰成员变量"); }
DeclaredType declaredType = (DeclaredType) elementType;
//解析完了注解,接下来使用javapoet生成java文件 //javapoet使用请参考github上介绍:https://github.com/square/javapoet //各个类的含义建议参考jdk文档java.compiler模块 //获取包名 String packageName = MoreElements.getPackage(element).getQualifiedName().toString(); logi("packageName : " + packageName); String className = enclosingElement.getQualifiedName().toString().substring( packageName.length() + 1).replace('.', '$'); logi("className : " + className);
//被注解标识的字段的名称 String name = element.getSimpleName().toString(); logi("element name : " + name);
//被注解标识的字段的类型(如android.view.TextView) TypeName type = TypeName.get(elementType); logi("element type : " + type);
//自己根据注解所在类的类名创建一个新的类 ClassName bindingClassName = ClassName.get(packageName, className + "_MyViewBinding");
//生成java类 TypeSpec.Builder classBuilder = TypeSpec.classBuilder(bindingClassName.simpleName()) .addModifiers(Modifier.PUBLIC);
Id myBindViewId = elementToId(element, MyBindView.class, element.getAnnotation(MyBindView.class).value());
//生成构造方法 MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder() .addAnnotation(ClassName.get("android.support.annotation", "UiThread")) .addModifiers(Modifier.PUBLIC) .addParameter(TypeName.get(enclosingElement.asType()), "target") //构造方法中添加findViewById代码 .addStatement("target.$L = ($L) target.findViewById($L)", name, type.toString(), myBindViewId.code);
//将构造方法添加到类中 classBuilder.addMethod(constructorBuilder.build()); //将类写入java文件中 JavaFile javaFile = JavaFile.builder(packageName, classBuilder.build()).build(); try { javaFile.writeTo(mFiler); } catch (IOException e) { loge("Unable to write binding for type " + enclosingElement.getSimpleName() + ", error : " + e.getMessage()); }
} |
最后将代码工程在这里,大家可以自己下载运行
https://download.csdn.net/download/guozhongrui000/10658885