深入理解 Java 反射

题外话

最近公司创建了技术部的公众号用来鼓励大家进行分享,很多同学比较纠结,觉得找不到比较适合聊的 topic。总的来说大概两个原因:一个是觉得太基础讲出来比较 low 没有人会关注,另一个是讲一些很牛的新技术又怕出错;然而每一项技术在自己的应用中都会有你自己独特的视角,也许这一点正是别人关心的。我个人认为分享一些我们在编码中经常会碰到,而大多数人可能知其然而不知其所以然的话题是很有意义的,今天我打算分享下我们 Java 中一个经常用到的工具, 反射  和  动态代理

当我们在 IDE 中编写代码的时候,打一个点号,IDE 会自动弹出对应的属性和方法名。当我们在 debug 的时候,IDE 会将方法运行时方法内局部变量和外部实例上属性的值都展示出来,spring 中的 IOC 和 AOP,以及一个 RPC 框架中,我们反序列化,consumer 的代理,以及 provider 的调用都会用到 Java 的反射功能,有人说使用反射会慢,那么到底慢在哪里呢?

反射

反射使 Java 语言有了动态编译的功能,也就是在我们编码的时候不需要知道对象的具体类型,但是在运行期可以通过 Class.forName() 获取一个类的 class 对象,在通过newInstance 获取实例。

先看下 java.lang.reflect 包下的几个主要类的关系图,当然动态代理的工具类也在该包下。

图 1

  • AnnotatedElement

作为顶级接口,这个接口提供了获取注解相关的功能,我们在方法,类,属性,构造方法上都可以加注解,所以下面的 Field,Method,Constructor 都有实现这个接口,以下是我们经常用的两个方法,jdk8 以后接口里面可以通过 default 修饰方法实现了。

 1Annotation[] getAnnotations(); //获取目标对象(方法和属性)上的所有注解
2default T getDeclaredAnnotation(Class annotationClass) {
3 Objects.requireNonNull(annotationClass);
4 // Loop over all directly-present annotations looking for a matching one
5 for (Annotation annotation : getDeclaredAnnotations()) {
6 if (annotationClass.equals(annotation.annotationType())) {
7 // More robust to do a dynamic cast at runtime instead
8 // of compile-time only.
9 return annotationClass.cast(annotation);
10 }
11 }
12 return null;
13 }
  • GenericDeclaration

提供了获取泛型相关的功能,只有方法和构造方法上支持泛型,所以只有 Method,Constructor 实现了该接口

– Member

作为一个对象内部方法和属性的声明的抽象,包含了名称,修饰符,所在的类,其中修饰符包含了 static final public private volatile 等,通过一个整数表示,每一个类型在二进制中占一个位。

 1public Class getDeclaringClass();
2public String getName();
3public int getModifiers();
4
5以下为Modifier类部分代码
6
7public static final int PUBLIC = 0x00000001;
8public static final int PRIVATE = 0x00000002;
9public static final int PROTECTED = 0x00000004;
10public static final int STATIC = 0x00000008;
11public static final int FINAL = 0x00000010;
12public static final int SYNCHRONIZED = 0x00000020;
13public static final int VOLATILE = 0x00000040;
14public static final int TRANSIENT = 0x00000080;
15public static final int NATIVE = 0x00000100;
16public static final int INTERFACE = 0x00000200;
17public static final int ABSTRACT = 0x00000400;
18public static final int STRICT = 0x00000800;
19public static boolean isPublic(int mod) {
20 return (mod & PUBLIC) != 0;
21}
  • AccessibleObject

这是一个类,提供了权限管理的功能,例如是否允许在反射中在外部调用一个 private 方法,获取一个 private 属性的值,所以 method,constructor, field 都继承该类,下面这段代码展示了如何在反射中访问一个私有的成员变量,class 对象的构造方法不允许对外。

 1private static void setAccessible0(AccessibleObject obj, boolean flag)
