你确定不了解下 Java 中反射黑魔法吗?

·  阅读 1105

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

前言

反射在Java 中算是黑魔法的存在了。 用一句话来形容「反其道而行之」

很多限制在反射面前,就是形同虚设。 例如我们设置了一个类的成员变量是 private, 目的就是为了不让外部可以随意修改访问。但是呢,使用反射就可以,你说牛不牛。

正因为反射技术的灵活性,所以在各大框架中被频繁的使用,所以在学习的过程中,了解反射的意义对后续框架的学习有很大的帮助。

具体是这么做到的?还是其他更巧妙的用法?想知道的话,就接着看下去吧~

What:反射是什么?

在java中,运行时,只要给定类的名字,能够知道这个类的所有信息,可以构造出指定对象,可以调用它的任意一个属性和方法。

这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

基于这个机制,反射的作用是

  • 动态的加载类,动态的获取类的信息(属性,方法,构造器)
  • 动态的构造对象
  • 动态调用类和对象的任意方法,构造器
  • 获取泛型信息
  • 处理注解
  • 动态代理

How:如何使用反射?

Java 反射机制主要提供了以下功能:

  • 在运行时判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法。

咱们下面,使用反射获取Person 这个类,来为大家一一演示下。

public class Person {

    public String name;
    private int age;
    private List<String> favorities;

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

    public void addFavorite(String favorite) {
        if (favorities == null) {
            favorities = new ArrayList<>();
        }
        favorities.add(favorite);
    }

    private void changeAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", favorities=" + favorities +
                '}';
    }

}
复制代码

获取反射中的Class对象

获取 Class 类对象有三种方法

  • 使用 Class.forName 静态方法。(需要给出该类的全路径名:包名+类名)
  • 使用 .class 方法。
  • 使用类对象的 getClass() 方法。
            //第一种,使用 Class.forName 静态方法。
            Class  clazz1 = Class.forName("reflect.Person");
            //第二种,使用 .class 方法。
            Class  clazz2 = Person.class;
            //第三种,使用类对象的 getClass() 方法。
            Person person =  new Person("浅浅",18);
            Class  clazz3 = person.getClass();
复制代码

其中

  • 基本类型只可以通过 .class 方法获得Class对象。

通过反射创建类对象

很多文章说主要有两种方式:

  • 通过 Class 对象的 newInstance() 方法、
  • 通过 Constructor 对象的 newInstance() 方法。

两者的区别是通过 Class 对象则只能使用默认的无参数构造方法,通过 Constructor 对象创建类对象可以选择特定构造方法。

其实, class.newInstance() 内部实现也是通过 Constructor。而且现在已经标记为 @Deprecated(since="9"),建议使用 Constructor 。

            //获取class对象
            Class clazz = Class.forName("reflect.Person");

            //如果有默认构造方法,就使用这个
            Person p1 = (Person) clazz.getConstructor().newInstance();

            //指定构造方法
            Person p2 = (Person) clazz.getDeclaredConstructor(String.class, int.class).newInstance("dr", 1);

复制代码

通过反射获取类的成员变量

  • 成员变量
            //获取class对象
            Class clazz = Class.forName("reflect.Person");
            //构造对象
            Person p = (Person) clazz.getDeclaredConstructor(String.class, int.class).newInstance("dr", 18);
            
            //获取指定变量 name
            Field nameField = clazz.getField("name");
            //修改变量的值
            nameField.set(p,"张三");
            //获取指定变量 age 
            Field ageField = clazz.getDeclaredField("age");
            //age 是private变量,需要先设置可用
            ageField.setAccessible(true);
            //修改变量的值
            ageField.setInt(p,17);
复制代码

需要注意的是:

  • getField() : 只能获取 public 修饰的变量,如果是私有变量,会报错NoSuchField
  • getDeclaredField() : 可用于获取全部变量,在不知道变量的权限修饰符的时候,建议使用 getDeclaredField(), 比较保险。

修改变量的值的方法分为两类

  • 引用类型 只有一个方法 set(Object obj, Object value)
  • 基本类型 为每个基本类型,都提供了一个方法,例如 setInt(Object obj, int i)setLong(Object obj, long l) 大家使用时,根据不同的类型,选择不同的方法。

