Java 注解(Annotation)又称为 Java 标注,是 JDK5.0 引入的一种注释机制。
Java 语言中的类、方法、变量、参数和包等都可以被标注,Java 标注可以通过反射获取标注内容。可以在编译、类加载、运行时被读取,并执行相应的处理。
一.Annotation的作用
注解将一些本来重复性的工作,变成程序自动完成,简化和自动化该过程。比如用于生成Java doc,比如编译时进行格式检查,比如自动生成代码等,用于提升软件的质量和提高软件的生产效率。在反射的 Class, Method, Field 等函数中,有许多于 Annotation 相关的接口。可以在反射中解析并使用 Annotation。
二.Annotation的解释
元注解是java API提供的,是用于修饰注解的注解,通常用在注解的定义上,Java提供了四种元注解,专门负责新注解的创建工作,四种注解如下:
@Target:注解的作用目标;用于指明被修饰的注解最终可以作用的目标是谁,也就是指明注解是用来修饰方法的?修饰类的?还是用来修饰字段属性的?
ElementType.CONSTRUCTOR:用于描述构造器
ElementType.FIELD:用于描述属性字段
ElementType.LOCAL_VARIABLE:用于描述局部变量
ElementType.METHOD:用于描述方法
ElementType.PACKAGE:用于描述包
ElementType.PARAMETER:用于描述参数
ElementType.TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retention:注解的生命周期; 表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy 中,包括:
RetentionPoicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;用于做一些检查性的操作,比如 @Override 和 @SuppressWarnings
RetentionPoicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;用于在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife)
RetentionPoicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;用于在运行时去动态获取注解信息。
@Documented:将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同;
@Inherited:是否允许子类继承该注解。
三.Annotation的使用
注解定义格式如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface xxxx {
int value();
}
使用注解需要用到Java反射机制,只有通过类才能去获取到其内的方法,变量等。
下面通过一个实例来进行实战,通过注解来减少View中的findViewById及setOnClickListenter等代码逻辑。
a.View类
public class InjectFragment extends BaseFragment {
@BindView(R.id.img1)
private ImageView mImg1;
@BindView(R.id.img2)
private ImageView mImg2;
@Override
public int getLayoutId() {
return R.layout.inject_layout;
}
@Override
public void initData(View view) {
InjectManager.inject(this, view);
}
@OnClick({R.id.btn1, R.id.btn2})
public void onViewClick(View view) {
switch (view.getId()) {
case R.id.btn1:
InputStream is = mContext.getResources().openRawResource(R.drawable.ic_chuancai);
Bitmap bmp = BitmapFactory.decodeStream(is);
mImg1.setImageBitmap(bmp);
break;
case R.id.btn2:
InputStream is1 = mContext.getResources().openRawResource(R.drawable.ic_lucai);
Bitmap bmp1 = BitmapFactory.decodeStream(is1);
mImg2.setImageBitmap(bmp1);
break;
}
}
}
通过以上代码可以看到:代码中没有了如findViewById及setOnClickListener,而是多了@BindView,@OnClick注解及InjectManager.inject(this, view),当进行点击事件后,最终会调用到onViewClick方法。
b.自定义注解类
定义用于field的注解BindView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
}
定义用于method的注解OnClick
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
int[] value();
}
c.注解的解析
public class InjectManager {
public static void inject(Object obj1, Object obj2) {
//控件注入
injectField(obj1, obj2);
//方法注入
injectMethod(obj1, obj2);
}
private static void injectField(Object obj1, Object obj2) {
Class clazz1 = obj1.getClass();
Class clazz2 = obj2.getClass();
Field[] declaredFields = clazz1.getDeclaredFields();
//遍历class中所有的Field
for (int i = 0; i < declaredFields.length; i++) {
Field field = declaredFields[i];
//设置为可访问,暴力反射,就算是私有的也能访问到
field.setAccessible(true);
//获取Field上的注解对象
BindView annotation = field.getAnnotation(BindView.class);
//不是所有Filed上都有想要的注解,需要对annotation进行null判断
if (annotation == null) {
continue;
}
//获取注解中的值
int id = annotation.value();
//获取控件,通过反射获取
try {
Method findViewById = clazz2.getMethod("findViewById", int.class);
findViewById.setAccessible(true);
View view = (View) findViewById.invoke(obj2, id);
//将view赋值给field
field.set(obj1, view);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
private static void injectMethod(final Object obj1, Object obj2) {
Class clazz1 = obj1.getClass();
Class clazz2 = obj2.getClass();
//获取所有的方法(私有方法也可以获取到)
Method[] declaredMethods = clazz1.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
final Method method = declaredMethods[i];
//获取方法上面的注解
OnClick annotation = method.getAnnotation(OnClick.class);
if (annotation == null) {
continue;
}
//传入需要响应的实例及方法
OnClickListenerInvoke clickListenerInvoke = new OnClickListenerInvoke(obj1, method, "onClick");
Object clickListener = Proxy.newProxyInstance(View.OnClickListener.class.getClassLoader(),
new Class[]{View.OnClickListener.class}, clickListenerInvoke);
int[] value = annotation.value();
for (int j = 0; j < value.length; j++) {
int id = value[j];
//获取控件,通过反射获取
try {
Method findViewById = clazz2.getMethod("findViewById", int.class);
findViewById.setAccessible(true);
View view = (View) findViewById.invoke(obj2, id);
Method setOnClickListenerMethod = view.getClass().getMethod(
"setOnClickListener", View.OnClickListener.class);
//通过动态代理实现设置setOnClickListener
setOnClickListenerMethod.invoke(view, clickListener);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
通过以上可以看到,通过反射对view中的控件进行赋值,通过反射及动态代理Java动态代理学习对控件进行设置点击响应。通过动态代理View.onClickListener后,当点击控件后,会最终执行到OnClickListenerInvoke的invoke()方法,参数method是onClick,但是view中没有onClick这个方法,所以不能直接使用method.invoke(),需要替换成view中的method执行invoke(),处理如下:
public class OnClickListenerInvoke implements InvocationHandler {
private Object mObject;
private Method mMethod;
private String mEventName;
public OnClickListenerInvoke(Object obj, Method method, String eventName) {
mMethod = method;
mObject = obj;
mEventName = eventName;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//此处的method是onClick或者onLongClick,需要执行的是onViewClick或onViewLongClick方法
if (method.getName().equals(mEventName)) {
return mMethod.invoke(mObject, args);
}
return null;
}
}
通过以上流程,注释就完成了。
四.Annotation的扩展
当点击事件不止setOnClickListener一个,再加上setOnLongClickListener,那需要如何处理呢?简单方式就是在InjectManager内再加一个方法,实现setOnLongClickListener的逻辑,这样的话,后续加一个注解就需要在InjectManager加方法;
当一个method上有多个注解时,我们需要来判断是自定义的注解然后来进行解释,那应该如何处理呢?那就需要定义一个元注解,在自定义的注解上加上该元注解就可以区分是否是自己定义的注解。
结合以上两点,实现如下:
a.View
@OnClick({R.id.btn1, R.id.btn2})
public void onViewClick(View view) {
switch (view.getId()) {
case R.id.btn1:
break;
case R.id.btn2:
break;
}
}
@OnLongClick(R.id.btn2)
public boolean onViewOnLongClick(View view) {
Toast.makeText(mContext, "这是长按事件", Toast.LENGTH_SHORT).show();
return true;
}
b.自定义注解及元注解
元注解:
listenerSetting:要设置的Listenter方法名字;
listenerClass:Listenter方法需要设置的参数表示的类;
eventName:点击后Listener的回调方法名字;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClickEvent {
String listenerSetting();
Class listenerClass();
String eventName();
}
OnClick注解修改:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ClickEvent(listenerSetting = "setOnClickListener", listenerClass = View.OnClickListener.class,
eventName = "onClick")
public @interface OnClick {
int[] value();
}
OnLongClick注解修改:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ClickEvent(listenerSetting = "setOnLongClickListener", listenerClass =
View.OnLongClickListener.class, eventName = "onLongClick")
public @interface OnLongClick {
int[] value();
}
通过以上可以看到,OnClick及OnLongClick上面加入了ClickEvent注解,且传入了需要的参数,从而可以区分出是setOnClickListener还是setOnLongClickListener。
c.注解的解析
//InjectManager.java
//统一处理所有自定义的注解
private static void injectMethod(final Object obj1, Object obj2) {
Class clazz1 = obj1.getClass();
Class clazz2 = obj2.getClass();
//获取所有的方法
Method[] declaredMethods = clazz1.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
final Method method = declaredMethods[i];
//获取到某一方法上的所有的Annotation
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
//获取到某个Annotation对应的class
Class<? extends Annotation> annotationTypeClass = annotation.annotationType();
//获取到Annotation上的Annotation,来区分是自定义注解还是其他注解
ClickEvent ano = annotationTypeClass.getAnnotation(ClickEvent.class);
if (ano == null) {
continue;
}
//获取到Annotation上的Annotation对应的值
String listenerSetting = ano.listenerSetting();
Class listenerClass = ano.listenerClass();
String eventName = ano.eventName();
//创建动态代理对象
OnClickListenerInvoke clickListenerInvoke = new OnClickListenerInvoke(obj1, method, eventName);
Object clickListener = Proxy.newProxyInstance(listenerClass.getClassLoader(),
new Class[]{listenerClass}, clickListenerInvoke);
try {
//进入该逻辑的annotation是OnClick或OnLongClick,但是通过annotation访问不到value(),可以强制转换,但是会增加处理逻辑,此处不合适
/*String name = annotation.annotationType().getSimpleName();
int[] ids;
if (name.equals("OnClick")) {
OnClick oc = (OnClick) annotation;
ids = oc.value();
} else {
OnLongClick olc = (OnLongClick) annotation;
ids = olc.value();
}*/
//所有此处通过反射获取到int[]
Method valueMethod = annotationTypeClass.getDeclaredMethod("value");
int[] ids = (int[]) valueMethod.invoke(annotation);
for (int id : ids) {
Method findViewById = clazz2.getMethod("findViewById", int.class);
findViewById.setAccessible(true);
View view = (View) findViewById.invoke(obj2, id);
Method setOnClickListenerMethod = view.getClass().getMethod(
listenerSetting, listenerClass);
//通过动态代理实现设置ClickListener
setOnClickListenerMethod.invoke(view, clickListener);
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
总结一下步骤:
①:先通过class获取到class内的所有methods;
②:遍历所有methods,获取到method上的所有annotations;
③:遍历所有annotations,通过annotation.annotationType()来得到annotation对应的class,然后获取到class上是否含有ClickEvent注解;
④:如果含有InjectEvent注解,那么获取到ClickEvent注解携带的参数内容;
⑤:创建对应的动态代理,然后把method传入,供后续invoke()时执行;
⑥:通过annotation class反射获取到value()方法,继而获取到view中annotation内的参数数组ids;
⑦:遍历所有的ids,通过id来获取到组件,然后设置点击事件。
网友评论