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

语言: CN / TW / HK

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

前言

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

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

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

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

What:反射是什么?

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

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

基于这个机制,反射的作用是 - 动态的加载类,动态的获取类的信息(属性,方法,构造器) - 动态的构造对象 - 动态调用类和对象的任意方法,构造器 - 获取泛型信息 - 处理注解 - 动态代理

How:如何使用反射?

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

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

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

```java 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() 方法。

java //第一种,使用 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 。

```java //获取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);

```

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

  • 成员变量

```java //获取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) 大家使用时,根据不同的类型,选择不同的方法。

通过反射获取类的方法

```java //获取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 的成员变量中。

```java public final class Class{ //反射数据数据结构 private static class ReflectionData { volatile Field[] declaredFields; volatile Field[] publicFields; volatile Method[] declaredMethods; volatile Method[] publicMethods; volatile Constructor[] declaredConstructors; volatile Constructor[] 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;

}

```

而且需要说明的是,我们每次获取反射的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() 方法

```java 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 方法进行进一步处理:

java 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() 方法中看看。

```java 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()方法。 ```java 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 机制开关设置,默认是开启的。

```java 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方法中。

java 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

```java 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,而且每个方法都会生成一个类加载器。

```java static Class<?> defineClass(String name, byte[] bytes, int off, int len, final ClassLoader parentClassLoader) { ClassLoader newLoader = AccessController.doPrivileged( new PrivilegedAction() { public ClassLoader run() { return new DelegatingClassLoader(parentClassLoader); } }); return JLA.defineClass(newLoader, name, bytes, null, "ClassDefiner"); }

//自定义加载器,内部没有额外实现,只是名字的区别 class DelegatingClassLoader extends ClassLoader { DelegatingClassLoader(ClassLoader parent) { super(parent); } } ```

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

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

最后

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


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