通过反射获取类的方法

            //获取class对象
            Class clazz = Class.forName("reflect.Person");
            //构造对象
            Person p = (Person) clazz.getDeclaredConstructor(String.class, int.class).newInstance("dr", 18);
            //获取指定方法 "addFavorite"
            Method method = clazz.getMethod("addFavorite", String.class);
            //调用对象方法
            method.invoke(p, "篮球");

            //获取指定方法 "changeAge"
            Method privateMethod = clazz.getDeclaredMethod("changeAge", int.class);
            //changeAge 是private方法,需要先设置可用
            privateMethod.setAccessible(true);
            privateMethod.invoke(p, 17);
复制代码

同样提供了 getMethod()getDeclaredMethod(),和 Field 作用一样,不赘述了,

调用方法的方式只有一种 invoke(Object obj, Object... args)

Why:反射为什么可以拿到所有的类信息

要想知道这个问题,需要从类的加载机制说起。

当我们写完一个类,它首先是一个 java 文件,首先会被编译成.class 文件,然后在运行时,要使用到这个类的时候,jvm 会开始加载这个 .class 文件到内存中。 **.class **是个二进制文件,里面包含了类的全部信息。当加载到内存的时候,分为两个部分:

  • 方法区:存储类运行时数据结构。
  • 堆:创建相应的 Class 对象

所谓类运行时数据结构,其实就是Java类在 JVM 内存中的一个快照,包括常量池、类字段、类方法等。

Class 对象就是相当于是一个入口,放在堆中,方便去程序员访问方法区的类运行时结构信息。 在这里插入图片描述

也就是说,只要类被加载到 JVM 内存,就可以获取它全部的信息了。

那类加载的时机是什么呢?

其实,虚拟机规范中并没有强制约束何时进行加载,但是规定了5种情况必须对类进行「初始化」,其中就有一条关于反射的:

使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。

那类初始化一定会先执行加载吗

类的生命周期如下: 在这里插入图片描述

其中还规定了 加载、验证、准备、初始化,卸载这5个阶段的先后顺序是确定的。

所以初始化一定会触发加载的。

反射的机制就是基于以上基础。

小问题:同一个类的 Class 对象有几个呢?

还记得上文「获取反射中的Class对象」创建的 clazz1,clazz2,clazz3 吗?你觉得这是3个不同的对象吗?

System.out.println(clazz1 == clazz2); 会输出什么?

System.out.println(clazz2 == clazz3); 会输出什么?

答案是都是同个对象,所以输出的都是 true。 原因是什么呢?我们马上来揭晓~

Java 中类的加载都是由类加载器实现的。在我们开发人员眼中,类加载器可以分为以下几种:

  • 启动类加载器:Bootstrap ClassLoader 它用来加载Java核心类库。使用C/C++语言实现的,嵌套在JVM内部,java程序无法直接操作这个类。
  • 扩展类加载器:Extension ClassLoader java.ext.dirs目录中加载类库,或者从JDK安装目录:jre/lib/ext目录下加载类库
  • 应用程序类加载器:Application Classloader 程序中默认的类加载器,咱们程序写的类,默认都是由它加载完成的。
  • 自定义加载器:User ClassLoader 咱们自己如果有特别需求,实现的加载器

他们的之间的关系是:

在这里插入图片描述

jvm 对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。

将这个过程详细说说的话,就是:

  • 第一步:如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

  • 第二步:如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader

  • 第三步:如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常

这就是我们常说的「双亲委派模型」类加载机制。这样做的好处是,加载器之间具备带有优先级的层次关系,每个加载器负责的 classpath 是不同的,而且每加载一个类,全部的加载器范围判定都是从上到下的,所以可以保证一个Class文件只能被同一类加载器加载一次,所以在堆中的Class都是同个对象。

Class 是如何存储反射数据的?

在Class里有个关键的属性叫做reflectionData,这里主要存的是每次从jvm里获取到的一些类属性,比如方法,字段等。避免每次都要从 JVM 里去获取数据。

这个属性主要是SoftReference的,内存不足的的情况下有可能会被回收。

ReflectionData 中的属性是按需懒加载的。当调用了某个反射方法获取属性时,才会将当前属性数据填充到ReflectionData 的成员变量中。

