注解与反射

注解

声明一个注解类型
public @interface Lance{
}
元注解

在定义注解时,注解类也能够使用其他的注解声明。对注解类型进行注解的注解类,我们称之为 meta�annotation(元注解)。

  • @Target
    注解标记另一个注解,以限制可以应用注解的 Java 元素类型。目标注解指定以下元素类型之一作为其值:
    ElementType.ANNOTATION_TYPE 可以应用于注解类型。
    ElementType.CONSTRUCTOR 可以应用于构造函数。
    ElementType.FIELD 可以应用于字段或属性。
    ElementType.LOCAL_VARIABLE 可以应用于局部变量。
    ElementType.METHOD 可以应用于方法级注解。
    ElementType.PACKAGE 可以应用于包声明。
    ElementType.PARAMETER 可以应用于方法的参数。
    ElementType.TYPE 可以应用于类的任何元素。
  • @Retention
    注解指定标记注解的存储方式:
    RetentionPolicy.SOURCE - 标记的注解仅保留在源级别中,并被编译器忽略。
    RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。
    RetentionPolicy.RUNTIME - 标记的注解由 JVM 保留,因此运行时环境可以使用它。

@Retention 三个值中 SOURCE < CLASS < RUNTIME,即CLASS包含了SOURCE,RUNTIME包含SOURCE、
CLASS。下文会介绍他们不同的应用场景。

注解应用场景

SOURCE

RetentionPolicy.SOURCE ,作用于源码级别的注解,可提供给IDE语法检查、APT等场景使用。
在类中使用 SOURCE 级别的注解,其编译之后的class中会被丢弃。

  • IDE语法检查
    在Android开发中, support-annotations 与 androidx.annotation) 中均有提供 @IntDef 注解,此注解的定义如下:
@Retention(SOURCE) //源码级别注解 
@Target({ANNOTATION_TYPE}) 
public @interface IntDef { 
int[] value() default {}; 
boolean flag() default false; 
boolean open() default false;
}

Java中Enum(枚举)的实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中。
比常量多5到10倍的内存占用。此注解的意义在于能够取代枚举,实现如方法入参限制。

如:我们定义方法 test ,此方法接收参数 teacher 需要在:Lance、Alvin中选择一个。如果使用枚举能够实现为:

public enum Teacher{
LANCE,ALVIN
}
public void test(Teacher teacher) {
}

而现在为了进行内存优化,我们现在不再使用枚举,则方法定义为:

public static final int LANCE = 1; 
public static final int ALVIN = 2; 
public void test(int teacher) {
}

然而此时,调用 test 方法由于采用基本数据类型int,将无法进行类型限定。此时使用@IntDef增加自定义注解:

public static final int LANCE = 1; 
public static final int ALVIN = 2; 
@IntDef(value = {MAN, WOMEN}) //限定为LANCE,ALVIN @Target(ElementType.PARAMETER) //作用于参数的注解 @Retention(RetentionPolicy.SOURCE) //源码级别注解 
public @interface Teacher { 
}
public void test(@Teacher int teacher) { 
}

此时,我们再去调用 test 方法,如果传递的参数不是 LANCE 或者 ALVIN 则会显示 Inspection 警告(编译不会报错)。

以上注解均为 SOURCE 级别,本身IDEA/AS 就是由Java开发的,工具实现了对Java语法的检查,借助注解能对被注解的特定语法进行额外检查。

  • APT注解处理器

APT全称为:"Anotation Processor Tools",意为注解处理器。顾名思义,其用于处理注解。编写好的Java源文件,需要经过 javac 的编译,翻译为虚拟机能够加载解析的字节码Class文件。注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。 注册的注解处理器由 javac调起,并将注解信息传递给注解处理器进行处理。
注解处理器是对注解应用最为广泛的场景。在Glide、EventBus3、Butterknifer、Tinker、ARouter等等常用框架中都有注解处理器的身影。但是你可能会发现,这些框架中对注解的定义并不是 SOURCE 级别,更多的是 CLASS 级别,别忘了:CLASS包含了SOURCE,RUNTIME包含SOURCE、CLASS。
关于注解处理器的实现,在后续课程中会有相当多的介绍。此处先不进行详细介绍。

CLASS

定义为 CLASS 的注解,会保留在class文件中,但是会被虚拟机忽略(即无法在运行期反射获取注解)。此时完全符合此种注解的应用场景为字节码操作。如:AspectJ、热修复Roubust中应用此场景。
所谓字节码操作即为,直接修改字节码Class文件以达到修改代码执行逻辑的目的。在程序中有多处需要进行是否登录的判断。


image.png

如果我们使用普通的编程方式,需要在代码中进行 if-else 的判断,也许存在十个判断点,则需要在每个判断点加入此项判断。此时,我们可以借助AOP(面向切面)编程思想,将程序中所有功能点划分为: 需要登录 与 无需登录两种类型,即两个切面。对于切面的区分即可采用注解。

