你确定不了解下 Java 中反射黑魔法吗?
携手创作,共同成长!这是我参与「掘金日新计划 · 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
// 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
}
```
而且需要说明的是,我们每次获取反射的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
//自定义加载器,内部没有额外实现,只是名字的区别 class DelegatingClassLoader extends ClassLoader { DelegatingClassLoader(ClassLoader parent) { super(parent); } } ```
方法注释中,解释的也很清楚:
- 安全风险:避免了在同一个加载器下,加载不同的字节码带来的未知风险。
- 性能考虑:在某些情况下可以提前卸载这些生成的类,从而减少运行时间。(因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的)
最后
呕心沥血万字长文,让我对反射认识的更清晰了,同时也希望能够带给大家帮助~
最简单的易懂的技术干货,好玩的事情,都会在「浅浅同学的开发笔记」分享,快来呀~期待与你共同成长!