public final class Class<T>{
   //反射数据数据结构
    private static class ReflectionData<T> {
        volatile Field[] declaredFields;
        volatile Field[] publicFields;
        volatile Method[] declaredMethods;
        volatile Method[] publicMethods;
        volatile Constructor<T>[] declaredConstructors;
        volatile Constructor<T>[] publicConstructors;
        // Intermediate results for getFields and getMethods
        volatile Field[] declaredPublicFields;
        volatile Method[] declaredPublicMethods;
        volatile Class<?>[] interfaces;

        // Cached names
        String simpleName;
        String canonicalName;
        static final String NULL_SENTINEL = new String();

        // Value of classRedefinedCount when we created this ReflectionData instance
        final int redefinedCount;

        ReflectionData(int redefinedCount) {
            this.redefinedCount = redefinedCount;
        }
    }

   //反射数据软应用对象
    private transient volatile SoftReference<ReflectionData<T>> reflectionData;
    
    
   
}

复制代码

而且需要说明的是,我们每次获取反射的Constructor/Method/Field时,都是重新生成的对象。无论是获取全部,还是某个指定的对象,都会执行下 copy() 动作。

以Method为例:

    public Method getMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        Objects.requireNonNull(name);
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
        }
        Method method = getMethod0(name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(methodToString(name, parameterTypes));
        }
        //执行Copy动作
        return getReflectionFactory().copyMethod(method);
    }
复制代码

copyMethod() 最终会调用 Method 类的 copy() 方法

    Method copy() {
        if (this.root != null)
            throw new IllegalArgumentException("Can not copy a non-root Method");

        Method res = new Method(clazz, name, parameterTypes, returnType,
                                exceptionTypes, modifiers, slot, signature,
                                annotations, parameterAnnotations, annotationDefault);
        res.root = this;
        res.methodAccessor = methodAccessor;
        return res;
    }
复制代码

由此可见,我们每次通过调用getDeclaredMethod方法返回的Method对象其实都是一个新的对象,所以不宜多调哦,如果调用频繁最好缓存起来。 同样的道理,也适用于 Constructor和 Method.

Why: setAccessible() 是如何修改访问权限的?

实际上并没有真正修改Method/Filed/Constructor的访问权限,而是通过一个 boolean 值数据 override 跳过权限检查的。

权限的检查,都封装在一个 AccessibleObject的类中,Method/Filed/Constructor 都继承了它。其中提供了 setAccessible()方法来进行权限检查开关设置。

public class AccessibleObject implements AnnotatedElement {

    //默认为false  
    boolean override;

    /**
     * setAccessible()最终会跳到这个方法
     * 修改override的值
     */
    boolean setAccessible0(boolean flag) {
        this.override = flag;
        return flag;
    }
    
}
复制代码

以Method.invoke 为例,其中就判断了 override属性。

 public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        //override == true,就会不执行权限检查
        if (!override) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz,
                        Modifier.isStatic(modifiers) ? null : obj.getClass(),
                        modifiers);
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }
复制代码

Why: 为什么说反射的性能很差?

我们平常很多框架都使用了反射,而反射中最多使用的就是 Method ,所以我们就从这里分析。

进入 Method 的 invoke 方法我们可以看到,一开始是进行了一些权限的检查,最后是调用了 MethodAccessor 类的 invoke 方法进行进一步处理:

    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        //检查权限
        if (!override) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz,
                        Modifier.isStatic(modifiers) ? null : obj.getClass(),
                        modifiers);
        }
        MethodAccessor ma = methodAccessor;  
        if (ma == null) {
            //获取MethodAccessor
            ma = acquireMethodAccessor();
        }
        //实际上调用了 MethodAccessor
        return ma.invoke(obj, args);
    }
复制代码

MethodAccessor 实际上是一个接口,的到底是哪个类对象,所以我们需要进入 acquireMethodAccessor() 方法中看看。

    private MethodAccessor acquireMethodAccessor() {
        //是否存在对应的 MethodAccessor 对象,
        MethodAccessor tmp = null;
        if (root != null) tmp = root.getMethodAccessor();
        if (tmp != null) {
             //存在就复用之前的对象
            methodAccessor = tmp;
        } else {
            // 否则通过ReflectionFactory 创建一个新的。
            tmp = reflectionFactory.newMethodAccessor(this);
            setMethodAccessor(tmp);
        }

        return tmp;
    }
