直到写这篇博客之前,其实我和很多同学都一样,听过注解,看过别人用注解,第三方框架到处都是注解。然而如果有人问我注解是什么,怎么用,用来干嘛的。我只能恨不得摇断脖子,就更加不用说能在工作中使用注解了,想研究像EventBus等第三方框架时,一遇到注解前功尽弃。所以说到底还是底子差,不懂注解的程序员成不了好的架构师。二话不说学吧。
一、注解的定义和使用
(1)注解概念定义
这里就不贴官方的定义了(对我们这种小白来说难懂的狠,都是用专业名词来解释专有名词,看的人云里雾里的烦得很)。其实我们可以将注解理解成为标签。如果把代码想象成一个具有生命的个体,注解就是给这些代码的某些个体打标签。就像我们在上学时,给有些老师打的个性绰号一样,老师还是那个老师,只是多了一个绰号,但是这个绰号又会跟随着这位老师,甚至会一届届传下去。代码也是一样通过注解打上标签,不影响代码本身的执行,但是会在注解的生命周期内一直跟随这段代码。
(2)如何自定义注解
如何自己定义一个注解?自定义注解需要注意些什么?
注解通过 @interface关键字进行定义。
public @interface DaggerTest {.....};
它的形式跟接口很类似,不过前面多了一个 @ 符号。上面的代码就创建了一个名字为 DaggerTest 的注解。 你可以简单理解为创建了一张名字为 DaggerTest 的标签。现在注解也自定义好了,那怎么用这个注解呢?
@DaggerTest
public class DaggerMain {
}
创建一个类 DaggerMain ,然后在类定义的地方加上@DaggerTest 就可以用 DaggerTest 注解这个类了
你可以简单理解为将 DaggerTest 这张标签贴到 DaggerMain 这个类上面。
咦?写的啥玩意,这就结束啦?这玩意这么简单?好像什么用也没有啊,我学他干嘛。当然不会这么简单,上面只是告诉大家如何创建一个最简单的(啥用也没有的)注解,然后在代码里面使用起来。但是在开发过程中,注解的定义和使用远没有这么简单。我们来一点点的带大家见识它的魅力和难度。
二、注解的生命周期和作用域
在Java中除了类对象具有生命周期外,其实注解同样具有生命周期。那么一个注解的生命周期是怎样来定义的呢?
(1)元注解
要弄明白一个自定义注解的生命周期和作用域,就必须要弄明白什么是 "元注解"。这个又是个什么东西呢,顾名思义元注解就是最原始的注解,是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。如果难于理解的话,你可以这样理解。元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的注解标签进行解释说明的。
元注解有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。
- @Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间(即生命周期)。
它的取值如下:
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们
因为除了元注解,自定义注解一般都是用在类上的,所以它的生命周期又和Java类息息相关,如下图:
- @Target
Target 是目标的意思,@Target 指定了注解运用的地方 你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。 类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就有了限制了,比如只能张贴到方法上、类上、方法参数上等等(即作用域)。@Target 有下面的取值
- ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
- ElementType.CONSTRUCTOR 可以给构造方法进行注解
- ElementType.FIELD 可以给属性进行注解
- ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
- ElementType.METHOD 可以给方法进行注解
- ElementType.PACKAGE 可以给一个包进行注解
- ElementType.PARAMETER 可以给一个方法内的参数进行注解
- @Documented
顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举。由于我们在开发中 接触这种很少,所以这里就不做过多讲解了。
- @Inherited
Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解,比如父类A被注解AnnotionTest注解了,而这个AnnotionTest是一个被@Inherited注解过的普通注解,那么A的子类B就自动继承了这个AnnotionTest注解。
需要注意的是一个自定义注解可以同时被多个元注解注解,如下:
/**
*同时被两个以上元注解进行注解
*
**/
@Inherited
@Retention( RetentionPolicy.RUNTIME)
public @interface DaggerTest {
}
- @Repeatable
Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
什么样的注解会多次应用呢?通常是注解的值可以同时取多个。这句话理解起来好像有点难哈,没关系我们通过代码来解释。
@Retention(RetentionPolicy.RUNTIME)
@interface Persons{
Person[] value();
}
@Repeatable(Persons.class)//这里使用@Repeatable来支持注解重复
@Retention(RetentionPolicy.CLASS)
@interface Person{
//每个人都有不同的属性,比如高的,矮的、胖的、瘦的
String field() default "";
}
/**
* 这里的Hunman我们就可以重复使用Person进行注解,但是这里需要注意的是,这个Person注解的元注解有一个是@Repeatable,所以需要在Gradle中添加如下一句增加JDK8支持
* compileOptions {
* sourceCompatibility = '1.8'
* targetCompatibility = '1.8'
* }
*如果没有上面这句JDK8支持,是编译不通过的。因为@Repeatable 是 Java 1.8 才加进来新特性
*/
@Person(field = "高的")
@Person(field = "矮的")
@Person(field = "判读")
@Person(field = "的的")
public class Hunman {
private String name = "名字";
}
(2)、注解的属性
上面例子的Person注解里面有一个String类型的field是我们自己定义。这个field就是我们说的注解属性,注解的属性也叫做成员变量。注解只有成员变量,没有方法。需要注意的是,在注解中定义属性时它的类型只能是(byte、short、char、int、long、float、double、boolean) 8 种基本数据类型外加String、Enum、 类(Class)、接口(Interface)、注解(annotations)以及这些类型的数组。 注解中属性必须有默认值,默认值需要用 default 关键值指定。
(3)、注解的提取
注解的提取这里需要涉及到前面反射的知识,如果对反射一点都不懂的同学可以移步到反射篇先了解Java的反射机制。
- 注解通过反射获取。首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
- 然后通过 getAnnotation() 方法来获取 Annotation 对象。
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
- getAnnotations() 方法获取所有注解列表。
public Annotation[] getAnnotations() {}
前一种方法getAnnotation()返回指定类型的注解,后一种方法getAnnotations()返回注解到这个元素上的所有注解。
- 如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了。比如
public static void main(String ... args){
/**
* 这里有个狠有意思的现象,那就是因为Person是被@Repeatable注解过的注解,
* 所以当Hunman只被Person注解一次的时候下面代码段一运行起来会打印出结果,
* 但是如果Hunman像上面的代码一样被Person注解过多次,这时候再执行代码段一就不会再输出结果啦。
* 这时候需要换成提取的注解对象为Persons啦,而不是Person。如下代码段二
*/
//代码段一
Class<?> clazz = Hunman.class;
boolean hasAnnotation = clazz.isAnnotationPresent(Person.class);
if(hasAnnotation){
Person person = clazz.getAnnotation(Person.class);
System.out.println("field:"+person.field());
}
//代码段二
boolean hadAnnotation = clazz.isAnnotationPresent(Persons.class);
if(hadAnnotation){
Persons persons = clazz.getAnnotation(Persons.class);
for (Person person:persons.value()){
System.out.println("field2 :"+person.field());
}
}
}
- 为什么要提取注解,它有什么用处
用过EventBus或者自己写过框架的朋友,应该就知道为什么要提取注解了。利用注解我们能干下面这些事
- 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
- 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理,这里最经典的就是butterknife框架了。
- 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取,就如同EventBus一样,注册后,通过遍历所有@Subscribe注解的函数,然后将事件(EventType)、函数(Method)与所在类(Class)缓存下来,当有事件发送过来时,再通过反射调用所有注册了该事件的注解函数。
值得注意的是,注解不是代码本身的一部分。
三、注解在EventBus中的应用
使用过EventBus的同学一定很熟悉这个框架,因为它使用起来很简单,只需要调用几个主要方法就能够帮助我们实现各页面之间的数据通信。这里我们就通过解读源码的方式来学习学习EventBus是如何通过注解来实现组件间的通信的。
(1)、EventBus的注册 register
我们直接来看它的注册源码吧
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
/**
* 这里的subscriberMethodFinder.findSubscriberMethods就是调用反射来提取当前Class中所有被@Subscribe注解的注解函数
*/
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
/**
* 取到这些注解函数后调用subscribe方法,实际上这个方法就是将事件、注解函数与注册的Class类进行绑定,并缓存到内存中
*/
subscribe(subscriber, subscriberMethod);
}
}
}
我们再来看看这个subscribe方法是不是像我们说的那样。
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
Class<?> eventType = subscriberMethod.eventType;//这个就是我们注册的Event类型,当有事件发过来时,会去通过这个类型去找到所有注册了该事件的Subscription。
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);//每一个Subscription对应的就是一个被@Subscribe注解的函数
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);//先从缓存中通过EventType查找已经存在的Subscription表
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);//Event做为Key,注解函数表做为Value缓存到Map结构中,用于后面post事件时查询操作
} else {
if (subscriptions.contains(newSubscription)) {//判断这个注解函数是否已经被注册过
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {//根据优先级重排序,将注解函数缓存的列表中
subscriptions.add(i, newSubscription);
break;
}
}
/**
* ..............这里还有一些如粘性事件缓存等逻辑,这里就不再解释了....................
*
*
* 判断是否是粘性注解函数
*/
if (subscriberMethod.sticky) {
if (eventInheritance) {
//如果是粘性注解函数,从粘性事件列表中查找是否有对应的粘性事件,如果有立马执行,
// 这里checkPostStickyEventToSubscription方法最终就是通过反射调用该注解函数。这里就不再贴源码解释了
Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
for (Map.Entry<Class<?>, Object> entry : entries) {
Class<?> candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}
(2)EventBus的事件发送 post
注册完,就该发送事件,进行组件间通信啦。
public void post(Object event) {
PostingThreadState postingState = currentPostingThreadState.get();
List<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);//将事件加入发送队列
if (!postingState.isPosting) {//如果发送队列不在工作直接发送
postingState.isMainThread = isMainThread();
postingState.isPosting = true;
if (postingState.canceled) {
throw new EventBusException("Internal error. Abort state was not reset");
}
try {
while (!eventQueue.isEmpty()) {
postSingleEvent(eventQueue.remove(0), postingState);//这里调用postSingleEvent方法会通过发送的Event查找注册表,最终循环反射执行各注册函数
}
} finally {
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}
所有的事件发送后一旦查到该事件有被注册的注解函数,最终都会执行下面的这段代码。
void invokeSubscriber(Subscription subscription, Object event) {
try {
//这里就是我们反复说的最终通过反射的方式调用注解函数,完成事件的传递
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}
(3)EventBus的解注册 unregister
有注册就必然有解注册,否者一个组件生命周期结束了,但是依然在注册状态,除了会降低效率外还有可能会造成不必要的泄漏等。
public synchronized void unregister(Object subscriber) {
/**
* 看,这里的解注册就狠简单明了啦
* 先通过Class类找到当前类所有的注册事件EventType
* 然后再通过EventType查找注解函数表,然后从注解函数表中移除当前类的注解函数
*/
List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
if (subscribedTypes != null) {
for (Class<?> eventType : subscribedTypes) {
unsubscribeByEventType(subscriber, eventType);
}
typesBySubscriber.remove(subscriber);
} else {
logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
}
}
private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
/**
* 通过EventType查找注解函数表
*/
List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions != null) {
int size = subscriptions.size();
for (int i = 0; i < size; i++) {
Subscription subscription = subscriptions.get(i);
if (subscription.subscriber == subscriber) {//移除当前类的注解函数
subscription.active = false;
subscriptions.remove(i);
i--;
size--;
}
}
}
}
好啦,注解的基本使用和原理、EventBus的原理就这样讲完啦。是不是从只会用不知其所以然的状态一下子清晰了很多,甚至感觉自己也能写一个像EventBus这样的框架?后面将会在注解的基础上我们学习注解更加高级的使用和原理:依赖注入框架Dagger。