2 throws SecurityException
3
{
4 if (obj instanceof Constructor && flag == true) {
5 Constructor c = (Constructor)obj;
6 if (c.getDeclaringClass() == Class.class) {
7 throw new SecurityException("Cannot make a java.lang.Class" +
8 " constructor accessible");
9 }
10 }
11 obj.override = flag;
12}
13
14boolean override;
15
16public boolean isAccessible() {
17 return override;
18}

以下为 Field 里面通过 field.get (原始对象) 获取属性值得实现,先通过 override 做校验,如果没有重载该权限,则需要校验访问权限。

 1public Object get(Object obj)
2 throws IllegalArgumentException, IllegalAccessException
3{
4 if (!override) {
5 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
6 Class caller = Reflection.getCallerClass();
7 checkAccess(caller, clazz, obj, modifiers);
8 }
9 }
10 return getFieldAccessor(obj).get(obj);
11}

下面我们看看如何通过反射修改 Field 里面属性的值。

通过上面的代码,我们可以看出 jdk 将 Field 属性的读取和写入委托给 FieldAccessor,那么如何获取 FieldAccessor 呢 ?

 1class UnsafeFieldAccessorFactory {
2 UnsafeFieldAccessorFactory() {
3 }
4
5 static FieldAccessor newFieldAccessor(Field var0, boolean var1) {
6 Class var2 = var0.getType();
7 boolean var3 = Modifier.isStatic(var0.getModifiers());
8 boolean var4 = Modifier.isFinal(var0.getModifiers());
9 boolean var5 = Modifier.isVolatile(var0.getModifiers());
10 boolean var6 = var4 || var5;
11 boolean var7 = var4 && (var3 || !var1);
12
13 if (var3) {
14 UnsafeFieldAccessorImpl.unsafe.ensureClassInitialized(var0.getDeclaringClass());
15
16 return (FieldAccessor) ((!var6)
17 ? ((var2 == Boolean.TYPE)
18 ? new UnsafeStaticBooleanFieldAccessorImpl(var0)
19 : ((var2 == Byte.TYPE)
20 ? new UnsafeStaticByteFieldAccessorImpl(var0)
21 : ((var2 == Short.TYPE)
22 ? new UnsafeStaticShortFieldAccessorImpl(var0)
23 : ((var2 == Character.TYPE)
24 ? new UnsafeStaticCharacterFieldAccessorImpl(var0)
25 : ((var2 == Integer.TYPE)
26 ? new UnsafeStaticIntegerFieldAccessorImpl(var0)
27 : ((var2 == Long.TYPE)
28 ? new UnsafeStaticLongFieldAccessorImpl(var0)
29 : ((var2 == Float.TYPE)
30 ? new UnsafeStaticFloatFieldAccessorImpl(var0)
31 : ((var2 == Double.TYPE)
32 ? new UnsafeStaticDoubleFieldAccessorImpl(var0)
33 : new UnsafeStaticObjectFieldAccessorImpl(var0)))))))))
34 : ((var2 == Boolean.TYPE)
35 ? new UnsafeQualifiedStaticBooleanFieldAccessorImpl(var0, var7)
36 : ((var2 == Byte.TYPE)
37 ? new UnsafeQualifiedStaticByteFieldAccessorImpl(var0, var7)
38 : ((var2 == Short.TYPE)
39 ? new UnsafeQualifiedStaticShortFieldAccessorImpl(var0, var7)
40 : ((var2 == Character.TYPE)
41 ? new UnsafeQualifiedStaticCharacterFieldAccessorImpl(var0, var7)
42 : ((var2 == Integer.TYPE)
43 ? new UnsafeQualifiedStaticIntegerFieldAccessorImpl(var0, var7)
44 : ((var2 == Long.TYPE)
45 ? new UnsafeQualifiedStaticLongFieldAccessorImpl(var0, var7)
46 : ((var2 == Float.TYPE)
47 ? new UnsafeQualifiedStaticFloatFieldAccessorImpl(var0, var7)
48 : ((var2 == Double.TYPE)
49 ? new UnsafeQualifiedStaticDoubleFieldAccessorImpl(var0, var7)
50 : new UnsafeQualifiedStaticObjectFieldAccessorImpl(var0, var7))))))))));
51 } else {
52 return (FieldAccessor) ((!var6)
53 ? ((var2 == Boolean.TYPE)
54 ? new UnsafeBooleanFieldAccessorImpl(var0)
55 : ((var2 == Byte.TYPE) ? new UnsafeByteFieldAccessorImpl(var0)
56 : ((var2 == Short.TYPE)
57 ? new UnsafeShortFieldAccessorImpl(var0)
58 : ((var2 == Character.TYPE)
59 ? new UnsafeCharacterFieldAccessorImpl(var0)
60 : ((var2 == Integer.TYPE)
61 ? new UnsafeIntegerFieldAccessorImpl(var0)
62 : ((var2 == Long.TYPE) ? new UnsafeLongFieldAccessorImpl(var0)
63 : ((var2 == Float.TYPE)
64 ? new UnsafeFloatFieldAccessorImpl(var0)
65 : ((var2 == Double.TYPE) ? new UnsafeDoubleFieldAccessorImpl(var0)
66 : new UnsafeObjectFieldAccessorImpl(var0)))))))))
67 : ((var2 == Boolean.TYPE)
68 ? new UnsafeQualifiedBooleanFieldAccessorImpl(var0, var7)
69 : ((var2 == Byte.TYPE)
70 ? new UnsafeQualifiedByteFieldAccessorImpl(var0, var7)
71 : ((var2 == Short.TYPE)
72 ? new UnsafeQualifiedShortFieldAccessorImpl(var0, var7)
73 : ((var2 == Character.TYPE)
74 ? new UnsafeQualifiedCharacterFieldAccessorImpl(var0, var7)
75 : ((var2 == Integer.TYPE)
76 ? new UnsafeQualifiedIntegerFieldAccessorImpl(var0, var7)
77 : ((var2 == Long.TYPE)
78 ? new UnsafeQualifiedLongFieldAccessorImpl(var0, var7)
79 : ((var2 == Float.TYPE)
80 ? new UnsafeQualifiedFloatFieldAccessorImpl(var0, var7)
81 : ((var2 == Double.TYPE)
82 ? new UnsafeQualifiedDoubleFieldAccessorImpl(var0, var7)
83 : new UnsafeQualifiedObjectFieldAccessorImpl(var0, var7))))))))));
84 }
85 }
86}

