美文网首页
Java反射

Java反射

作者: 淡季的风 | 来源:发表于2021-08-28 17:13 被阅读0次

    一、简介

    image.png

    1、概述

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

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

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

    image.png
    • 什么是程序运行时刻Hello.java经过编译器编译,生成字节码文件Hello.Class, 想要运行该程序, 就需要JVM的类加载器加载Hello.class ,然后JVM来运行Hello.class, 程序的运行时刻就是此时刻。
    • 什么是反射, 它的功能反射就是在程序运行期间动态获取Hello。class的属性和方法, 构造它的实例, 这个功能就是反射。

    2、反射的使用场景

    反射的应用场景主要有:

    • 开发通用框架: 反射最重要的用途就是开发各种通用框架。 很多框架(比如Spring)都是配置化的, 通过XML、Yaml等配置JavaBean、Filter等,为了保证框架的通用性, 它们可能需要通过配置文件加载不同的对象或类, 调用不同的方法,这个时候就必须用到反射-运行时动态加载需要加载的对象。

    • 动态代理: 在切面编程(AOP)中, 需要拦截特定的方法, 通常会选择动态代理方式, 此时需要反射来实现。

    • 注解: 注解本身只起到标记作用, 他需要利用反射机制,根据注解标记去调用注解解释器, 执行相应行为。

    • 可扩展功能: 应用程序可以通过使用完全可限定类名创建可扩展性对象实例来使用外部的用户定义类剋。

    3、 反射的缺点

    • 性能开销: 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用程序中频繁调用的代码段中避免。

    • 破坏封装性: 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

    • 内部曝光: * 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。

    二、原理

    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();
    

    步骤说明:

    • JVM加载方法的时候, 遇到new User(), JVM会根据User的全限定类名去加载User.class;
    • JVM回去从本地或网络等寻找User.class文件并加载到内存中。
    • JVM通过类加载器自动创建这个类的Class对象, 并存储在JVM的方法区,注意一个类有且仅有一个Class对象

    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个具体的实现类:

    • NativeMethodAccessorImpl: 本地方法来实现反射调用
    • DelegatingMethodAccessorImpl: 委派方法来实现委托调用

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

    • 生成MethodAccessor
        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;
            }
        }
    
    • DelegatingMethodAccessorImpl源码:
    // 一个典型的静态代理模式
    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、反射调用的开销

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

    • 变长参数方法导致的Object数组
    • 基本类型的自动装箱拆箱
    • 最重要的方法内联

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

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

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

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

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

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

    三、常用API

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


    image.png
    • Field类:提供有关类的属性信息吗,以及它的动态访问权限。它是一个封装反射类的属性的类。
    • Constructor类: 提供类的构造方法信息, 以及对它的动态访问权限。它是一个封装反射类的构造器的类。
    • Method类: 提供类的方法信息,包括抽象方法。它是一个封装反射类的方法的类。
    • Class类: 表示Java应用程序中的类的实例。

    通过一个经典的例子, 我们来学习反射。 新建一个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构造方法。 它包含的核心接口和类如下(不包含已经介绍过的):

    • Member接口:反映关于单个成员(字段或方法)或构造函数的标识信息。
    • Array类:该类提供动态地生成和访问 JAVA 数组的方法。
    • Modifier类:提供了 static 方法和常量,对类和成员访问修饰符进行解码。
    • Proxy类:* 提供动态地生成代理类和类实例的静态方法。

    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、是否某个类的实例

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

    • instanceof的关键字
    • Class.isInstance方法(它是一个native方法)
    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对象的newInstance方法;
    • 使用Constructor对象的newInsatnce方法:
            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);
    

    注意事项:

    • 使用Class对象的newInstance方法时,要求对应的类必须定义一个public的无参构造方法, 如果无参构造方法设置成privateprotected但没有访问权限, 就会报一个IllegalAccessException错误。
    • 使用Constructor对象的newInstance方法时, 构造方法的入参必须不能是基础类型, 否则会报参数不匹配。

    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

    • getField(String name): 根据名称获取公共类成员, 包括父类。
    • getFields(): 获取所有公共类成员, 包括父类。
    • getDeclareField(String name): 获取所有类成员, 不包括父类。
    • getDeclareFields(): 获取所有类成员, 不包括父类。
        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

    • getMethod(String name, Class<?>... parameterTypes): 根据方法名称获取指定公共方法, 包含父类公共方法。其中第一个参数为方法名称, 后面参数时方法入参的Class对象。

    • getMethods(): 获取所有公共方法, 包含父类公共方法。

    • getDeclareMethod(String name, Class<?>... parameterTypes): 根据方法名称获取指定方法, 包含所有公共非公共方法, 但不包含父类方法。其中第一个参数为方法名称, 后面参数时方法入参的Class对象。

    • getDeclareMethod(): 获取所有该类方法, 不包含父类方法。

            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):

    • getConstructor: 返回类的特定 public 构造方法。参数为方法参数对应 Class 的对象。
    • getDeclaredConstructor: 返回类的特定构造方法。参数为方法参数对应 Class 的对象。
    • getConstructors: 返回类的所有 public 构造方法。
    • getDeclaredConstructors: 返回类的所有构造方法。

    获取一个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

    相关文章

      网友评论

          本文标题:Java反射

          本文链接:https://www.haomeiwen.com/subject/xdfovltx.html