Java注解在我们项目开发 中是非常常见的。比如经常用到的几种java内置的注解:
@Override,表示当前的方法定义将覆盖超类中的方法。
@Deprecated,表示当前方法即将废弃,不推荐使用。
@SuppressWarnings,表示忽略编译器的警告信息。
对于上面几个注解想必大家都不会陌生。除此之外,我们还经常在一些第三方框架中看到一些自定义注解。比如大名鼎鼎的ButterKnife和EventBus都是基于注解实现的。网上关于注解的文章数不胜数,但是,很多章都是贴下注解的定义,然后解释下几种元注解,扔出一个自定义注解的例子就不了了之了。刚接触注解的时候,看了半天注解相关的文章也没弄懂注解到底有什么用。其实注解往往是需要结合反射来用的,离了反射,注解也就失去了灵魂。因此,本篇文章我们会先来学习一些注解相关的一些基础知识,然后结合反射来实现一个与ButterKnife一样功能的实例。当然ButterKnife的实现并非是用反射而是使用注解处理器(AnnotationProcessor)来实现的,但是,本篇文章重点是自定义注解,因此,我们就用注解结合反射来模仿ButterKnife的效果。
1.注解(Annotation)的声明
同类(class)与接口(interface)一样,注解( @interface)也是一种定义类型,它是在JDK 5.0中引入的。我们可以通过@interface来声明一个注解:
public @interface MAnnotation {
}
2.注解的成员变量
注解与类一样,都存在成员变量。与类的区别是注解中没有方法。因此,来看下如何在注解中声明成员变量。
public @interface MAnnotation {
string name();
int age();
}
上述示例中我们在MAnnotation中声明了一个String类型的name和一个int类型的age的成员变量。除此之外,我们还可以为成员变量制定默认值:
public @interface MAnnotation {
string name() default "Jack";
int age() default 18;
}
如果注解的成员变量被赋予了默认值,那么使用注解时可以不为成员变量赋值,而是用直接使用默认值。
3.注解的分类
根据注解是否包含成员变量,可以把Annotation分为如下两类:
(1)标记注解 标记注解指的时没有包含成员变量的注解,例如java内置的注解@Override注解。
(2)元数据注解 元数据注解指的是包含成员变量的注解,这类注解可以接受更多的元数据。例如,ButteerKnife的@BindView注解
4.元注解
元注解可以理解为注解的注解。用来提供对给其他的注解类型类型做说明。JDK中提供了如下4个元注解:
@Target
@Retention
@Documented
@Inherited
针对以上四种注解,我们来分别解析
(1)@Target注解
指定Annotation用于修饰哪些程序元素。@Target也包含一个名为”value“的成员变量,该value成员变量类型为ElementType[ ],ElementType为枚举类型,值有如下几个:
ElementType.TYPE:能修饰类、接口或枚举类型
ElementType.FIELD:能修饰成员变量
ElementType.METHOD:能修饰方法
ElementType.PARAMETER:能修饰参数
ElementType.CONSTRUCTOR:能修饰构造器
ElementType.LOCAL_VARIABLE:能修饰局部变量
ElementType.ANNOTATION_TYPE:能修饰注解
ElementType.PACKAGE:能修饰包
举个栗子,用FIELD和METHOD来作为Target的value,那么MAnnotation 就只能用来修饰类的成员变量和方法
@Target(ElementType.FIELD,, ElementType.METHOD)
public @interface MAnnotation {
string name() default "Jack";
int age() default 18;
}
(2)@Retention
这个注解定义了该注解可以保留多长时间。某些注解仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
@Retention同样包含一个名为“value”的成员变量,该value成员变量是RetentionPolicy枚举类型。使用@Retention时,必须为其value指定值。value成员变量的值只能是如下3个:
RetentionPolicy.SOURCE:Annotation只保留在源代码中,编译器编译时,直接丢弃这种Annotation。
RetentionPolicy.CLASS:编译器把Annotation记录在class文件中。当运行Java程序时,JVM中不再保留该Annotation。
RetentionPolicy.RUNTIME:编译器把Annotation记录在class文件中。当运行Java程序时,JVM会保留该Annotation,程序可以通过反射获取该Annotation的信息。
@Retention举个栗子:
@Target(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MAnnotation {
string name() default "Jack";
int age() default 18;
}
(3)@Documented
@Documented是一个标记注解,如果定义注解MAnnotation ,使用了@Documented修饰定义,则在用javadoc命令生成API文档后,所有使用注解MAnnotation 修饰的程序元素,将会包含注解MAnnotation 的说明。举了这么久的栗子,也挺累,这个就不举了吧。。。
(4)@Inherited
@Inherited是一个标记注解,指定注解具有继承性。要注意的是它并不是说注解本身可以继承,而是说如果一个父类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了父类的注解。这次我们还是需要举个栗子的:
Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface MAnnotation {}
@MAnnotation
public class ClassA{}
public class ClassB extends ClassA {}
注解 @MAnnotation 被 @Inherited 修饰,ClassA 被 MAnnotation 注解,ClassB 继承 ClassA,那么此时ClassB也拥有@MAnnotation 注解。
说了这么久,关于注解的相关基础知识终于讲完了。但是,即使看到这里不知道小伙伴是否仍然会迷茫,注解到底有什么用?在文章开头我们就提到离开反射的注解是没有灵魂的,因此,正是因为反射才赋予了注解实质的用途。那么接下来,我们用注解和反射来模仿并实现ButterKnife的功能吧。
首先,来分析一下要实现的功能。
使用注解注入布局文件省去setContentView
使用注解省去findViewById
使用注解省去setOnClickListener
**1.根据上述需求,我们可以定义三个注解。**如下:
// 给Activity注入布局文件的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectLayout {
int value() default -1;
}
// 查找控件ID的注解
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface BindView {
int value() default -1;
}
// 给View设置监听事件的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
int[] value();
}
2.声明BindProcessor类通过反射处理以上注解
// 处理@InjectLayout
private static void injectLayout(Activity activity) {
Class> activityClass = activity.getClass();
if (activityClass.isAnnotationPresent(InjectLayout.class)) {
InjectLayout mId = activityClass.getAnnotation(InjectLayout.class);
int id = mId.value();
try {
Method method = activityClass.getMethod("setContentView", int.class);
method.setAccessible(true);
method.invoke(activity, id);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
// 处理@BindView
private static void bindView(Activity activity) {
Class> activityClass = activity.getClass();
Field[] declaredFields = activityClass.getDeclaredFields();
for (Field field : declaredFields) {
if (field.isAnnotationPresent(BindView.class)) {
BindView mId = field.getAnnotation(BindView.class);
int id = mId.value();
try {
Method method = activityClass.getMethod("findViewById", int.class);
method.setAccessible(true);
Object view = method.invoke(activity, id);
field.setAccessible(true);
field.set(activity, view);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
// 处理@OnClick
private static void bindOnClick(final Activity activity) {
Class> cls = activity.getClass();
Method[] methods = cls.getMethods();
for (int i = 0; i < methods.length; i++) {
final Method method = methods[i];
if (method.isAnnotationPresent(OnClick.class)) {
OnClick mOnclick = method.getAnnotation(OnClick.class);
int[] ids = mOnclick.value();
for (int j = 0; j < ids.length; j++) {
final View view = activity.findViewById(ids[j]);
if(view==null) continue;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
method.setAccessible(true);
method.invoke(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
});
}
}
}
}
// bind方法对外开放
public static void bind(Activity activity) {
injectLayout(activity);
bindView(activity);
bindOnClick(activity);
}
以上代码并没有什么难度,只要是了解一点反射知识的相信都能看懂。那么我们只要在Activity中调用bind方法后便可以使用注解了。下面来看Activity中的代码:
@InjectLayout(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_test)
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
BindProcessor.bind(this);
mButton.setText("通过注解设置的Text");
}
@OnClick({R.id.tv_test, R.id.btn_reflect})
public void onClick(View view) {
switch (view.getId()) {
case R.id.tv_test:
Toast.makeText(this, "通过注解点解了Text", Toast.LENGTH_SHORT).show();
break;
case R.id.btn_reflect:
testReflect();
break;
}
}
}
可以看到,我们并没有在onCreate方法中调用setContentView方法,也没有去为Button按钮findViewById,更没有为其设置监听事件,我们统统都是用上面自定义的注解实现的。那么效果如何呢?我们来看下运行及起来的效果:
效果貌似还不错,实现了与ButterKnife的部分功能,甚至我们还比ButterKnife多了一个注入布局的功能。但是,我们要知道的是ButterKnife并非是直接用反射实现的,因为反射是在运行时处理的,会影响到程序的效率。但对于神一般存在的Jake,怎么会做如此没有效率的事情。关于ButterKnife是如何实现注解,我们在下篇文章中在做探讨。
源码参考
给大家推荐一下BannerViewPager。这是一个基于ViewPager实现的具有强大功能的无限轮播库。通过BannerViewPager可以实现腾讯视频、QQ音乐、酷狗音乐、支付宝、天猫、淘宝、优酷视频、喜马拉雅、网易云音乐、哔哩哔哩等APP的Banner样式以及指示器样式。
欢迎大家到github关注BannerViewPager!