以上代码可以发现,通过工厂模式根据 field 属性类型以及是否静态来获取,为什么会有这样的划分呢?

首先,jdk 是通过 UNSAFE 类对堆内存中对象的属性进行直接的读取和写入,要读取和写入首先需要确定属性所在的位置,也就是相对对象起始位置的偏移量,而静态属性是针对类的不是每个对象实例一份,所以静态属性的偏移量需要单独获取。

其实通过该偏移量我们可以大致推断出一个实例内每个属性在堆内存的相对位置,以及分别占用多大的空间,有了位置信息,我们还需要这个字段的类型,以方便执行器知道读几个字节的数据,并且如何进行解析,目前提供了 8 大基础类型(char vs Charector)和数组和普通引用类型。

Java 虚拟机为了保证每个对象所占的空间都是 8 个字节倍数,有时候为了避免两个volatile 字段存放在同一个缓存行,所以有时候会再某些字段上做空位填充。

以下为 UnSafe 类的部分代码

 1public final class Unsafe {
2 private static final Unsafe theUnsafe;
3 private Unsafe() {
4 }
5
6 @CallerSensitive
7 public static Unsafe getUnsafe() {
8 Class var0 = Reflection.getCallerClass();
9 if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
10 throw new SecurityException("Unsafe");
11 } else {
12 return theUnsafe;
13 }
14 }
15
16 public native int getInt(Object var1, long var2);
17 public native void putInt(Object var1, long var2, int var4);
18 public native Object getObject(Object var1, long var2);
19 public native void putObject(Object var1, long var2, Object var4);
20 public native boolean getBoolean(Object var1, long var2);
21 public native void putBoolean(Object var1, long var2, boolean var4);
22 public native byte getByte(Object var1, long var2);
23
24 public native long objectFieldOffset(Field var1);
25@Deprecated
26public int fieldOffset(Field var1) {
27 return Modifier.isStatic(var1.getModifiers())?(int)this.staticFieldOffset(var1):(int)this.objectFieldOffset(var1);
28}

