Java反射

2021-08-28  本文已影响0人  淡季的风

一、简介

image.png

1、概述

Java的反射是指在程序运行过程中, 可以构造任意一个类的对象, 获取任意一个类的的所有属性和方法, 可以调用任意一个类的属性和方法。 这种动态获取程序信息和动态调用程序对象的功能称为Java的反射机制。反射被视为动态语言关键。

通常情况下, 我们想调用一个Java类的属性和方法, 必须先实例化这个Java类的对象, 然后通过对象去调用该类的属性和方法; 通过Java的反射机制, 我们可以在程序运行时, 动态获取类的信息, 调用类的属性和方法, 完成对象的实例化等操作。

如图所示, 介绍什么是反射

image.png

2、反射的使用场景

反射的应用场景主要有:

3、 反射的缺点

二、原理

1、类加载过程

image.png

类加载的完整过程如下:

  1. 在编译时,Java 编译器编译好 .java 文件之后,在磁盘中产生 .class 文件。.class 文件是二进制文件,内容是只有 JVM 能够识别的机器码。

  2. JVM 中的类加载器读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息。类加载器会根据类的全限定名来获取此类的二进制字节流;然后,将字节流所代表的静态存储结构转化为方法区的运行时数据结构;接着,在内存中生成代表这个类的 java.lang.Class 对象。

  3. 加载结束后,JVM 开始进行连接阶段(包含验证、准备、初始化)。经过这一系列操作,类的变量会被初始化。

2、 Class对象

使用反射,必须先获得带操作对象的Class对象。Java中, 无论生成某个类的多少个对象, 这些对象都对应于同一个Class对象。这个Class对象有JVM生成, 通过它能够获悉整个类的所有结构。所以, java.lang.Class可以视为所有反射API的入口点。

反射的本质就是, 在Java运行时, 将Java类的各种成分映射成一个个的Java对象。

举例来说, 加入定义了以下代码:

User user = new User();

步骤说明:

3、方法的反射调用

方法的反射调用, 也就是Method.invoke()方法。

    @CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

Method.invoke()方法实际委派给MethodAccessor执行。MethodAccessor接口有2个具体的实现类:

每个Method实例第一次调用都会生成一个委派实现(DelegatingMethodAccessorImpl), 他所委派的具体实现便是一个本地实现(NativeMethodAccessorImpl)。本地实现非常容易理解, 当进入Java虚拟机内部后, 我们便拥有了Method实例所指向方法的具体地址, 这个时候反射无非是将准备好的参数, 然后调用目标方法。

    private MethodAccessor acquireMethodAccessor() {
        // First check to see if one has been created yet, and take it
        // if so
        MethodAccessor tmp = null;
        if (root != null) tmp = root.getMethodAccessor();
        if (tmp != null) {
            methodAccessor = tmp;
        } else {
            // Otherwise fabricate one and propagate it up to the root
            tmp = reflectionFactory.newMethodAccessor(this);
            setMethodAccessor(tmp);
        }

        return tmp;
    }
    public MethodAccessor newMethodAccessor(Method var1) {
        checkInitted();
        if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
            return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
        } else {
            NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
           // 生成DelegatingMethodAccessorImpl, 实际委派的是NativeMethodAccessorImpl 
            DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
            var2.setParent(var3);
            return var3;
        }
    }
// 一个典型的静态代理模式
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
    private MethodAccessorImpl delegate;

    DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {
        this.setDelegate(var1);
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        return this.delegate.invoke(var1, var2);
    }

    void setDelegate(MethodAccessorImpl var1) {
        this.delegate = var1;
    }
}

Method实例的每次调用都是先调用DelegatingMethodAccessorImplinvoke方法, 在调用NativeMethodAccessorImplinvoke方法。

其实,Java 的反射调用机制还设立了另一种动态生成字节码的实现(下称动态实现),直接使用 invoke 指令来调用目标方法。之所以采用委派实现,便是为了能够在本地实现以及动态实现中切换。动态实现和本地实现相比,其运行效率要快上 20 倍。这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换,但由于生成字节码十分耗时,仅调用一次的话,反而是本地实现要快上 3 到 4 倍。

考虑到许多反射调用仅会执行一次,Java 虚拟机设置了一个阈值 15(可以通过 -Dsun.reflect.inflationThreshold 来调整),当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。

源码如下:

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            this.parent.setDelegate(var3);
        }

        return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
public class ReflectionFactory {
    private static boolean initted = false;
    private static final Permission reflectionFactoryAccessPerm = new RuntimePermission("reflectionFactoryAccess");
    private static final ReflectionFactory soleInstance = new ReflectionFactory();
    private static volatile LangReflectAccess langReflectAccess;
    private static volatile Method hasStaticInitializerMethod;
    private static boolean noInflation = false;
    private static int inflationThreshold = 15;

