在研究Retrofit源码的时候,发现每个网络请求接口类就是提供的一种外观模式来实现将该接口类转换成了一个Http请求。而该网络请求接口类就是一个通过自定义注解来初始化网络请求接口的一些基本参数。本文主要总结下自定义注解来实现的这种外观模式。
目录
外观模式
定义一个统一的接口,外部通过该接口来对子系统进行访问。
注解Annotatiion文件的定义
在自定义一个注解的java文件,用@interface来声明。文件名可以理解为该一系列注解的集合。
元注解
即在@interface声明的java文件中要添加上需要注解一些表示标示,用来标记该定义的一系列的注解的存在方式、作用域等,主要包括下面四种作用
@Retention (存在方式) | @Retention(RetentionPolicy.SOURCE) | 注解仅存在于源码,不包含在class字码文件 |
@Retention(RetentionPolicy.CLASS) | 默认的方式。在class字节码中存在,但运行时无法获得 | |
@Retention(RetentionPolicy.RUNTIME) | ||
在class字节码中存在,在运行时可以反射获得。 | ||
@Target (作用域:即这个注解可以标记在哪里) | @Target(ElementType.TYPE) | 接口、类、枚举、注解 |
@Target(ElementType.FIELD) | 字段、枚举常量 | |
@Target(ElementType.METHOD) | 方法 | |
@Target(ElementType.PARAMETER) | 方法参数 | |
@Target(ElementType.CONSTRUCTOR) | 构造函数 | |
@Target(ElementType.LOCAL_VARIABLE) | 局部变量 | |
@Target(ElementType.ANNOTATION_TYPE) | 注解 | |
@Target(ElementType.PACKAGE) | 包 | |
@Documented | 注解包含在javadoc中 | |
@Inherited | 注解可以被继承 |
实例讲解
要实现一个班级的花名册,每个学生的信息包括姓名、年龄、性别。最后的文件包括下面三个文件
- 1、定义学生的接口类
也就是自定义注解类IStudent,通过注解来完成每个信息的字段赋值,代码如下
@Inherited
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IStudent {
String name() default "";
int age() default 0;
boolean isGirl() default true;
}
@Retention(RetentionPolicy.RUNTIME)这个是可以通过反射获取到的,后面会有一个需要通过反射获取对应值的处理;@Target({ElementType.FIELD, ElementType.METHOD})表示该注解既可以作用于成员变量又可以作用于方法。
为了更多的来描述上面表格中提到的注解类型,这里写的稍微麻烦点。其实完成可以只作用于成员变量,我们直接读取成员变量或者方法的注解值就可以。像Retrofit中的GET注解,就是注解到ElementType.METHOD,代码如下:
/** Make a GET request. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
String value() default "";
}
好了回到我们这个话题上来。
- 2、定义类来使用这些注解完成初始化
public class Student {
private String name;
private int age;
@IStudent(isGirl = false)
private boolean sex;
@IStudent(name = "张三")
public void setName(String name) {
this.name = name;
}
@IStudent(age = 3)
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getSex() {
return sex ? "女生" : "男生";
}
@Override
public String toString() {
return String.format("姓名:%s,年龄:%d,性别:%s" , getName(), getAge(), getSex());
}
不要纠结里面怎么这么复杂,只是想多方面展示下自定义的那几个注解的用法。还是那句话完成可以选择一种类型来去做这个注解就可以了。
上面两步就是完成了注解的自定义。
- 3、通过反射来将注解生成对应的实例
public class StudentBook {
/**
* 创建一个实例
*
* @param student 通过注解来定义的具体的一个学生
* @return
*/
public static <T> T create(Class<T> student) {
try {
T studentInstance = student.getConstructor().newInstance();
//读取Student里面的所有方法
Method[] methods = student.getDeclaredMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(IStudent.class)) {
continue;
}
IStudent istu = method.getAnnotation(IStudent.class);
//这里因为两个方法的参数不同,有没有什么其他好的方式,可以修改到对应的参数
if (method.getName().equals("setName")) {
method.invoke(studentInstance, istu.name());
}
if (method.getName().equals("setAge")) {
method.invoke(studentInstance, istu.age());
}
}
//读取Student里面的成员变量
Field[] fields = student.getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(IStudent.class)) {
continue;
}
//在设置私有的成员变量的时候,一定设置field.setAccessible(true)
field.setAccessible(true);
IStudent istu1 = field.getAnnotation(IStudent.class);
if (field.getName().equals("sex")) {
field.set(studentInstance, istu1.isGirl());
}
}
return studentInstance;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
Student student = StudentBook.create(Student.class);
System.out.println(student.toString());
}
}
注意在设置私有的成员变量的时候,一定设置field.setAccessible(true);否则会抛出以下异常:
java.lang.IllegalAccessException: Class com.j1.aidl.annotation.StudentBook can not access a member of class com.j1.aidl.annotation.Student with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
执行main函数,输出
姓名:张三,年龄:3,性别:男生
如果在添加其他的学生,直接在定义一个StudentA.class、StudentB.class……即可。
总结
只要通过定义一个接口类,就可以轻松完成去实现初始化一个实例对象。我们去反过来去看Retrofit源码,就很清楚的了解我们定义的接口类来创建一个Http请求。开发人员不用关心这个转换过程,只要根据接口类的定义方式去完成相应的设置即可。
越来越发现研究源码其乐无穷。开心每天的一点点进步。