关注「Java视界」公众号,获取更多技术干货

反射 及 Reflections反射框架

一、类加载过程

在这里插入图片描述
要知道反射机制,还需要理解类的加载过程。总的来说,类加载的五个过程:加载、验证、准备、解析、初始化。
除了加载(装载)阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。
在这里插入图片描述

(1)装载

加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。有两处需要说明一下:

  1. 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译而来的.class文件
  2. 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。(为什么会有自定义类加载器?一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。)

具体的,在加载阶段,虚拟机主要完成三件事:

  1. 通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。
  2. 将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域),这里只是结构的转化,不涉及其他操作。
  3. 在方法区中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。(这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。)
(2)验证(链接过程)

类的加载过程后生成了类的java.lang.Class对象,接着会进入链接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段,首先是验证:即验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。
这个验证很好理解,加载过程只是完成了.class文件的导入,但导入的文件是否有效有用符合规范就不能保证了,因此需要验证。

(3)准备(链接过程)

准备就是为类的静态变量、静态常量在方法区分配内存,并为静态变量赋默认初值(0值或null值)。如static int a = 100; 静态变量a就会在准备阶段被赋默认值0。(普通的成员变量是在类实例化时候,随对象一起分配在堆内存中。)

(4)解析(链接过程)

将类的二进制数据中的符号引用换为直接引用。(就像学号和学生姓名映射关系一样,利用学号找到姓名再找到这个学生就是符号引用,学号就是符号;利用姓名直接找到学生就是直接引用)

  • 符号引用(Symbolic References):以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  • 直接引用:
    直接引用可以是(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)(3)一个能间接定位到目标的句柄。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
(5)初始化

到了初始化阶段才真正执行Java代码。
类的初始化的主要工作是为静态变量赋程序设定的初值。
如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。

二、什么是反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

反射就是在运行前只知道这个类名称是啥,在运行时才知道要操作的类是什么,并可以在运行时获取类的完整构造,并调用对应的方法。

反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。
「正」常的,我们使用某个类时必定知道它是什么类,是用来做什么的。先导入,然后对这个类进行实例化,之后使用这个类对象进行操作。
「反」过来,反射是开始并不知道我要初始化的类对象是什么,无法使用 new 关键字来创建对象。但是由上面类的加载过程可知,类加载器在加载好某个类的.class文件后,都会在方法区中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口,这就为使用这个类提供大门。(Class对象的由来是将class文件读入内存,并为之创建一个Class对象,每个类只有唯一的Class对象)
总的来说,反射是动态加载,也就是在运行的时候才会加载,而不是在编译的时候。
正常导入后再使用如下:

package Reflect;

public class Ball {
    private int price;
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
}
package Reflect;

public class Demo1 {
    public static void main(String[] args) {
        Ball ball = new Ball();
        ball.setPrice(100);
        System.out.println(ball.getPrice());
    }
}

通过JDK 提供的反射 API 进行反射调用如下:

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 正常调用
        Ball ball = new Ball();
        ball.setPrice(66);
        System.out.println("正常调用" + ball.getPrice());
        // 反射调用
        // (此时只知道这个类叫啥,这个类的内部信息还一无所知)
        Class clz = Class.forName("Reflect.Ball");
        // (通过这个类的Class对象去了解这个类的内部)
        Method setMethod = clz.getMethod("setPrice", int.class);
        Constructor constructor = clz.getConstructor();
        Object object = constructor.newInstance();
        setMethod.invoke(object, 66);
        Method getMethod = clz.getMethod("getPrice");
        System.out.println("反射调用" + getMethod.invoke(object));
    }
}
正常调用66
反射调用66

上面两段代码的执行结果,其实是完全一样的。但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(Ball),而第二段代码则是在运行时通过字符串值才得知要运行的类(Reflect.Ball)。

三、Class对象特点

  1. Class 类的实例对象表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有很多的实例,每个类都有唯一的Class对象。
  2. Class 类没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机自动构造的。也就是说我们不需要创建,JVM已经帮我们创建了。
  3. Class 对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法

四、反射的用途