//Java源码 
@Target(ElementType.METHOD) 
@Retention(RetentionPolicy.CLASS) 
public @interface Login {
}
@Login 
public void jumpA(){ 
 startActivity(new Intent(this,AActivity.class)); 
}
public void jumpB(){ 
 startActivity(new Intent(this,BActivity.class)); 
}

在上诉代码中, jumpA 方法需要具备登录身份。而 Login 注解的定义被设置为 CLASS 。因此我们能够在该类所编译的字节码中获得到方法注解 Login 。在操作字节码时,就能够根据方法是否具备该注解来修改class中该方法的内容加入 if-else 的代码段:

//Class字节码 
@Login public void jumpA() { 
 if (this.isLogin) { 
   this.startActivity(new Intent(this, LoginActivity.class)); 
 } else { 
   this.startActivity(new Intent(this, AActivity.class)); 
 } 
}

public void jumpB() { 
 startActivity(new Intent(this,BActivity.class)); 
}

注解能够设置类型元素(参数),结合参数能实现更为丰富的场景,如:运行期权限判定等。

RUNTIME

注解保留至运行期,意味着我们能够在运行期间结合反射技术获取注解中的所有信息。

反射

一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的,并且能够获得此类的引用。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。这时候,我们使用 JDK 提供的反射 API 进行反射调用。

反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。是Java被视为动态语言的关键。

Java反射机制主要提供了以下功能:

  • 在运行时构造任意一个类的对象
  • 在运行时获取或者修改任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法(属性)
Gson反序列化
static class Response<T> { 
 T data; 
 int code; 
 String message; 
 @Override 
 public String toString() { 
   return "Response{" + "data=" + data + ", code=" + code + ", message='" + message + '\'' + '}'; 
 }
 public Response(T data, int code, String message) { 
   this.data = data; 
   this.code = code; 
   this.message = message; 
 } 
}
static class Data { 
 String result; 
 public Data(String result) { 
   this.result = result; 
 }

 @Override 
 public String toString() { 
   return "Data{" + "result=" + result + '}'; 
 } 
}
 public static void main(String[] args) { 
   Response<Data> dataResponse = new Response(new Data("数据"), 1, "成功");  
   Gson gson = new Gson(); String json = gson.toJson(dataResponse);   
   System.out.println(json);
   //为什么TypeToken要定义为抽象类?
   Response<Data> resp = gson.fromJson(json,   new TypeToken<Response<Data>>() { }.getType());   
   System.out.println(resp.data.result); 
}

在进行GSON反序列化时,存在泛型时,可以借助 TypeToken 获取Type以完成泛型的反序列化。但是为什么TypeToken 要被定义为抽象类呢?
因为只有定义为抽象类或者接口,这样在使用时,需要创建对应的实现类,此时确定泛型类型,编译才能够将泛型signature信息记录到Class元数据中。

控件绑定

/**
*
*/
public class InjectViewUtils {

   public static void injectView(Activity activity) {
       if (null == activity) return;

       Class<? extends Activity> activityClass = activity.getClass();
       Field[] declaredFields = activityClass.getDeclaredFields();
       for (Field field : declaredFields) {//获取Activity类里面声明的所有成员变量
           if (field.isAnnotationPresent(InjectView.class)) {//找出标注了@InjectView的成员变量
               //解析InjectView 获取button id
               InjectView injectView = field.getAnnotation(InjectView.class);
               int value = injectView.value();//获取变量的值,也就是控件的id

               try {
                   //找到findViewById方法
                   Method findViewById = activityClass.getMethod("findViewById", int.class);
                   findViewById.setAccessible(true);
                   Object view = findViewById.invoke(activity, value);
                   field.set(activity, view);
               } catch (Exception e) {
                   e.printStackTrace();
               }
           }
       } 
   }

   public static void injectAutowired(Activity activity) {
       Class<? extends Activity> cls = activity.getClass();
       //获得数据
       Intent intent = activity.getIntent();
       Bundle extras = intent.getExtras();
       if (extras == null) {
           return;
       }

       //获得此类所有的成员
       Field[] declaredFields = cls.getDeclaredFields();
       for (Field field : declaredFields) {
           if (field.isAnnotationPresent(Autowired.class)) {
               Autowired autowired = field.getAnnotation(Autowired.class);
               //获得key
               String key = TextUtils.isEmpty(autowired.value()) ? field.getName() : autowired.value();

               if (extras.containsKey(key)) {
                   Object obj = extras.get(key);
                   // todo Parcelable数组类型不能直接设置,其他的都可以.
                   //获得数组单个元素类型
                   Class<?> componentType = field.getType().getComponentType();
                   //当前属性是数组并且是 Parcelable(子类)数组
                   if (field.getType().isArray() &&
                           Parcelable.class.isAssignableFrom(componentType)) {
                       Object[] objs = (Object[]) obj;
                       //创建对应类型的数组并由objs拷贝

                       Object[] objects = Arrays.copyOf(objs, objs.length, (Class<? extends Object[]>) field.getType());
                       obj = objects;
                   }

                   field.setAccessible(true);
                   try {
                       field.set(activity, obj);
                   } catch (IllegalAccessException e) {
                       e.printStackTrace();
                   }
               }
           }
       }
   } 
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容