复制代码

关键的实现是 reflectionFactory.newMethodAccessor()方法。

 public MethodAccessor newMethodAccessor(Method method) {
        //检查是否初始化
        checkInitted();

        if (Reflection.isCallerSensitive(method)) {
            Method altMethod = findMethodForReflection(method);
            if (altMethod != null) {
                method = altMethod;
            }
        }

        // use the root Method that will not cache caller class
        Method root = langReflectAccess().getRoot(method);
        if (root != null) {
            method = root;
        }

        //如果是不使用Inflation机制
        if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
            //生成新的 MethodAccessor
            return new MethodAccessorGenerator().
                generateMethod(method.getDeclaringClass(),
                               method.getName(),
                               method.getParameterTypes(),
                               method.getReturnType(),
                               method.getExceptionTypes(),
                               method.getModifiers());
        } else {
        //否则使用NativeMethodAccessorImpl
            NativeMethodAccessorImpl acc =
                new NativeMethodAccessorImpl(method);
            DelegatingMethodAccessorImpl res =
                new DelegatingMethodAccessorImpl(acc);
            acc.setParent(res);
            return res;
        }
    }
复制代码

条件语句上出现了 noInflation,这是个啥呢,从初始化方法 checkInitted() 中可以看到都是从JVM的参数设置中读取的,默认 noInflation = false,也就是说 Inflation 机制开关设置,默认是开启的。

    private static void checkInitted() {
        //省略......

        Properties props = GetPropertyAction.privilegedGetProperties();
        // Inflation 机制开关设置,默认开启,noInflation 默认是false
        String val = props.getProperty("sun.reflect.noInflation");
        if (val != null && val.equals("true")) {
            noInflation = true;
        }
        // Inflation 机制的阈值参数设置,默认是15
        val = props.getProperty("sun.reflect.inflationThreshold");
        if (val != null) {
            try {
                inflationThreshold = Integer.parseInt(val);
            } catch (NumberFormatException e) {
                throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", e);
            }
        }
         //省略......
    }

复制代码

还多了一个inflationThreshold 参数,这是一个Int 类型的数据,默认是15,具体使用在 NativeMethodAccessorImpl 的 invoke方法中。

public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
  // 如果native版本执行超过 -Dsun.reflect.inflationThreshold的的值,默认值是15,则设置DelegatingMethodAccessorImpl的delegate属性为java版本的MethodAccessor实现,即切换为java版本
        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());
          // 切换java版本
            this.parent.setDelegate(var3);
        }
        // native方法
        return invoke0(this.method, var1, var2);
    }
复制代码

注意了,这个地方就是 Inflation 机制的精髓所在。

反射方法调用的版本实现有两个版本:Java 和 Native 。

  • Native: 具体实现是NativeMethodAccessorImpl,实现最终调用native方法 invoke0() 。启动时相对较快,但是运行效率较慢。
  • Java 具体实现是 MethodAccessorGeneratorX,通过ASM技术动态生成一个MethodAccessorImpl 的子类并实现 invoke方法。初始化慢,但是运行效率较快

所以综合两个实现,基本性能考虑,引申出 inflation 机制,即:初次加载字节码实现反射,使用花费时间更短的 native 实现。如果频繁使用,再动态生成一个类作为 java 实现,后续都切换为 java 实现。

另外这其中,还是使用了设计模式之代理模式:

  • MethodAccessor的实现都委托给 DelegatingMethodAccessorImpl
  • DelegatingMethodAccessorImpl 内部通过delegate属性来包装真正实现 invoke 方法的 MethodAccessorImpl
  • Inflation 机制切换实现方法,就是通过修改delegate属性来实现的。

有没有感受到它的妙处~

Java 版本的实现详解

未开启 Inflation 机制和超过native 方法调用次数阈值都是通过MethodAccessorGenerator.generateMethod() 来生成 Java 版的 MethodAccessor 的实现类。

