发表于 2017-07-18 | 分类于 JAVA知识点 |
字数统计: 7,519 | 阅读时长 ≈ 31
现在在我们构建自己或公司的项目中,或多或少都会依赖几个流行比较屌的第三方库,比如:Butter Knife
、Retrofit 2
、Dagger 2
、GreenDao
等,如果你没用过,那你需要找时间补一下啦;有时在使用后我们会好奇他们到底是怎么做到这种简洁、高效、松耦合等诸多优点的,当然这里我不探讨它们具体怎么实现的 (可以看看我之前写的几篇文章) ,而关心的是它们都用到同样的技术那就是本篇所讲的反射和注解,并实现的依赖注入。
阅读本篇文章有助于你更好的理解这些大形框架的原理和复习Java的知识点。为什么要把反射放在前面讲呢,实际上是因为我们学习注解的时候需要用到反射机制,所以,先学习反射有助于理解后面的知识。
主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。
面试有可能会问到,这句话不管你能不能理解,但是你只要记住就可以了
反射机制就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
用一句话总结就是反射可以实现在运行时可以知道任意一个类的属性和方法。
反射机制主要提供了以下功能:
为什么要用反射机制?直接创建对象不就可以了吗,这就涉及到了动态与静态的概念
静态编译:在编译时确定类型,绑定对象,即通过。
动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了java的灵活性,体现了多态的应用,有以降低类之间的藕合性。
优点
缺点
想要了解反射首先理解一下Class类,它是反射实现的基础。
类是java.lang.Class类的实例对象,而Class是所有类的类(There is a class named Class)
对于普通的对象,我们一般都会这样创建和表示:
1 |
Code code1 = new Code(); |
上面说了,所有的类都是Class的对象,那么如何表示呢,可不可以通过如下方式呢:
1 |
Class c = new Class(); |
但是我们查看Class的源码时,是这样写的:
1 2 3 |
private Class(ClassLoader loader) { classLoader = loader; } |
可以看到构造器是私有的,只有JVM可以创建Class的对象,因此不可以像普通类一样new一个Class对象,虽然我们不能new一个Class对象,但是却可以通过已有的类得到一个Class对象,共有三种方式,如下:
1 2 3 |
Class c1 = Code.class; 这说明任何一个类都有一个隐含的静态成员变量class,这种方式是通过获取类的静态成员变量class得到的 Class c2 = code1.getClass(); code1是Code的一个对象,这种方式是通过一个类的对象的getClass()方法获得的 Class c3 = Class.forName("com.trigl.reflect.Code"); 这种方法是Class类调用forName方法,通过一个类的全量限定名获得 |
这里,c1、c2、c3都是Class的对象,他们是完全一样的,而且有个学名,叫做Code的类类型(class type)。
这里就让人奇怪了,前面不是说Code是Class的对象吗,而c1、c2、c3也是Class的对象,那么Code和c1、c2、c3不就一样了吗?为什么还叫Code什么类类型?这里不要纠结于它们是否相同,只要理解类类型是干什么的就好了,顾名思义,类类型就是类的类型,也就是描述一个类是什么,都有哪些东西,所以我们可以通过类类型知道一个类的属性和方法,并且可以调用一个类的属性和方法,这就是反射的基础。
举个简单例子代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class ReflectDemo { public static void main(String[] args) throws ClassNotFoundException { //第一种:Class c1 = Code.class; Class class1=ReflectDemo.class; System.out.println(class1.getName()); //第二种:Class c2 = code1.getClass(); ReflectDemo demo2= new ReflectDemo(); Class c2 = demo2.getClass(); System.out.println(c2.getName()); //第三种:Class c3 = Class.forName("com.trigl.reflect.Code"); Class class3 = Class.forName("com.tengj.reflect.ReflectDemo"); System.out.println(class3.getName()); } } |
执行结果:
1 2 3 |
com.tengj.reflect.ReflectDemo com.tengj.reflect.ReflectDemo com.tengj.reflect.ReflectDemo |
在这里先看一下sun为我们提供了那些反射机制中的类:
java.lang.Class;
java.lang.reflect.Constructor; java.lang.reflect.Field;
java.lang.reflect.Method;
java.lang.reflect.Modifier;
前面我们知道了怎么获取Class,那么我们可以通过这个Class干什么呢?
总结如下:
下面来具体介绍
两个参数分别是方法名和方法参数类的类类型列表。
1 2 3 4 5 6 7 8 |
public Method getDeclaredMethod(String name, Class>... parameterTypes) // 得到该类所有的方法,不包括父类的 public Method getMethod(String name, Class>... parameterTypes) // 得到该类所有的public方法,包括父类的 //具体使用 Method[] methods = class1.getDeclaredMethods();//获取class对象的所有声明方法 Method[] allMethods = class1.getMethods();//获取class对象的所有public方法 包括父类的方法 Method method = class1.getMethod("info", String.class);//返回次Class对象对应类的、带指定形参列表的public方法 Method declaredMethod = class1.getDeclaredMethod("info", String.class);//返回次Class对象对应类的、带指定形参列表的方法 |
举个例子:
例如类A有如下一个方法:
1 2 3 |
public void fun(String name,int age) { System.out.println("我叫"+name+",今年"+age+"岁"); } |
现在知道A有一个对象a,那么就可以通过:
1 2 3 4 |
Class c = Class.forName("com.tengj.reflect.Person"); //先生成class Object o = c.newInstance(); //newInstance可以初始化一个实例 Method method = c.getMethod("fun", String.class, int.class);//获取方法 method.invoke(o, "tengj", 10); //通过invoke调用该方法,参数第一个为实例对象,后面为具体参数值 |
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public class Person { private String name; private int age; private String msg="hello wrold"; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Person() { } private Person(String name) { this.name = name; System.out.println(name); } public void fun() { System.out.println("fun"); } public void fun(String name,int age) { System.out.println("我叫"+name+",今年"+age+"岁"); } } public class ReflectDemo { public static void main(String[] args){ try { Class c = Class.forName("com.tengj.reflect.Person"); Object o = c.newInstance(); Method method = c.getMethod("fun", String.class, int.class); method.invoke(o, "tengj", 10); } catch (Exception e) { e.printStackTrace(); } } } |
执行结果:
我叫tengj,今年10岁
怎样,是不是感觉很厉害,我们只要知道这个类的路径全称就能玩弄它于鼓掌之间。
有时候我们想获取类中所有成员方法的信息,要怎么办。可以通过以下几步来实现:
1.获取所有方法的数组:
1 2 3 4 |
Class c = Class.forName("com.tengj.reflect.Person"); Method[] methods = c.getDeclaredMethods(); // 得到该类所有的方法,不包括父类的 或者: Method[] methods = c.getMethods();// 得到该类所有的public方法,包括父类的 |
2.然后循环这个数组就得到每个方法了:
1 |
for (Method method : methods) |
完整代码如下:
person类跟上面一样,这里以及后面就不贴出来了,只贴关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class ReflectDemo { public static void main(String[] args){ try { Class c = Class.forName("com.tengj.reflect.Person"); Method[] methods = c.getDeclaredMethods(); for(Method m:methods){ String methodName= m.getName(); System.out.println(methodName); } } catch (Exception e) { e.printStackTrace(); } } } |
执行结果:
getName
setName
setAge
fun
fun
getAge
这里如果把c.getDeclaredMethods();改成c.getMethods();执行结果如下,多了很多方法,以为把Object里面的方法也打印出来了,因为Object是所有类的父类:
getName
setName
getAge
setAge
fun
fun
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll
想一想成员变量中都包括什么:成员变量类型+成员变量名
类的成员变量也是一个对象,它是java.lang.reflect.Field
的一个对象,所以我们通过java.lang.reflect.Field
里面封装的方法来获取这些信息。
单独获取某个成员变量,通过Class类的以下方法实现:
参数是成员变量的名字
1 2 3 4 5 6 7 8 |
public Field getDeclaredField(String name) // 获得该类自身声明的所有变量,不包括其父类的变量 public Field getField(String name) // 获得该类自所有的public成员变量,包括其父类变量 //具体实现 Field[] allFields = class1.getDeclaredFields();//获取class对象的所有属性 Field[] publicFields = class1.getFields();//获取class对象的public属性 Field ageField = class1.getDeclaredField("age");//获取class指定属性 Field desField = class1.getField("des");//获取class指定的public属性 |
举个例子:
例如一个类A有如下成员变量:
1 |
private int n; |
如果A有一个对象a,那么就可以这样得到其成员变量:
1 2 |
Class c = a.getClass(); Field field = c.getDeclaredField("n"); |
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class ReflectDemo { public static void main(String[] args){ try { Class c = Class.forName("com.tengj.reflect.Person"); //获取成员变量 Field field = c.getDeclaredField("msg"); //因为msg变量是private的,所以不能用getField方法 Object o = c.newInstance(); field.setAccessible(true);//设置是否允许访问,因为该变量是private的,所以要手动设置允许访问,如果msg是public的就不需要这行了。 Object msg = field.get(o); System.out.println(msg); } catch (Exception e) { e.printStackTrace(); } } } |
执行结果:
hello wrold
同样,如果想要获取所有成员变量的信息,可以通过以下几步
1.获取所有成员变量的数组:
1 |
Field[] fields = c.getDeclaredFields(); |
2.遍历变量数组,获得某个成员变量field
1 |
for (Field field : fields) |
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class ReflectDemo { public static void main(String[] args){ try { Class c = Class.forName("com.tengj.reflect.Person"); Field[] fields = c.getDeclaredFields(); for(Field field :fields){ System.out.println(field.getName()); } } catch (Exception e) { e.printStackTrace(); } } } |
执行结果:
name
age
msg
最后再想一想构造函数中都包括什么:构造函数参数
同上,类的成构造函数也是一个对象,它是java.lang.reflect.Constructor
的一个对象,所以我们通过java.lang.reflect.Constructor
里面封装的方法来获取这些信息。
单独获取某个构造函数,通过Class
类的以下方法实现:
这个参数为构造函数参数类的类类型列表
1 2 3 4 5 6 7 8 |
public Constructor |
举个例子:
例如类A有如下一个构造函数:
1 2 3 |
public A(String a, int b) { // code body } |
那么就可以通过:
1 |
Constructor constructor = a.getDeclaredConstructor(String.class, int.class); |
来获取这个构造函数。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class ReflectDemo { public static void main(String[] args){ try { Class c = Class.forName("com.tengj.reflect.Person"); //获取构造函数 Constructor constructor = c.getDeclaredConstructor(String.class); constructor.setAccessible(true);//设置是否允许访问,因为该构造器是private的,所以要手动设置允许访问,如果构造器是public的就不需要这行了。 constructor.newInstance("tengj"); } catch (Exception e) { e.printStackTrace(); } } } |
执行结果:
tengj
注意:Class的newInstance方法,只能创建只包含无参数的构造函数的类,如果某类只有带参数的构造函数,那么就要使用另外一种方式:
1 |
fromClass.getDeclaredConstructor(String.class).newInstance("tengj"); |
获取所有的构造函数,可以通过以下步骤实现:
1.获取该类的所有构造函数,放在一个数组中:
1 |
Constructor[] constructors = c.getDeclaredConstructors(); |
2.遍历构造函数数组,获得某个构造函数constructor
:
1 |
for (Constructor constructor : constructors) |
完整代码:
1 2 3 4 5 6 7 8 9 10 11 |
public class ReflectDemo { public static void main(String[] args){ Constructor[] constructors = c.getDeclaredConstructors(); for(Constructor constructor:constructors){ System.out.println(constructor); } } catch (Exception e) { e.printStackTrace(); } } } |
执行结果:
public com.tengj.reflect.Person()
public com.tengj.reflect.Person(java.lang.String)
注解需要用到的
1 2 3 4 |
Annotation[] annotations = (Annotation[]) class1.getAnnotations();//获取class对象的所有注解 Annotation annotation = (Annotation) class1.getAnnotation(Deprecated.class);//获取class对象指定注解 Type genericSuperclass = class1.getGenericSuperclass();//获取class对象的直接超类的 Type Type[] interfaceTypes = class1.getGenericInterfaces();//获取class对象的所有接口的type集合 |
获取class对象的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
boolean isPrimitive = class1.isPrimitive();//判断是否是基础类型 boolean isArray = class1.isArray();//判断是否是集合类 boolean isAnnotation = class1.isAnnotation();//判断是否是注解类 boolean isInterface = class1.isInterface();//判断是否是接口类 boolean isEnum = class1.isEnum();//判断是否是枚举类 boolean isAnonymousClass = class1.isAnonymousClass();//判断是否是匿名内部类 boolean isAnnotationPresent = class1.isAnnotationPresent(Deprecated.class);//判断是否被某个注解类修饰 String className = class1.getName();//获取class名字 包含包名路径 Package aPackage = class1.getPackage();//获取class的包信息 String simpleName = class1.getSimpleName();//获取class类名 int modifiers = class1.getModifiers();//获取class访问权限 Class>[] declaredClasses = class1.getDeclaredClasses();//内部类 Class> declaringClass = class1.getDeclaringClass();//外部类 getSuperclass():获取某类的父类 getInterfaces():获取某类实现的接口 |
扩展的知识点,了解就可以了。后续会为大家写一篇关于泛型的文章。
首先下结论:
Java中集合的泛型,是防止错误输入的,只在编译阶段有效,绕过编译到了运行期就无效了。
下面通过一个实例来验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/** * 集合泛型的本质 */ public class GenericEssence { public static void main(String[] args) { List list1 = new ArrayList(); // 没有泛型 List |
执行结果:
list2的长度是:1
true
list2的长度是:2
有助于理解上述所讲的知识点
拓展阅读
Java反射机制深入详解 - 火星十一郎 - 博客园
Java反射入门 - Trigl的博客 - CSDN博客
Java反射机制 - ①块腹肌 - 博客园
Java 反射机制浅析 - 孤旅者 - 博客园
反射机制的理解及其用途 - 每天进步一点点! - ITeye博客
Java动态代理与反射详解 - 浩大王 - 博客园
在java语法中,使用@
符号作为开头,并在@后面紧跟注解名。被运用于类,接口,方法和字段之上,例如:
1 2 3 4 |
@Override void myMethod() { ...... } |
这其中@Override就是注解。这个注解的作用也就是告诉编译器,myMethod()方法覆写了父类中的myMethod()方法。
java中有三个内置的注解:
本文不在阐述三种内置注解的使用情节和方法,感兴趣的请看这里
自定义注解的时候用到的,也就是自定义注解的注解;(这句话我自己说的,不知道对不对)
元注解的作用就是负责注解其他注解。Java5.0
定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。
Java5.0
定义的4个元注解:@Target
@Retention
@Documented
@Inherited
java8加了两个新注解,后续我会讲到。
这些类型和它们所支持的类在java.lang.annotation包中可以找到。
@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
取值(ElementType)有:
类型 | 用途 |
---|---|
CONSTRUCTOR | 用于描述构造器 |
FIELD | 用于描述域 |
LOCAL_VARIABLE | 用于描述局部变量 |
METHOD | 用于描述方法 |
PACKAGE | 用于描述包 |
PARAMETER | 用于描述参数 |
TYPE | 用于描述类、接口(包括注解类型) 或enum声明 |
比如说这个注解表示只能在方法中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Target({ElementType.METHOD}) public @interface MyCustomAnnotation { } //使用 public class MyClass { @MyCustomAnnotation public void myMethod() { ...... } } |
@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:
类型 | 用途 | 说明 |
---|---|---|
SOURCE | 在源文件中有效(即源文件保留) | 仅出现在源代码中,而被编译器丢弃 |
CLASS | 在class文件中有效(即class保留) | 被编译在class文件中 |
RUNTIME | 在运行时有效(即运行时保留) | 编译在class文件中 |
使用示例:
1 2 3 4 5 6 7 8 |
/*** * 字段注解接口 */ @Target(value = {ElementType.FIELD})//注解可以被添加在属性上 @Retention(value = RetentionPolicy.RUNTIME)//注解保存在JVM运行时刻,能够在运行时刻通过反射API来获取到注解的信息 public @interface Column { String name();//注解的name属性 } |
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
作用:将注解包含在javadoc中
示例:
1 2 3 |
java.lang.annotation.Documented @Documented public @interface MyCustomAnnotation { //Annotation body} |
作用:允许子类继承父类中的注解
示例,这里的MyParentClass 使用的注解标注了@Inherited,所以子类可以继承这个注解信息:
1 2 3 4 |
java.lang.annotation.Inherited @Inherited public @interface MyCustomAnnotation { } |
1 2 3 4 |
@MyCustomAnnotation public class MyParentClass { ... } |
1 2 3 |
public class MyChildClass extends MyParentClass { ... } |
1 2 3 |
public @interface 注解名{ 定义体 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * test注解 * @author ddk * */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TestAnnotation { /** * id * @return */ public int id() default -1; /** * name * @return */ public String name() default ""; } |
java.lang.reflect.AnnotatedElement
Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
/***********注解声明***************/ /** * 水果名称注解 * @author peida * */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FruitName { String value() default ""; } /** * 水果颜色注解 * @author peida * */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FruitColor { /** * 颜色枚举 * @author peida * */ public enum Color{ BULE,RED,GREEN}; /** * 颜色属性 * @return */ Color fruitColor() default Color.GREEN; } /** * 水果供应者注解 * @author peida * */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FruitProvider { /** * 供应商编号 * @return */ public int id() default -1; /** * 供应商名称 * @return */ public String name() default ""; /** * 供应商地址 * @return */ public String address() default ""; } /***********注解使用***************/ public class Apple { @FruitName("Apple") private String appleName; @FruitColor(fruitColor=Color.RED) private String appleColor; @FruitProvider(id=1,name="陕西红富士集团",address="陕西省西安市延安路89号红富士大厦") private String appleProvider; public void setAppleColor(String appleColor) { this.appleColor = appleColor; } public String getAppleColor() { return appleColor; } public void setAppleName(String appleName) { this.appleName = appleName; } public String getAppleName() { return appleName; } public void setAppleProvider(String appleProvider) { this.appleProvider = appleProvider; } public String getAppleProvider() { return appleProvider; } public void displayName(){ System.out.println("水果的名字是:苹果"); } } /***********注解处理器***************/ //其实是用的反射 public class FruitInfoUtil { public static void getFruitInfo(Class> clazz){ String strFruitName=" 水果名称:"; String strFruitColor=" 水果颜色:"; String strFruitProvicer="供应商信息:"; Field[] fields = clazz.getDeclaredFields(); for(Field field :fields){ if(field.isAnnotationPresent(FruitName.class)){ FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class); strFruitName=strFruitName+fruitName.value(); System.out.println(strFruitName); } else if(field.isAnnotationPresent(FruitColor.class)){ FruitColor fruitColor= (FruitColor) field.getAnnotation(FruitColor.class); strFruitColor=strFruitColor+fruitColor.fruitColor().toString(); System.out.println(strFruitColor); } else if(field.isAnnotationPresent(FruitProvider.class)){ FruitProvider fruitProvider= (FruitProvider) field.getAnnotation(FruitProvider.class); strFruitProvicer=" 供应商编号:"+fruitProvider.id()+" 供应商名称:"+fruitProvider.name()+" 供应商地址:"+fruitProvider.address(); System.out.println(strFruitProvicer); } } } } /***********输出结果***************/ public class FruitRun { /** * @param args */ public static void main(String[] args) { FruitInfoUtil.getFruitInfo(Apple.class); } } ==================================== 水果名称:Apple 水果颜色:RED 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延安路89号红富士大厦 |
1 2 3 4 5 6 7 8 9 10 11 12 |
//初始化对象时 String myString = new @NotNull String(); //对象类型转化时 myString = (@NonNull String) str; //使用 implements 表达式时 class MyList |
深入理解Java:注解 - 牛奶、不加糖 - 博客园
Java 注解基础知识 - 简书
【译】从java注解分析ButterKnife工作流程 - 简书
转载自:https://www.daidingkang.cc/2017/07/18/java-reflection-annotations/
博主:戴定康