Java进阶篇之——反射与注解
Java中的反射与注解,很多人不太明白,这明明是两个不相干的内容,为什么大家总是放到一块去讲述?实际上这他们两个单独使用并没有什么特别的,但是二者结合可以产生超脱认知的程序编写方式。
本文将从三方面讲述反射和注解之间的兄弟情结:
- 注解介绍
- 反射介绍
- 注解式开发
今后的Java架构中将大量的出现注解式开发,它其实没什么令人难以理解的,经过本文的阅读,你将轻而易举的推算出它们的实现。
第一节 注解
注解 (Annotation)又称为Java标注,Java中的类、方法、变量、参数和包等都可以被标注。
在编译器生成类文件时,注解可以被嵌入到JVM字节码中。和我们平常的注释不同,通过此法JVM虚拟机可以阅读你标注内容,从而构建代码程序。
所以那些主流框架程序可以通过反射去获取注解内的代码,让框架更加简洁(注解式开发)。
Java 有10个注解
6个在java.lang中,剩下4个在java.lang.annotation中
@override - 检查该方法是否存在重写方法。如果父类或者引用接口中并没有该方法时,会报编译错误。
@Deprecated - 标记过时的方法。如果使用该方法会编译警告
@SupperssWarnings - 指示编译器忽略注解中声明的警告。
@SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
@FunctionalInterface - Java 8 开始支持,标记一个匿名函数或者函数接口(我在Java-Lambda篇:函数接口中有提到)
@Repertable - Java 8 开始支持,表示某注解上同一个声明使用多次。
剩下四个称之为元注解:
@Retention - 表示这个注解是怎么保存的,是只在代码中,还是注入到Class文件中,或者在运行时可以通过反射访问。
@Target - 标记这个注解应该是那种 Java 成员,允许标注的地点。
@Documented - 标记这些注解是否包含在用户文档。
@Inherited - 标记这个注解是继承于那个注解类(默认 注解并没有继承于任何子类)
我们遇到比较多的是@override 表示重写的元注解 和 @Deprecated 表示弃用的元注解,但是今天我们介绍注解式开发最核心的两个元注解:@Target和@Inherited
元注解——首次开发注解使用示范:
1、@Target
首先我们先观察下@Target 注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
它最为核心的就是这个ElementType[] 枚举类型,它表示该注解可以存在于class文件的什么位置(类名上方、方法上方、参数上方),枚举类型中包含了以下几种类型(JDK 17):
名称 |
地点 |
来自JDK |
TYPE |
Type,可以在类、接口、枚举上注解 |
1.5 |
FIELD |
Field,类的字段常量,也可以在枚举上注解 |
1.5 |
METHOD |
Method,方法上 |
1.5 |
PARAMETER |
Parameter,方法的参数上 |
1.5 |
CONSTRUCTOR |
Constructor,构造函数上 |
1.5 |
LOCAL_VARIABLE |
Local Variable,方法内的局部变量上 |
1.5 |
ANNOTATION_TYPE |
Annotation Type,注解上 |
1.5 |
PACKAGE |
Package,包Package-info文件中 |
1.5 |
TYPE_PARAMETER |
Type Parameter,泛型中类型参数声明 |
1.8 |
TYPE_USE |
Type Use,泛型中指定具体类型(子类继承) |
1.8 |
MODULE |
module,模块上,JDK9模块化新增 |
9 |
RECORD_COMPONENT |
标注在记录类的组件上 |
16 |
这个注解规定了你自定义注解放置的位置。
2. @Retention
这个注解就更好理解了,它代表注解的生命周期
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
名称 |
地点 |
来自JDK |
SOURCE |
注解只保留在源文件,编译完毕后消除注解 |
1.8 |
CLASS |
注解被保留到class文件,类加载前消除 |
1.8 |
RUNTIME |
注解在运行时依旧存在,可以被反射获取 |
1.8 |
所以我们在进行注解开发时,为了保证JVM虚拟机能够阅读到注解,我们必须规定 @Retention 为RUNTIME
3.@Documented
这个注解没啥好说的,主要帮助生成API文档,项目当中相当重要,但不在我们本文主要演示范围内。
4.自定义注解
我们先定义一个自定义的注解:
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String value() default "";
int age() default 0;
}
这个注解中第一个元注解@Target中的参数叫做ElementType.METHOD和ElementType.FIELD,这个注解是可以标注在对象方法以及对象常量之上的:
public class Dog {
private String name;
@MyAnnotation(age = 10) //标注在常量字段之上
private int age;
public String getName() {
return name;
}
@MyAnnotation("ABC") //标注在方法之上
public void setName(String name) {
this.name = name;
}
}
注解内部中有一个默认的value() 方法,它允许你使用注解时你可以不填写注解属性的名字,左侧的类型代表你注解的输入类型,而其他非value名的方法代表你在使用注解时你需要指定该属性的名字。
此处我们直接填写一个字符串到注解中,它要求必须输入字符串,我们用来作为名字注入到狗对象属性常量中,而age常量上,我们的注解指定了age方法是一个int数字值。同时你还可以添加default 关键字来设定默认值。
那么仔细研究,尝试的去使用注解,你就可以理解注解的使用方法了。
2.反射
反射是非常暴力的方式去建立对象
java是动态加载的语言,因为设计者并没有限定Class一定是从磁盘来的,也可能是网络,也可能是压缩包,总之Class是符合规则的二进制代码就行,剩余的建立对象的任务都会交由JVM虚拟机来建立,而且每个Class一定是独一无二的.所以反射最重要的用途就是开发各种通用框架。
那么Class是什么?
答:Class是一个类,小写的class代表这是个类型,大写的Class代表了这个类的名称。
Class文件中到底包含了那些信息?
答:一个类中应该有类名,属性,方法,构造器,修饰符,常量等等。
Class类是一个对象照镜子的结果,对象可以看到自己有哪些属性,方法,构造器,实现了哪些接口等等。
对于每个类而言,Class的类型只会保存一个,并且它其中保存了这个类的所有有关信息,而对象必须由这个类加载器实现。
那么我们为什么要使用反射?
1.这个对象,不属于我们磁盘当中,它可能是从网络或者更加奇妙的地方传递来的。
2.可能我们没有这个对象,只有一个全类名。
3.有些信息,我们并不能简简单单的通过"变量"来读取。
通过反射,我们可以得到这个类里面的全部信息,并且动态的间造对象
获取Class对象三种方式:
1.通过类名获取 例: Class <User> cc = User.class
2.通过对象获取 例: Class<? extends User> cc = user.getClass(); //继承user的子类都为反射类型。
3.通过全类名获取 例: Class<?> cc = Class.forName("top.domain.User") //JDBC开发中常用此方法加载数据库驱动
记住,用这三种方法获取的Class永远的只能获取且仅有的一个,所以这三种方式获取的Class类都是一致的(内存地址相同)
Class类的常用方法:
1.建立对象
方法名 |
作用 |
实例 |
.istanceof(Object obj) |
判断是否属于某个类 |
apple instanceof Apple ? true : false; |
.getName() |
返回Class全名(或对象路径) |
Wolf.class.getName() |
.getSimpleName() |
返回Class简略类名 |
Wolf.class.getName() |
- 获取类的构造方法(Constructor)
方法名 |
作用 |
实例 |
.getConstructor() |
获取类不带参且public权限的构造函数 |
Constructor<Apple> constructor = Apple.class.getConstructor(); |
.getConstructors() |
获取类全部public权限的构造函数List |
Constructor<?>[] constructors = Apple.class.getConstructors(); |
.getDeclaredConstructor() |
暴力获取类不带参构造函数 |
Constructor<Worlf> declaredConstructor1 =Wolf.class.getDeclaredConstructor(); |
.getDeclaredConstructors() |
暴力获取类全部的构造函数List |
Constructor<?>[] declaredConstructors = Wolf.class.getDeclaredConstructors(); |
构造方法是允许重载的,因此我们可以指定对象的构造方法,例如:
Constructor<Worlf> constructor = Wolf.class.getDeclaredConstructor(String.class, int.class);
基本上来说,获取一个类的全部构造方法似乎毫无用处,我们并不知道自己想要的构造方法在那里,所以实际上我们都是获取反射类的无参构造方法后建立实例,使用get和set方法填充进去.此时我们构造方法获得了,那么我们就需要使用构造方法建立实例了:
2.1 构造方法建立实例对象:
.getConstructor()/.getDeclaredConstructor()
.newInstance(Object ... initargs) |
使用构造方法建立对象 |
Constructor<Wolf> constructor = Wolf.class .getDeclaredConstructor(String.class, int.class); Wolf wolf = constructor.newInstance("大狼", 2); |
.newInstance(Object ... initargs) |
暴力的构造方法建立实例 |
和上面类似 |
3.获取字段常量(Type)
方法名 |
作用 |
例 |
.getFields() |
获取所有 Public 属性的字段常量 |
clazz.getFields(); |
.getDeclaredFields() |
返回数组类型 以数组的形式返回所有属性字段 |
Field[] fields = clazz.getDeclaredFields(); Arrays.stream(fields).forEach(System.out::println); |
.getField(Str) |
获取特定属性的 Public 属性字段常量 |
Field name = clazz.getField("name") |
.getDeclareField(Str) |
获取特定属性的 暴力获取类封装的 |
Field used = clazz.getDeclareField("name") |
对Field类操作,如果常量属性不是Public类型,那么需要设置setAccessible属性:
方法名 |
作用 |
例 |
.getName() |
获取常量名 |
field.getName() |
.getModifiers() |
获取修饰符(以数字来表示) |
1表示public,4表示protected,16表示default,3表示private |
.getType() |
获取常量的Class |
|
.set(Obj obj,Obj value) |
传入对象,修改其属性 |
Dog objDog= new Dog("dang",2,DogType.XIA); Field name = clazz.getDeclaredField("dogType"); name.set(objDog,DogType.China); |
.setAccessible(boolean); |
允许该对象修改非pulice值 |
name.setAccessible(true); |
.setInt/Byte... (Obj obj,Int/Byte... value) |
修改对象属性,基本类型特化 |
|
3.获取方法(Method)
方法名 |
作用 |
例 |
.getMethod(Str name,Class<?>... parameterTypes) |
传入方法名,返回值类型,返回特定的方法 |
clazz.getMethod("setName", String.class); |
.getMethods() |
获取所有的方法 |
|
.getDeclaredMethod(Str, ...Types) |
暴力获取方法 |
clazz.getDeclaredMethod("setName", String.class); |
.getDeclaredMethods() |
暴力获取所有方法 |
|
对Method类进行操作,如果方法不是Public修饰符,那么需要设置setAccessible属性,来开放权限(有够暴力的)
方法名 |
作用 |
例 |
.invoke(Obj obj,Obj... args) |
应用方法 |
setName.invoke(dog,"huahua"); |
.setAccessible(boolean); |
允许该对象修改非Pulice值 |
name.setAccessible(true); |
.getReturnType() |
取得返回值类型 |
|
.getModifiers() |
获取修饰符(以数字来表示) |
1表示public,4表示protected,16表示default,3表示private |
4.获取注解(Annotation)
注解可以从类、方法、构造器,参数上获取到注解,通过这些注解可以获取到重要的代码信息。
方法名 |
作用 |
例 |
.getDeclaredAnnotations() |
获取所有注解 |
|
.getAnnotations() |
获取所有Pulice注解 |
|
.getDeclaredAnnotation(Class<A>) |
暴力获取指定类的注解 |
clazz.getDeclaredAnnotation(Controller.class); |
.getAnnotation(Class<A>) |
获取指定类的注解 |
clazz.getAnnotation(Controller.class); |
2.反射式编程
我们学习了注解方法,也学习了反射,现在我们只需要获得类的加载器,就能读取到他所有的方法、注解、参数,你所能想到的所有都能读取到!@Target({ElementType.METHOD,ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String value() default "";
int age() default 0;
DogType dogType() default DogType.China;
}
先定义了一个枚举类;
public enum DogType {
China("中华田园犬"),
English("英国贵族犬");
private String name;
DogType(String name){
this.name = name;
}
public String getName(){
return name;
}
}
@MyAnnotation("铁蛋")
@Data //来自于Lombok
@NoArgsConstructor //来自于Lombok
public class Dog {
private String name;
@MyAnnotation(age = 2)
protected int age;
@MyAnnotation(dogType = DogType.English)
DogType dogType;
public String color;
}
public class demo01 {
public static void main(String[] args) throws NoSuchFieldException,
NoSuchMethodException,
InvocationTargetException,
InstantiationException,
IllegalAccessException {
Class<Dog> clazz = Dog.class;
//获取类上方的注解,它是小狗的名字
MyAnnotation ant = clazz.getDeclaredAnnotation(MyAnnotation.class);
String name = ant.value();
//获取类字段常量上方的注解,他是小狗的年龄
Field fieldAge = clazz.getDeclaredField("age");
MyAnnotation ant2 = fieldAge.getAnnotation(MyAnnotation.class);
int age = ant2.age();
//获取类字段常量上方的注解,他是小狗的国籍
Field fieldDogType = clazz.getDeclaredField("dogType");
MyAnnotation ant3 = fieldDogType.getAnnotation(MyAnnotation.class);
DogType dogType = ant3.dogType();
Constructor<Dog> constructor = clazz.getConstructor();
Dog dog = constructor.newInstance();
//获取构造器,创建对象
dog.setName(name);
dog.setAge(age);
dog.setDogType(dogType);
//将空值对象注入属性
System.out.println(dog);
//Dog(name=铁蛋, age=2, dogType=English, color=null)
}
}
完成这些练习后,相信你也对注解+反射的框架有了初步的了解!