然后我们在来看看通过反射来调用方法

同样 jdk 通过 MethodAccessor 来进行method的调用,Java 虚拟机提供了两种模式来支持 method 的调用 一个是 NativeMethodAccessorImpl 一个是通过 ASM 字节码直接动态生成一个类在 invoke 方法内部调用目标方法,由于是动态生成所以 jdk 中没有其源码,但 jdk 提供了 DelegatingMethodAccessorImpl 委派模式以方便在运行过程中可以动态切换字节码模式和 native 模式,我们可以看下生成 MethodAccessor 的代码。

 1class NativeMethodAccessorImpl extends MethodAccessorImpl {
2 private final Method method;
3 private DelegatingMethodAccessorImpl parent;
4 private int numInvocations;
5
6 NativeMethodAccessorImpl(Method var1) {
7 this.method = var1;
8 }
9
10 public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
11 if(++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
12 MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
13 this.parent.setDelegate(var3);
14 }
15
16 return invoke0(this.method, var1, var2);
17 }
18
19 void setParent(DelegatingMethodAccessorImpl var1) {
20 this.parent = var1;
21 }
22
23 private static native Object invoke0(Method var0, Object var1, Object[] var2);
24}

可以看到 JDK 内部通过 numInvocations 判断如果该反射调用次数超过 ReflectionFactory.inflationThreshold() 则用字节码实现。如果小于该值则采用 native 实现,native 的调用比字节码方式慢很多, 动态实现和本地实现相比执行效率要快 20 倍。因为动态实现无需经过 Java,C++ 再到 Java 的转换,之前在 jdk6 以前有个工具 ReflectAsm 就是采用这种方式提升执行效率,不过在 jdk8 以后,也提供了字节码方式,由于许多反射只需要执行一次,然而动态方式生成字节码十分耗时,所以 jdk 提供了一个阈值默认15,当某个反射的调用次数小于15的话就走本地实现,大于15则走动态模式,而这个阈值可以在 jdk 启动参数里面做配置。

反射为什么慢

经过以上优化,其实反射的效率并不慢,在某些情况下可能达到和直接调用基本相同的效率,但是在首次执行或者没有缓存的情况下还是会有性能上的开销,主要在以下方面:

1.Class.forName(); 会调用本地方法,我们用到的 method 和 field 都会在此时加载进来,虽然会进行缓存,但是本地方法免不了有 Java 到 C++ 在到Java 得转换开销。

2.class.getMethod(),会遍历该 class 所有的公用方法,如果没匹配到还会遍历父类的所有方法,并且 getMethods() 方法会返回结果的一份拷贝,所以该操作不仅消耗 CPU 还消耗堆内存,在热点代码中应该尽量避免,或者进行缓存。

3.invoke 参数是一个 object 数组,而 object 数组不支持 Java 基础类型,而自动装箱也是很耗时的。

反射的运用

  • spring doc

spring 加载 bean 的流程基本都用到了反射机制

1. 获取类的实例 通过构造方法getInstance(静态变量初始化,属性赋值,构造方法);

2.如果实现了BeanNameAware接口,则用反射注入bean赋值给属性;