  static int inflationThreshold() {
        return inflationThreshold;
    }

}

4、反射调用的开销

方法的反射会带来不小的开销, 原有如下:

Class.forName会调用本地方法,Class.getMethod会遍历该类的所有公共方法, 如果没有匹配到, 还会遍历父类的所有公共方法, 可想而知这两个操作都非常费时。

注意,以getMethod为代表的查找方法操作, 会返回查找结果的一份拷贝。因此我们应当在热点代码中避免使用返回Method数组的getMethodsgetDeclareMethods方法,以减少不必要的堆空间消耗。在实践中, 往往会在应用中缓存Class.forNameClass.getMethod的结果。

反射调用自身的性能开销:

第一,由于Method.invoke方法自身是一个可变长参数方法,在字节码层面它的最后一个参数是一个Object数组。Java编译器会在调用处生成一个长度为传入参数数量的Object数组。

第二,Object不能存储基本数据类型, Java编译器会对传入的基本数据类型进行自动装箱、拆箱。

这两个操作都很耗时, 还可能占用堆内存,是的GC频繁。

三、常用API

Java类的成员包括以下三类: 属性字段、构造函数、方法。 反射的API也是和这几个相关。


image.png

通过一个经典的例子, 我们来学习反射。 新建一个Student类:

public class Student {

    private static volatile Student INSTANCE;
    private String name;
    public int age;

    private Student(){}

    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    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;
    }

    private String show(String message){
        System.out.println("show: " + name + "," + age+ "," + message);
        return "testReturnValue";
    }

    public static Student getInstance(){
        if(INSTANCE==null){
            synchronized (Student.class){
                if(INSTANCE==null){
                    INSTANCE = new Student();
                }
            }
        }
        return INSTANCE;
    }
}

该类包含2个成员变量、1个类变量、2个构造器、6个方法,基本覆盖平常所用的所有类成员。

1、java.lang.reflect

Java中java.lang.reflect包提供了反射功能, 它下面的类都没有public构造方法。 它包含的核心接口和类如下(不包含已经介绍过的):

2、获取Class对象

获取Class对象的3种方法:

  1. Class.forName方法:
public class UserClient {
    public static void main(String[] args) throws ClassNotFoundException {
        // 加载一个类
        Class<?> studentClass = Class.forName("org.example.good.reflect.Student");
        System.out.println(studentClass.getCanonicalName());

        // 加载基础类型
        Class<?> arrayClass = Class.forName("[D");
        System.out.println(arrayClass.getCanonicalName());

        // 加载一个类的一维数组, [代表一维数组, L和;之间的代表全限定类名
        Class<?> arrayClass2 = Class.forName("[Lorg.example.good.reflect.Student;");
        System.out.println(arrayClass2.getCanonicalName());

        // 加载一个类的二维数组
        Class<?> arrayClass3 = Class.forName("[[Lorg.example.good.reflect.Student;");
        System.out.println(arrayClass3);


    }
}

使用类的全限定类名来反射对象的类, 常用的场景是JDBC中加载数据库驱动。

  1. 类名 + .class方法:
    直接使用类名+'.class'获取Class对象。
public class UserClient {
    public static void main(String[] args) throws ClassNotFoundException {
        // 加载一个类
        Class<?> studentClass = Class.forName("org.example.good.reflect.Student");
        System.out.println(studentClass.getCanonicalName());

        Class<?> studentClass2 = Student.class;
        System.out.println(studentClass2.getCanonicalName());
        System.out.println(studentClass2 == studentClass);


    }
}
  1. ObjectgetClass方法:
    Object类中有getClass方法,因为所有类都继承自Object类。因此可以调用Object.getClass方法来获取Class对象。
public class UserClient {
    public static void main(String[] args) throws ClassNotFoundException {
        // 加载一个类
        Class<?> studentClass = Class.forName("org.example.good.reflect.Student");
        System.out.println(studentClass.getCanonicalName());

        Student student = new Student("王子", 1);
        Class<?> studentClass3 = student.getClass();
        System.out.println(studentClass3.getCanonicalName());
        System.out.println(studentClass == studentClass3);

    }
}

3、是否某个类的实例

判断是否是某个类的实例,有两种方式:

public class UserClient {
    public static void main(String[] args) throws ClassNotFoundException {
        
        List<String> dataList = new ArrayList<>();
        System.out.println(dataList instanceof List);
        System.out.println(List.class.isInstance(dataList));
    }
}

4、创建类的实例