方法很长,简单看一下:

    private MagicAccessorImpl generate(final Class<?> declaringClass,
                                       String name,
                                       Class<?>[] parameterTypes,
                                       Class<?>   returnType,
                                       Class<?>[] checkedExceptions,
                                       int modifiers,
                                       boolean isConstructor,
                                       boolean forSerialization,
                                       Class<?> serializationTargetClass)
    {
        ByteVector vec = ByteVectorFactory.create();
        //字节码工具实现
        asm = new ClassFileAssembler(vec);
        this.declaringClass = declaringClass;
        this.parameterTypes = parameterTypes;
        this.returnType = returnType;
        this.modifiers = modifiers;
        this.isConstructor = isConstructor;
        this.forSerialization = forSerialization;

        asm.emitMagicAndVersion();
        asm.emitShort(add(numCPEntries, S1));

        final String generatedName = generateName(isConstructor, forSerialization);
        asm.emitConstantPoolUTF8(generatedName);
        asm.emitConstantPoolClass(asm.cpi());
        thisClass = asm.cpi();
        if (isConstructor) {
            if (forSerialization) {
                asm.emitConstantPoolUTF8
                    ("jdk/internal/reflect/SerializationConstructorAccessorImpl");
            } else {
                asm.emitConstantPoolUTF8("jdk/internal/reflect/ConstructorAccessorImpl");
            }
        } else {
            asm.emitConstantPoolUTF8("jdk/internal/reflect/MethodAccessorImpl");
        }
        asm.emitConstantPoolClass(asm.cpi());
        superClass = asm.cpi();
        //省略......
        
复制代码

核心逻辑主要是通过 ClassFileAssembler 类来生成字节码,会生成一个 class 文件,名称是 GeneratedMethodAccessorX,这个 X 是调用次数,会递增的。初始化慢的原因就在这里。

生成的 class 文件大概是这个样子的,想当于直接调用。

例如是实现 Person#addFavorite() 方法反射的时候,就会生成如下的 GeneratedMethodAccessorX

package sun.reflect;

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
    public GeneratedMethodAccessor1() {
        super();
    }

   //invoke方法实现
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException {
        // prepare the target and parameters
        if (obj == null) throw new NullPointerException();
        try {
            //对象实例类型强转成 Person
            Person target = (Person) obj;
            if (args.length != 1) throw new IllegalArgumentException();
            String arg0 = (String) args[0];
        } catch (ClassCastException e) {
            throw new IllegalArgumentException(e.toString());
        } catch (NullPointerException e) {
            throw new IllegalArgumentException(e.toString());
        }
        // make the invocation
        try {
            //直接调用对象的方法 "addFavorite"
            target.addFavorite(arg0);
        } catch (Throwable t) {
            throw new InvocationTargetException(t);
        }
    }
}
复制代码

总结起来,Java版的实现就是用空间换时间。

需要特别说明的是,加载Java版本 GeneratedMethodAccessorX的类加载器是专门提供的自定义加载器 DelegatingClassLoader,而且每个方法都会生成一个类加载器。

  static Class<?> defineClass(String name, byte[] bytes, int off, int len,
                                final ClassLoader parentClassLoader)
    {
        ClassLoader newLoader = AccessController.doPrivileged(
            new PrivilegedAction<ClassLoader>() {
                public ClassLoader run() {
                        return new DelegatingClassLoader(parentClassLoader);
                    }
                });
        return JLA.defineClass(newLoader, name, bytes, null, "__ClassDefiner__");
    }
    
//自定义加载器,内部没有额外实现,只是名字的区别
class DelegatingClassLoader extends ClassLoader {
    DelegatingClassLoader(ClassLoader parent) {
        super(parent);
    }
}
复制代码

方法注释中,解释的也很清楚:

  • 安全风险:避免了在同一个加载器下,加载不同的字节码带来的未知风险。
  • 性能考虑:在某些情况下可以提前卸载这些生成的类,从而减少运行时间。(因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的)

最后

呕心沥血万字长文,让我对反射认识的更清晰了,同时也希望能够带给大家帮助~


最简单的易懂的技术干货,好玩的事情,都会在「浅浅同学的开发笔记」分享,快来呀~期待与你共同成长!

分类:
Android
收藏成功!
已添加到「」, 点击更改