反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括其modifiers(修饰符),fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。那么我们便可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度;还有动态代理的实现等等。

说完是不是很糊涂?

那就举个例子,例如:在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。另外虽然反射并不能方便你去创建一个对象,但会让代码更加灵活,降低耦合,提高代码的自适应能力。

还是有点糊涂?OK,那就再具体点:

① 假如我需要实例化一个HashMap,代码就会是这样子:

Map<Integer, Integer> map = new HashMap<>();
map.put(1, 1);

② 某一天发现,该段程序不适合用 HashMap 存储键值对,更倾向于用LinkedHashMap存储,怎么办?只能改代码:

Map<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);

③ 假如又有一天,发现数据还是适合用 HashMap来存储,难道又要重新修改源码吗?
发现问题了吗?我们每次改变一种需求,都要去重新修改源码,然后对代码进行编译,打包,再到 JVM 上重启项目。这么些步骤下来,效率非常低。
④ 这种需求频繁变更但变更不大的场景,频繁地更改源码肯定是一种不允许的操作,你可能想到了使用if/else,判断什么时候使用哪一种数据结构,修改成下面的:

public Map<Integer, Integer> getMap(String param) {
    Map<Integer, Integer> map = null;
    if (param.equals("HashMap")) {
        map = new HashMap<>();
    } else if (param.equals("LinkedHashMap")) {
        map = new LinkedHashMap<>();
    } else if (param.equals("WeakHashMap")) {
        map = new WeakHashMap<>();
    }
    return map;
}

通过传入参数param决定使用哪一种数据结构,可以在项目运行时,通过动态传入参数决定使用哪一个数据结构。
貌似解决了,但真的以后都不用改代码了么?
⑤ 如果某一天还想用TreeMap,还是避免不了修改源码。

怎么办?这个时候,反射就派上用场了。
在代码运行之前,不确定将来会使用哪一种数据结构,只有在程序运行时才决定使用哪一个数据类,而反射可以在程序运行过程中动态获取类信息和调用类方法。通过反射构造类实例,代码会演变成下面这样:

public Map<Integer, Integer> getMap(String className) {
    Class clazz = Class.forName(className);
    Consructor con = clazz.getConstructor();
    return (Map<Integer, Integer>) con.newInstance();
}

无论使用什么 Map,只要实现了Map接口,就可以使用全类名路径传入到方法中,获得对应的 Map 实例。例如java.util.HashMap / java.util.LinkedHashMap····如果要创建其它类例如WeakHashMap,也不需要修改上面这段源码。

所以看出反射的好处了吧?
对于在编译期无法确定使用哪个数据类的场景,通过反射可以在程序运行时构造出不同的数据类实例。
不用修改源码,也就是上面说的可以使代码更灵活,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度。

五、反射机制的相关类

类名用途
Class类代表类的实体,在运行的Java应用程序中表示类和接口
Field类代表类的成员变量(成员变量也称为类的属性)
Method类代表类的方法
Constructor类代表类的构造方法

Class类

包含以下方法(黄色标注为分界,获得类、类中属性、类中注解、类中构造器、类中方法):