3.如果实现了BeanFactoryAware接口,则设置 beanFactory;

4.如果实现了ApplicationContextAware,则设置 ApplicationContext;

5.调用BeanPostProcesser的预先初始化方法;

6.如果实现了InitializingBean,调用AfterPropertySet方法;

7.调用定制的 init-method()方法  对应的直接 @PostConstruct;

8.调用BeanPostProcesser的后置初始化完毕的方法。

  • 序列化

fastjson 可以参考 ObjectDeserializer 的几个实现 JavaBeanDeserializer 和ASMJavaBeanDeserializer。

动态代理

jdk 提供了一个工具类来动态生成一个代理,允许在执行某一个方法时进行额外的处理。

 1Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)
2
3class HWInvocationHandler implements InvocationHandler{
4 //目标对象
5 private Object target;
6 public HWInvocationHandler(Object target){
7 this.target = target;
8 }
9 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
10 System.out.println("------插入前置通知代码-------------");
11 //执行相应的目标方法
12 Object rs = method.invoke(target,args);
13 System.out.println("------插入后置处理代码-------------");
14 return rs;
15 }
16}

我们分析下这个方法的实现,首先生成的代理对象,需要实现参数里面声明的所有接口。接口的实现应给委托给 InvocationHandler 进行处理,invocationHandler 里面可以根据 method 声明判断是否需要做增强,所以所生成的代理类里面必须能够获取到 InvocationHandler,在我们无法知道代理类的具体类型的时候,我们可以通过反射从构造方法里将 InvocationHandler 传给代理类的实例。

所以 总的来说生成代理对象需要两步

1. 获取代理类的 class 对象

2. 通过 class 对象获取构造方法,通过反射生成代理类的实例,并将 InvocationHandler 传人。

 1@CallerSensitive
2public static Object newProxyInstance(ClassLoader loader,
3 Class[] interfaces,
4 InvocationHandler h)

5 throws IllegalArgumentException
6
{
7 Objects.requireNonNull(h);
8
9 final Class[] intfs = interfaces.clone();
10
11 /*
12 * Look up or generate the designated proxy class.
13 *
生成代理类
14 */

15 Class cl = getProxyClass0(loader, intfs);
16
17 /*
18 * Invoke its constructor with the designated invocation handler.
19 */

20 try {
21
22 //获取代理类的构造方法
23 final Constructor cons = cl.getConstructor(constructorParams);
24 final InvocationHandler ih = h;
25 if (!Modifier.isPublic(cl.getModifiers())) {
26 AccessController.doPrivileged(new PrivilegedAction() {
27 public Void run() {
28 cons.setAccessible(true);
29 return null;
30 }
31 });
32 }
33 //获取代理类的实例,并且将invocationhandler传人
34 return cons.newInstance(new Object[]{h});
35 } catch (IllegalAccessException|InstantiationException e) {
36 ...
37 }
38}

下面我们在看下 getProxyClass0 如何获取代理类的 class 对象,这里 jdk 通过 WeakCache 来缓存已经生成的 class 对象,因为生成该 class 通过字节码生成还是很耗时,同时为了解决之前由于动态代理生成太多 class 对象导致内存不足,所以这里通过弱引用 WeakReference 来缓存所生成的代理对象 class,当发生 GC 的时候如果该 class 对象没有其他的强引用将会被直接回收。 生成代理类的 class 在 ProxyGenerator 的 generateProxyClass 方法内实现,该方法返回一个 byte[] 数组,最后通过一个本地方法加载到虚拟机,所以可以看出生成该对象还是非常耗时的。

 1//生成字节码数组
2byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
3 proxyName, interfaces, accessFlags);
4try {
5//加载进虚拟机
6 return defineClass0(loader, proxyName,
7 proxyClassFile, 0, proxyClassFile.length);
8} catch (ClassFormatError e) {
9 /*
10 * A ClassFormatError here means that (barring bugs in the
11 * proxy class generation code) there was some other
12 * invalid aspect of the arguments supplied to the proxy
13 * class creation (such as virtual machine limitations
14 * exceeded).
15 */

16 throw new IllegalArgumentException(e.toString());
17}
18
19private byte[] generateClassFile() {
20 this.addProxyMethod(hashCodeMethod, Object.class);
21 this.addProxyMethod(equalsMethod, Object.class);
22 this.addProxyMethod(toStringMethod, Object.class);
23 Class[] var1 = this.interfaces;
24 int var2 = var1.length;
25
26 int var3;
27 Class var4;
28 for(var3 = 0; var3 < var2; ++var3) {
29 var4 = var1[var3];
30 Method[] var5 = var4.getMethods();
31 int var6 = var5.length;
32
33 for(int var7 = 0; var7 < var6; ++var7) {
34 Method var8 = var5[var7];
35 this.addProxyMethod(var8, var4);
36 }
37 }
38
39 this.methods.add(this.generateConstructor());
40...
41 }
42 //生成一个带invocationhandler参数的构造方法
43private ProxyGenerator.MethodInfo generateConstructor() throws IOException {
44 ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
45 DataOutputStream var2 = new DataOutputStream(var1.code);
46 this.code_aload(0, var2);
47 this.code_aload(1, var2);
48 var2.writeByte(183);
49 var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "", "(Ljava/lang/reflect/InvocationHandler;)V"));
50 var2.writeByte(177);
51 var1.maxStack = 10;
52 var1.maxLocals = 2;
53 var1.declaredExceptions = new short[0];
54 return var1;
55}

上面的流程可以简单归纳为

1. 增加 hashcode,equals,toString 方法;

2. 增加所有接口中声明的未实现方法;

3. 增加一个方法参数为 java/lang/reflect/InvocationHandler 的构造方法;

4. 其他静态初始化数据。

动态代理的应用

1. spring-aop

spring aop 默认基于 jdk 动态代理来实现,我们来看下下面这个经典的面试问题

一个类里面,两个方法A和方法B,方法B上有加注解做事物增强,那么A调用 this.B 为什么没有事物效果?

因为 spring-aop 默认基于 jdk 的动态代理实现,最终执行是通过生成的代理对象的,而代理对象执行A方法和B方法其实是调用的 InvocationHandler 里面的增强后的方法,其中B方法是经过 InvocationHandler 做增强在方法前后增加了事物开启和提交的代码,而真正执行代码是通过 methodB.invoke(原始对象)  而A方法的实现内部虽然包含了this.B 方法。 但其实是调用了 methodA.invoke(原始对象),而这一句代码相当于调用的是原始对象的 methodA 方法,而这里面的 this.B() 方法其实是调用的原始对象的B方法,没有进行过事物增强,而如果是通过 cglib 做字节码增强,生成这个类的子类,这种调用 this.B 方法是有事物效果的。

图 2

2. rpc consumer

有过 RMI 开发经验的人可能会很熟悉,为什么在对外 export rmi 服务的时候会分别在client 和 server 生成两个 stub 文件,其中 client 的文件其实就是用动态代理生成了一个代理类。 这个代理类实现了所要对外提供服务的所有接口,每个方法的实现其实就是将接口信息,方法声明、参数、返回值信息通过网络发给服务端,而服务端收到请求后通过找到对应的实现然后用反射 method.invoke 进行调用,然后将结果返回给客户端。

其实其他的 RPC 框架的实现方式大致和这个类似,只是客户端的代理类可能不仅要将方法声明通过网络传输给服务提供方,也可以做一下服务路由、负载均衡以及传输一些额外的 attachment 数据给 provider。

图 3

下次我们聊一聊互金公司怎么扣份额。

作者简介

小强,铜板街资金端后台开发工程师,2015年6月加入铜板街。目前负责铜板街资金端清结算相关的开发。

———- END ———-

长按关注我们