反射(Reflection)属于java中很重要的高级特性,被广泛应用在许多著名的开源框架中,例如Spring家族、mybatis,同时也是注解和动态代理的基础,但现有的许多教程和博客缺乏对反射概念本身清晰的定义和简洁明了的实例,而直接介绍反射API的用法,让人读完后仍对反射认识模糊,一知半解,很难主动去应用。这篇博客尝试对反射进行简洁且全面的介绍。
什么是反射?
反射是指通过对象、类或字符串(类全称)得到类或接口对应的Class实例
,利用这个实例获取类的所有信息和调用成员方法的机制。
可以从字面意思去理解反射,Class实例
包括了类的所有信息,一般知道它就可以创建对象了,而反射是通过对象获取Class实例
,包含了相反的意思,有反射的意味了,可以从这个角度去直观理解java反射的含义。
java反射可以通过下图解释
可以看出,反射在代码层面就是Class类
的使用,所以有必要简单介绍Class
类。
Class类
Class类表示运行时的类和接口(enum
是一种特殊的类,annotation
是一种特殊的接口)。JVM为每种类型(type,此处指比类更宽泛的范围,包括类、接口、数组)在从.class文件中加载时创建唯一的Class对象(Class
没有公开的构造器)。
Class实例获取有三种方式
-
getClass()
,适用于对象 -
.class
属性,适用于类 -
Class.forName()
,类全称字符串。
类的签名如下
public final class Class implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {}
反射实例
以下以Student类为例,来展示反射的用法,包括创建对象和调用成员方法。为了简洁起见,Student类只保留了姓名和学号两个属性,类定义如下:
//lombok插件,用于自动生成getter、setter、ToString、构造方法等
@Data//等价于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode.
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String name;
private String id;
}
通常反射创建Student
的代码如下
@Test
public void createObjectByReflection() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Class> studentClass02= Class.forName("demo.reflection.Student");
Object obj=studentClass02.getConstructor(String.class,String.class).newInstance("小强_Constructor","20200121");
Student student=(Student) obj;
System.out.println(student);
//通过反射修改属性
Field name = student.getClass().getDeclaredField("name");//getField只能访问public的属性
name.setAccessible(true);
name.set(student,"小军_Field");//第一个参数是对象
System.out.println(student);
//通过setter方法去修改属性
Method setName = student.getClass().getDeclaredMethod("setName", String.class);
setName.invoke(student,"小花_Method");//第一个参数是对象
System.out.println(student);
}
可以看出Class实例
对应的是类,而不是对象,通过反射设置属性和调用函数时,都需要提供对象,具体体现为方法的第一个参数为类所实例化的对象。
作为对比,我们可以观察一下直接通过new来创建并修改对象的代码,具体如下所示:
public void createObjectByNew(){
//创建对象
Student student = new Student("小强_Constructor", "20200121");
//修改属性
student.setName("小花_Method");
}
对比两个创建对象的方式,我们可以发现通过反射创建对象和调用方法的方式稍显啰嗦,都需要首先获得要修改的部分(Constructor、Field、Method),然后才能修改。那么为什么反射仍被广泛地应用呢?
反射的优缺点
优点
反射所具有的不可替代的优势表现为以下方面。
-
可以在不知道类的情况下,调用某些方法和设置某些属性,很多框架主要应用这一点。
例如,
Student.class.getDeclaredMethod("setName", String.class)
就是针对setName
方法的,与前面的类没有关系,也就是说,你可以用String、Integer、Boolean
等类来替换Student
,我只关注类中的setName
方法。 -
可以通过表示类全称的字符串以统一的方式去创建对象。提高了程序的通用性和灵活性
主要通过调用
Class.forName()
函数来完成此特性。 反射可以实现注解和动态代理
缺点
反射的缺点主要有以下三个方面
- 带来额外的性能开销。反射涉及到运行时动态解析,JVM无法进行优化
- 一定程度上破坏了封装性。反射可以通过调用
setAccessible(true)
去访问private
修饰的属性和方法。
反射应用之注解
注解(Annotation
)是用于表示元数据(metadata)的标签,被广泛地应用在SpringBoot,Spring MVC中。注解可以在类、接口、方法、属性上使用,用于提供额外的信息。注解不会对代码有直接的影响,只是起到标记的作用,需要编写额外的代码去利用注解,否则注解一无所用。幸运的是,无论是框架还是java编译器已经提供了相应的注解处理代码。
java语言内置的注解如下图所示。
java中自定义注解的语法为@interface
,实例如下
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation{
int value() default 1;
}
注解是一种特殊的注解,其特殊之处在于用严格受限的方法签名去表示属性,注解中对方法签名的限制如下:
- 方法没有参数
- 方法的返回值必须是以下类型:.基本数据类型、String, 枚举类型、注解类型、Class类型、以上类型的一维数组类型
推荐使用default
设定为属性(方法名)设置默认值。
注解的使用
java反射中与注解相关的API包括:
-
判断某个注解是否存在于
Class
、Field
、Method
、Constructor
:Xxx.isAnnotationPresent(Class)
例如判断
@MyAnnotation
注解是否存在于Student
类的代码如下:Student.class.isAnnotationPresent(MyAnnotation.class);
2.读取注解:Xxx.getAnnotation(Class)
; 例如读取@MyAnnotation
注解中的value
属性
MyAnnotation test = Student.class.getAnnotation(MyAnnotation.class);
int vaule = test.vaule();
注解实例
下面我们自定义一个名为@IDAuthenticator
的注解去验证Student
类的学号,要求学号的长度必须为4,对不满足要求的学号抛出异常。
首先我们定义注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface IDAuthenticator {
int length() default 8;
}
然后应用注解:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String name;
@IDAuthenticator(length = 4)
private String id;
}
最后解析注解
public void check(Student student) throws IllegalAccessException {
for(Field field:student.getClass().getDeclaredFields()){
if(field.isAnnotationPresent(IDAuthenticator.class)){
IDAuthenticator idAuthenticator = field.getAnnotation(IDAuthenticator.class);
field.setAccessible(true);
//只有id有@IDAuthenticator注解,
//也只要当field为id时@IDAuthenticator才不为空,才能满足判断条件,
// 体会,注解的本质是标签,起筛选作用
Object value=field.get(student);
if(value instanceof String){
String id=(String) value;
if(id.length()!=idAuthenticator.length()){
throw new IllegalArgumentException("the length of "+field.getName()+" should be "+idAuthenticator.length());
}
}
}
}
测试
@Test
public void useAnnotation(){
Student student01 = new Student("小明", "20210122");
Student student02 = new Student("小军", "2021");
Student student03 = new Student("小花", "20210121");
for(Student student:new Student[]{student01,student02,student03}){
try{
check(student);
System.out.println(" Student "+student+" checks ok ");
} catch (IllegalArgumentException | IllegalAccessException e) {
System.out.println(" Student "+student+" checks failed "+e);
}
}
}
测试结果如下
通过以上实例可以看出,注解的本质就是标记,利用这个标记可以对某些属性和方法进行特殊的处理
反射应用之动态代理
代理是一种设计模式,用于增强一个已存在的类。增强内容包括:日志、参数检查等。代理对象和原对象通常具有相同的方法名。常见的增强方式是实现接口,而动态代理是指在运行时直接创建代理对象,而将所有方法调用派遣给另一个对象的单个方法的机制,其核心是调用处理器接口(InvocationHandler
),其目的是执行被代理的接口方法。这里引入InvocationHandler
接口的目的是为了满足单一职责原则,使得代理对象专心做代理,方法调用交给InvocationHandler
。
代理的本意就是将事情交给别人去做,在计算机科学中,代理在这个本意上有两个延伸,一个是将任务交给别的程序,而自己只需要结果。这个含义主要用于计算机网络,例如VPN,和代理上网;另一个是将对象交给别的程序,而自己需要增强后的对象,这种增强可以表现为日志、用户鉴权、性能监控、事务处理。这个含义用于设计模式中的代理模式和java的动态代理,用UML图表示如下:
在java中代理实例(proxy instance )都关联一个调用处理对象(invocation handler object,实现了InvocationHandler
接口),目的是将方法调用分发到invoke()
方法中。
Each proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.
一个最简单的例子如下:
public class DynamicProxyDemo {
public static void main(String[] args) {
//定义InvocationHandler实例,它负责实现接口的方法调用
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);//方法内容之外的增强
if(method.getName().equals("morning")){
System.out.println("Good morning, "+args[0]);//此处便实现了接口中morning方法的内容
}
return null;
}
};
//2.创建interface实例
Hello hello = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(),//接口的ClassLoader
new Class>[]{Hello.class},//接口
handler);//传入处理调用方法的handler
//使用
hello.morning("Bob");
}
}
interface Hello{
void morning(String name);
}
可以看出,动态代理通过Proxy.newProxyInstance
创建代理对象,而把接口方法委托给了InvocationHandler。所以InvocationHandler的实现类的成员属性一般是接口的实现类。
下面我们定义一个飞翔的接口,有6个类分别实现了这个接口,分别是Helicopter、Jet、Airliner、Eagle、Sparrow、Swan(这里用多个类举例是为了说明动态代理的用途),代码如下。
interface CanFly {
void fly();
}
class Helicopter implements CanFly {
@Override
public void fly() {
System.out.println("我是直升机,我能飞");
}
}
class Jet implements CanFly {
@Override
public void fly() {
System.out.println("我是喷气式飞机,我能飞");
}
}
class Airliner implements CanFly {……}
class Eagle implements CanFly {……}
class Sparrow implements CanFly {……}
class Swan implements CanFly {……}
现在有个需求,统计每个类中fly()
方法的运行时间,一种直观的方式是为每个类生成一个代理类,代码如下:
class HelicopterProxy implements CanFly{
private Helicopter helicopter ;
public HelicopterProxy(Helicopter helicopter) {
this.helicopter = helicopter;
}
@Override
public void fly() {
long start =System.currentTimeMillis();
helicopter.fly();//原有的代码,
long end=System.currentTimeMillis();
System.out.println("running time of fly method is "+ (end-start)+"ms");
}
}
class JetProxy implements CanFly{
private Jet jet ;
public JetProxy(Helicopter helicopter) {
this.jet = jet;
}
@Override
public void fly() {
long start =System.currentTimeMillis();
jet.fly();
long end=System.currentTimeMillis();
System.out.println("running time of fly method is "+ (end-start)+"ms");
}
}
//......
以上代码称为静态代理,可以看到有许多缺点,具体表示如下:
代理类与原实现类强耦合,导致相同逻辑的代码重复出现,实现类过多,会增加很大的编程负担。可以看到
HelicopterProxy
和JetProxy
的唯一不同之处在于实现类不同,其余逻辑完全相同!增强的内容与代理类强耦合,导致不同的增强对应不同的代理类(类数量爆炸)。
为了解决以上问题,就可以使用java的动态代理,其核心是实现InvocationHandler
接口,该接口只有invoke
这一个方法,实现代码如下:
class FlyInvocationHandler implements InvocationHandler{
private final Object obj;
public FlyInvocationHandler(Object obj) {
this.obj=obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result;
long start =System.currentTimeMillis();//aop,增强内容
result=method.invoke(obj,args);
long end=System.currentTimeMillis();
System.out.println("In class "+obj.getClass().getName()+" running time of fly method is "+ (end-start)+"ms");
return result;
}
}
测试代码如下:
public static void flyProxy() {
//动态代理,体会,只是接口的实现内容不同,其余内容都相同!
InvocationHandler handler01=new FlyInvocationHandler(new Helicopter());
InvocationHandler handler02=new FlyInvocationHandler(new Jet());
InvocationHandler handler03=new FlyInvocationHandler(new Airliner());
InvocationHandler handler04=new FlyInvocationHandler(new Eagle());
InvocationHandler handler05=new FlyInvocationHandler(new Sparrow());
InvocationHandler handler06=new FlyInvocationHandler(new Swan());
InvocationHandler[] handlers={handler01,handler02,handler03,handler04,handler05,handler06};
for(InvocationHandler handler:handlers){
CanFly canFly=(CanFly) Proxy.newProxyInstance(CanFly.class.getClassLoader(),
new Class>[]{CanFly.class},
handler);
canFly.fly();
}
}
测试结果如下:
理解动态代理最好的方式是阅读动态生成的字节码,可以使用以下命令将内存中字节码持久化到磁盘上:
public static void main(String[] args) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
helloProxy();
}
好了,本次的内容就到这儿了,如果您觉得以上内容对您帮助,请点赞关注,谢谢。