类名用途
asSubclass(Class clazz)把传递的类的对象转换成代表其子类的对象
Cast把对象转换成代表类或是接口的对象
getClassLoader()获得类的加载器
getClasses()返回一个数组,数组中包含该类中所有公共类和接口类的对象
getDeclaredClasses()返回一个数组,数组中包含该类中所有类和接口类的对象,包括private 声明的和继承类
forName(String className)根据类名返回类的对象
getName()获得类的完整路径名字
newInstance()创建类的实例
getPackage()获得类的包
getSimpleName()获得类的名字
getSuperclass()获得当前类继承的父类的名字
getInterfaces()获得当前类实现的类或是接口
getField(String name)获得某个公有的属性对象
getFields()获得所有公有的属性对象
getDeclaredField(String name)获得某个属性对象
getDeclaredFields()获得所有属性对象
getAnnotation(Class<?> annotationClass)返回该类中与参数类型匹配的公有注解对象
getAnnotations()返回该类所有的公有注解对象
getDeclaredAnnotation(Class<?> annotationClass)返回该类中与参数类型匹配的所有注解对象
getDeclaredAnnotations()返回该类所有的注解对象
getConstructor(Class…<?> parameterTypes)获得该类中与参数类型匹配的公有构造方法
getConstructors()获得该类的所有公有构造方法
getDeclaredConstructor(Class…<?> parameterTypes)获得该类中与参数类型匹配的构造方法
getDeclaredConstructors()获得该类所有构造方法
getMethod(String name, Class…<?> parameterTypes)获得该类某个公有的方法
getMethods()获得该类所有公有的方法
getDeclaredMethod(String name, Class…<?> parameterTypes)获得该类某个方法
getDeclaredMethods()获得该类所有方法
isAnnotation()如果是注解类型则返回true
isAnnotationPresent(Class<? extends Annotation> annotationClass)如果是指定类型注解类型则返回true
isAnonymousClass()如果是匿名类则返回true
isArray()如果是一个数组类则返回true
isEnum()如果是枚举类则返回true
isInstance(Object obj)如果obj是该类的实例则返回true
isInterface()如果是接口类则返回true
isLocalClass()如果是局部类则返回true
isMemberClass()如果是内部类则返回true

Field类

类名用途
equals(Object obj)属性与obj相等则返回true
get(Object obj)获得obj中对应的属性值
set(Object obj, Object value)设置obj中对应属性值

Method类

类名用途
invoke(Object obj, Object… args)传递object对象及参数调用该对象对应的方法

Constructor类

类名用途
newInstance(Object… initargs)根据传递的参数创建类的对象

下面利用代码来尝试调用:

package reflect;

public class Ball {
    private int price;
    public String name;
    private String count;
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getCount() {
        return count;
    }
    public void setCount(String count) {
        this.count = count;
    }
}
public class Demo3 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class clz = Class.forName("reflect.Ball");
        System.out.println("1、 " + clz.getField("name"));
        System.out.println("2、 " + clz.getName());
        Field fields [] = clz.getFields();
        for (int i = 0; i < fields.length; i++) {
            System.out.println("field: " + i + "、 " + fields[i]);
        }
        Method methods [] = clz.getMethods();
        for (int i = 0; i < methods.length; i++) {
            System.out.println("method: " + i + "、 " + methods[i]);
        }
        System.out.println("3、 " + clz.isArray());
        System.out.println("4、 " + clz.isEnum());
        System.out.println("5、 " + clz.isInterface());
        System.out.println("6、 " + clz.isLocalClass());
        System.out.println("7、 " + clz.isMemberClass());
    }
}
1、 public java.lang.String reflect.Ball.name
2、 reflect.Ball
field: 0、 public java.lang.String reflect.Ball.name
method: 0、 public java.lang.String reflect.Ball.getName()
method: 1、 public void reflect.Ball.setName(java.lang.String)
method: 2、 public void reflect.Ball.setPrice(int)
method: 3、 public int reflect.Ball.getPrice()
method: 4、 public void reflect.Ball.setCount(java.lang.String)
method: 5、 public java.lang.String reflect.Ball.getCount()
method: 6、 public final void java.lang.Object.wait() throws java.lang.InterruptedException
method: 7、 public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method: 8、 public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
method: 9、 public boolean java.lang.Object.equals(java.lang.Object)
method: 10、 public java.lang.String java.lang.Object.toString()
method: 11、 public native int java.lang.Object.hashCode()
method: 12、 public final native java.lang.Class java.lang.Object.getClass()
method: 13、 public final native void java.lang.Object.notify()
method: 14、 public final native void java.lang.Object.notifyAll()
3false
4false
5false
6false
7false

六、获取class文件对象的三种方式

可以概括为下面3种:

  1. Object类的getClass()方法
  2. 静态属性class
  3. Class类中静态方法forName()
(1)Object类的getClass()方法
public class Demo2 {
    public void get(Ball b1, Ball b2) {
        // 1. Object类的getClass()方法
        Class clz1 = b1.getClass();
        Class clz2 = b2.getClass();
        System.out.println(b1 == b2);     // false
        System.out.println(clz1 == clz2); // true
    }
}