创建类的实例有2种方法:

        Class<?> clazz1 = Student.class;
        Student student1 = (Student) clazz1.newInstance();
        System.out.println(student1);

        Constructor<?> constructor = clazz1.getConstructor(String.class, Integer.class);
        Student student2 = (Student)constructor.newInstance("王子", 1);
        System.out.println(student2);

注意事项:

5、创建数组实例

数组在Java中是一个特殊的数据类型,它可以复制给一个对象引用。Java中通过Array.newInstance创建数组的实例。利用反射创建数组:

  Class<?> clazz = Integer.class;
        Object object = Array.newInstance(clazz, 5);
        Array.set(object, 0, 1);
        System.out.println(object);
        System.out.println(Array.getLength(object));
        System.out.println(Array.get(object, 4));
        System.out.println(Array.get(object, 0));

java.lang.reflect.Array的源码:

  public static Object newInstance(Class<?> componentType, int length) throws NegativeArraySizeException {
        return newArray(componentType, length);
    }

6、Field

Class对象提供以下方法获取Field

    System.out.println("list public field:");
        Class<?> clazz = Student.class;
        Field[] fields = clazz.getFields();
        for(Field field: fields){
            System.out.println(String.format("field name: %s, type: %s", field.getName(), field.getType().getName()));
        }

        System.out.println("list all field:");
        Field[] fields2 = clazz.getDeclaredFields();
        for(Field field: fields2){
            System.out.println(String.format("field name: %s, type: %s", field.getName(), field.getType().getName()));
        }

        Field field = clazz.getField("age");
        Field field2 = clazz.getDeclaredField("name");
        System.out.println(String.format("field name: %s, type: %s", field.getName(), field.getType()));

        Student student =new Student("", 0);
        System.out.println(String.format("before, field value: %s", field.get(student)));
        field.set(student, 25);
        System.out.println(String.format("after, field value: %s", field.get(student)));

        field2.setAccessible(true);
        field2.set(student, "小可爱");
        System.out.println(String.format("after, field value: %s", field2.get(student)));

// Output
list public field:
field name: age, type: java.lang.Integer
list all field:
field name: INSTANCE, type: org.example.good.reflect.Student
field name: name, type: java.lang.String
field name: age, type: java.lang.Integer
field name: age, type: class java.lang.Integer
before, field value: 0
after, field value: 25
after, field value: 小可爱

7、Method

Class对象提供以下方法获取Method

        Class<?> clazz = Student.class;
        Method method = clazz.getMethod("getAge");
        System.out.println(method.getName());

        Method[] methods = clazz.getDeclaredMethods();
        for(Method method1: methods){
            System.out.println(String.format("%s", method1.getName()));
        }

// Output
// getAge
// getName
// getName
// getInstance
// setName
// setAge
// show
// getAge

获取一个Method对象后, 可以调用invoke方法类调用改方法。

        Student student = (Student)clazz.newInstance();
        Field field = clazz.getDeclaredField("name");
        Method method2 = clazz.getMethod("setAge", int.class);
        method2.invoke(student, 25);
        field.setAccessible(true);
        field.set(student, "Lily");

        Method method1 = clazz.getDeclaredMethod("show", String.class);
        method1.setAccessible(true);
        method1.invoke(student, "hello");
        
        // Output
       show: Lily,25,hello

Method 调用invoke() 的时候,存在许多细节:

invoke() 方法中第一个参数 Object 实质上是 Method 所依附的 Class 对应的类的实例,如果这个方法是一个静态方法,那么 ojb 为 null,后面的可变参数 Object 对应的自然就是参数。

invoke() 返回的对象是 Object,所以实际上执行的时候要进行强制转换。

在对Method调用invoke()的时候,如果方法本身会抛出异常,那么这个异常就会经过包装,由Method统一抛InvocationTargetException。而通过InvocationTargetException.getCause() 可以获取真正的异常。

8、Constructor

Class 对象提供以下方法获取对象的构造方法(Constructor):

获取一个Constructor 对象后,可以用newInstance 方法来创建类实例。

        Constructor<?> constructor = clazz.getConstructor();
        Student student = (Student) constructor.newInstance();

        Constructor<?> constructor1 = clazz.getConstructor(String.class, Integer.class);
        Student student1 = (Student) constructor1.newInstance("lily", 25);

        System.out.println(student.toString());
        System.out.println(student1.toString());

        // Output
        Student{name='null', age=null}
        Student{name='lily', age=25}

9、绕开访问限制

有时候,我们需要通过反射访问私有成员、方法。可以使用 Constructor/Field/Method.setAccessible(true) 来绕开 Java 语言的访问限制。

四、总结

参考资料
https://dunwu.github.io/javacore/basics/java-reflection.html#_4-3-cglib-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86

上一篇下一篇

猜你喜欢

热点阅读