通常应用在:比如你传过来一个 Object类型的对象,而我不知道你具体是什么类,用这种方法。

(2)静态属性class
public class Demo2 {
    public static void main(String[] args) {
        // 1. Object类的getClass()方法
        Ball b1 = new Ball();
        Class clz1 = b1.getClass();
        Ball b2 = new Ball();
        Class clz2 = b2.getClass();
        System.out.println(b1 == b2);     // false
        System.out.println(clz1 == clz2); // true

        // 2. 静态属性class
        Class clz3 = Ball.class;
        System.out.println(clz1 == clz3); // true
    }
}

该方法最为安全可靠,程序性能更高,这种方法只适合在编译前就知道操作的 Class。

(3)Class类中静态方法forName()
public class Demo2 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 1. Object类的getClass()方法
        Ball b1 = new Ball();
        Class clz1 = b1.getClass();
        Ball b2 = new Ball();
        Class clz2 = b2.getClass();
        System.out.println(b1 == b2);     // false
        System.out.println(clz1 == clz2); // true

        // 2. 静态属性class
        Class clz3 = Ball.class;
        System.out.println(clz1 == clz3); // true

        // 3. Class类中静态方法forName()
        Class clz4 = Class.forName("reflect.Ball");
        System.out.println(clz4 == clz1); // true
    }
}

这里需要注意Class.forName(“reflect.Ball”);时需要ClassNotFoundException异常处理。这种方式用的最多,尤其是知道某类的全路径名时。

七、反射处理步骤

// 1、获取类的 Class 对象实例, 该对象表示正在运行的类和接口
Class clz = Class.forName("reflect.Ball");
// 2、根据 Class 对象实例获取 Constructor 对象
Constructor constructor = clz.getConstructor();
// 3、使用 Constructor 对象的 newInstance 方法获取反射类对象
Object object = constructor.newInstance();
// 4、获取方法的 Method 对象
Method setMethod = clz.getMethod("setPrice", int.class);
// 5、利用 invoke 方法调用方法
setMethod.invoke(object, 100);

前3步就是通过反射创建类对象的过程。

八、反射获得的对象与new对象区别

  1. 在使用反射的时候,必须确保这个类已经加载并已经链接了。因为反射是基于class对象的,由上面类的加载过程可以知道,只有加载后JVM才会创建这个类的class对象。
  2. new关键字可以调用任何public构造方法,而反射只能调用无参构造方法
  3. new关键字是强类型的,效率相对较高。 反射是弱类型的,效率低。

九、如何使用反射创建一个对象?

方法一:使用Class的newInstance()方法,仅适用于无参构造方法;
示例代码:
Class clazz=(Class)Class.forName(“cn.bjsxt.bean.User”);
User u=clazz.newInstance();
方法二:用Constructor的newInstance()方法,适用所有构造方法;
示例代码:
Class clazz=(Class)Class.forName(“cn.bjsxt.bean.User”);
Constructor cons = clazz.getConstructor(int.class,int.class, String.class,String.class);
User u= cons.newInstance( 1001,19,“王一一”,“男”);

十、反射的应用场景

反射常见的应用场景有三个:

  1. Spring 实例化对象:当程序启动时,Spring 会读取配置文件applicationContext.xml并解析出里面所有的标签实例化到IOC容器中。
  2. 反射 + 工厂模式:通过反射消除工厂中的多个分支,如果需要生产新的类,无需关注工厂类,工厂类可以应对各种新增的类,反射可以使得程序更加健壮。
  3. JDBC连接数据库:使用JDBC连接数据库时,指定连接数据库的驱动类时用到反射加载驱动类

10.1 Spring中对反射机制的利用

Spring中利用了大量反射,以第七节反射处理步骤为例,Spring中若调用set方法大致会做如下处理:
1、先导入类的路径

bean.xml
<bean id="id" class="reflect.Ball">
    <property name="price" value="100" />
</bean>

2、Spring创建该类的实例,并注入值:

Class c = Class.forName("reflect.Ball");
Object bean = c.newInstance();

3、通过一些操作获取对price对应的setter方法名

String setname = "set" + "Price";
Method method = c.getMehod(setprice,int.Class);
method.invoke(bean,"100");

这样就完成了最基本的注入操作。
当然Spring还可以通过构造函数进行注入,或者Class还可以访问Annotation,这样Spring使用注解时就完成注入的功能。

10.2 反射 + 抽象工厂模式

传统的工厂模式,如果需要生产新的子类,需要修改工厂类,在工厂类中增加新的分支:

public class MapFactory {
    public Map<Object, object> produceMap(String name) {
        if ("HashMap".equals(name)) {
            return new HashMap<>();
        } else if ("TreeMap".equals(name)) {
            return new TreeMap<>();
        } // ···
    }
}

反射 + 抽象工厂的核心思想是:在运行时通过参数传入不同子类的全限定名获取到不同的 Class 对象,调用 newInstance() 方法返回不同的子类。
例如,在运行时才确定使用哪一种 Map 结构,我们可以利用反射传入某个具体 Map 的全限定名,实例化一个特定的子类:

public class MapFactory {
    /**
     * @param className 类的全限定名
     */
    public Map<Object, Object> produceMap(String className) {
        Class clazz = Class.forName(className);
        Map<Object, Object> map = clazz.newInstance();
        return map;
    }
}

className 可以指定为 java.util.HashMap,或者 java.util.TreeMap 等等,根据业务场景来定。 工厂模式(简单工厂模式、工厂模式、抽象工厂模式) 中也有相关的例子供参考。

可以看到,利用反射和工厂模式相结合,在产生新的子类时,工厂类不用修改任何东西,可以专注于子类的实现。

10.3 JDBC 加载数据库驱动类

在导入第三方库时,JVM不会主动去加载外部导入的类,而是等到真正使用时,才去加载需要的类,正是如此,我们可以在获取数据库连接时传入驱动类的全限定名,交给 JVM 加载该类。
application.yml中的数据库配置:
在这里插入图片描述
这里的 driver-class-name,因为MySQL版本不同引起的驱动类不同,这使用反射的好处:不需要修改源码,仅加载配置文件就可以完成驱动类的替换。

十一、Reflections 反射框架

当你需要利用反射技术找到classpath目录下所有包含指定注解的类,然后根据注解配置完成指定的功能时,可以选择一个非常好用的反射框架:reflections。
Reflections通过扫描classpath,索引元数据,并且允许在运行时查询这些元数据。
使用Reflections可以很轻松的获取以下元数据信息:

  • 获取某个类型的全部子类
  • 获取带有特定注解的全部信息(类型、构造器、方法,字段)
  • 获取所有能匹配某个正则表达式的资源
  • 获取所有带有特定签名的方法,包括参数,参数注解,返回类型
  • 获取所有方法的名字
  • 获取代码里所有字段、方法名、构造器的使用

Maven依赖 :

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.11</version>
</dependency>
// 实例化Reflections,并指定要扫描的包名
Reflections reflections = new Reflections("my.project");
// 获取某个类的所有子类
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
// 获取包含某个注解的所有类
Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(SomeAnnotation.class);

//ResourcesScanner 扫描资源
Set<String> properties = 
    reflections.getResources(Pattern.compile(".*\\.properties"));

//MethodAnnotationsScanner 扫描方法(构造方法)注解
Set<Method> resources =
    reflections.getMethodsAnnotatedWith(javax.ws.rs.Path.class);
Set<Constructor> injectables = 
    reflections.getConstructorsAnnotatedWith(javax.inject.Inject.class);

//FieldAnnotationsScanner 扫描字段注解
Set<Field> ids = 
    reflections.getFieldsAnnotatedWith(javax.persistence.Id.class);

//MethodParameterScanner 扫描方法参数
Set<Method> someMethods =
    reflections.getMethodsMatchParams(long.class, int.class);
Set<Method> voidMethods =
    reflections.getMethodsReturn(void.class);
Set<Method> pathParamMethods =
    reflections.getMethodsWithAnyParamAnnotated(PathParam.class);

//MemberUsageScanner 扫描方法调用情况
Set<Member> usages = 
    reflections.getMethodUsages(Method.class)

十二、总结

优点
增加程序的灵活性:面对需求变更时,可以灵活地实例化不同对象。

缺点

  • 破坏类的封装性:可以强制访问 private 修饰的信息
  • 性能损耗:反射相比直接实例化对象、调用方法、访问变量,中间需要非常多的检查步骤和解析步骤,JVM无法对它们优化。

增加代码的灵活性

例如上面说的在springboot项目中使用application.properties配置文件指定数据源:
某一天,原来的数据源不符合现在的项目需求了,要换成 MysqlDataSource,那么只需要修改配置文件,重新加载配置文件,并重启项目,就可以完成数据源的切换。

这个过程中不需要修改代码。

原因是Spring Boot 底层封装好了连接数据库的数据源配置,利用反射,适配各个数据源:

private Class<? extends DataSource> type;

public void setType(Class<? extends DataSource> type) {
    this.type = type;
}

DataSourceProperties 类中,发现使用setType() 将全类名转化为 Class 对象注入到type成员变量当中。在连接并监控数据库时,就会使用指定的数据源操作。Class对象指定了泛型上界 DataSource,即只要是DataSource 接口的实现类即可:
在这里插入图片描述
可以看到,无论指定使用哪一种数据源,都只需要与配置文件打交道,而无需更改源码,这就是反射的灵活性!

破坏类的封装性

反射可以获取类中被private修饰的变量、方法和构造器,这违反了面向对象的封装特性,因为被 private 修饰意味着不想对外暴露,只允许本类访问,而setAccessable(true)可以无视访问修饰符的限制,外界可以强制访问。

public class SmallPineapple {
    public String name;
    public int age;
    private double weight; // 体重只有自己知道
    
    public SmallPineapple(String name, int age, double weight) {
        this.name = name;
        this.age = age;
        this.weight = weight;
    }
}
SmallPineapple sp = new SmallPineapple("小菠萝", 21, "54.5");
Clazz clazz = Class.forName(sp.getClass());
Field weight = clazz.getDeclaredField("weight");
weight.setAccessable(true);
System.out.println("窥觑到小菠萝的体重是:" + weight.get(sp));

虽然 weight 属性理论上只有自己知道,但是如果经过反射,在反射面前变得一览无遗。

性能损耗

在直接 new 对象并调用对象方法和访问属性时,编译器会在编译期提前检查可访问性,如果尝试进行不正确的访问,IDE会提前提示错误,例如参数传递类型不匹配,非法访问 private 属性和方法。

而在利用反射操作对象时,编译器无法提前得知对象的类型,访问是否合法,参数传递类型是否匹配。只有在程序运行时调用反射的代码时才会从头开始检查、调用、返回结果,JVM也无法对反射的代码进行优化。
虽然反射具有性能损耗的特点,但是我们不能一概而论,产生了使用反射就会性能下降的思想,反射的慢,需要同时调用上100W次才可能体现出来,在几次、几十次的调用,并不能体现反射的性能低下。所以不要一味地戴有色眼镜看反射,在单次调用反射的过程中,性能损耗可以忽略不计。如果程序的性能要求很高,那么尽量不要使用反射。

补充一、反射到底慢在哪?

反射的确会导致性能问题。

反射导致的性能问题是否严重跟使用的次数有关系,如果控制在100次以内,基本上没什么差别,如果调用次数超过了100次,性能差异会很明显。
在这里插入图片描述
四种访问方式,直接访问实例的方式效率最高;其次是直接调用方法的方式,耗时约为直接调用实例的1.4倍;接着是通过反射访问实例的方式,耗时约为直接访问实例的3.75倍;最慢的是通过反射访问方法的方式,耗时约为直接访问实例的6.2倍。

反射到底慢在哪?

getMethod和getDeclaredField方法会比invoke和set方法耗时。

不要过于频繁地使用反射,大量地使用反射会带来性能问题。

posted @ 2022-06-25 14:02  沙滩de流沙  阅读(245)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货