4万字《腾讯云技术实践精选集 2021》发布,点击下载 了解详情
写点什么

Java 深度历险(七)——Java 反射与动态代理

  • 2011 年 4 月 08 日
  • 本文字数:4121 字

    阅读完需:约 14 分钟

上一篇文章中介绍Java 注解的时候,多次提到了Java 的反射API。与 javax.lang.model 不同的是,通过反射 API 可以获取程序在运行时刻的内部结构。反射 API 中提供的动态代理也是非常强大的功能,可以原生实现 AOP 中 的方法拦截功能。正如英文单词 reflection 的含义一样,使用反射 API 的时候就好像在看一个 Java 类在水中的倒影一样。知道了 Java 类的内部 结构之后,就可以与它进行交互,包括创建新的对象和调用对象中的方法等。这种交互方式与直接在源代码中使用的效果是相同的,但是又额外提供了运行时刻的灵活性。使用反射的一个最大的弊端是性能比较差。相同的操作,用反射API 所需的时间大概比直接的使用要慢一两个数量级。不过现在的JVM 实现中,反射操作的性能已经有了很大的提升。在灵活性与性能之间,总是需要进行权衡的。应用可以在适当的时机来使用反射API。

基本用法

Java 反射 API 的第一个主要作用是获取程序在运行时刻的内部结构。这对于程序的检查工具和调试器来说,是非常实用的功能。只需要短短的十几行代码,就可以遍历出来一个 Java 类的内部结构,包括其中的构造方法、声明的域和定义的方法等。这不得不说是一个很强大的能力。只要有了 java.lang.Class 类 的对象,就可以通过其中的方法来获取到该类中的构造方法、域和方法。对应的方法分别是 getConstructor getField getMethod 。这三个方法还有相应的 getDeclaredXXX 版本,区别在于 getDeclaredXXX 版本的方法只会获取该类自身所声明的元素,而不会考虑继承下来的。 Constructor Field Method 这三个类分别表示类中的构造方法、域和方法。这些类中的方法可以获取到所对应结构的元数据。

反射 API 的另外一个作用是在运行时刻对一个 Java 对象进行操作。 这些操作包括动态创建一个 Java 类的对象,获取某个域的值以及调用某个方法。在 Java 源代码中编写的对类和对象的操作,都可以在运行时刻通过反射 API 来实现。考虑下面一个简单的 Java 类。

复制代码
class MyClass {
public int count;
public MyClass(int start) {
count = start;
}
public void increase(int step) {
count = count + step;
}
}

使用一般做法和反射 API 都非常简单。

复制代码
MyClass myClass = new MyClass(0); // 一般做法
myClass.increase(2);
System.out.println("Normal -> " + myClass.count);
try {
Constructor<myclass> constructor = MyClass.class.getConstructor(int.class); // 获取构造方法 <br></br> MyClass myClassReflect = constructor.newInstance(10); // 创建对象 <br></br> Method method = MyClass.class.getMethod("increase", int.class); // 获取方法 <br></br> method.invoke(myClassReflect, 5); // 调用方法 <br></br> Field field = MyClass.class.getField("count"); // 获取域 <br></br> System.out.println("Reflect -> " + field.getInt(myClassReflect)); // 获取域的值 <br></br>} catch (Exception e) { <br></br> e.printStackTrace();<br></br>} <br></br></myclass>

由于数组的特殊性, Array 类提供了一系列的静态方法用来创建数组和对数组中的元素进行访问和操作。

复制代码
Object array = Array.newInstance(String.class, 10); // 等价于 new String[10]
Array.set(array, 0, "Hello"); // 等价于 array[0] = "Hello"
Array.set(array, 1, "World"); // 等价于 array[1] = "World"
System.out.println(Array.get(array, 0)); // 等价于 array[0]

使用 Java 反射 API 的时候可以绕过 Java 默认的访问控制检查,比如可以直接获取到对象的私有域的值或是调用私有方法。只需要在获取到 Constructor、Field 和 Method 类的对象之后,调用 setAccessible 方法并设为 true 即可。有了这种机制,就可以很方便的在运行时刻获取到程序的内部状态。

处理泛型

Java 5 中引入了泛型的概念之后,Java 反射 API 也做了相应的修改,以提供对泛型的支持。由于类型擦除机制的存在,泛型类中的类型参数等信息,在运行时刻是不存在的。JVM 看到的都是原始类型。对此,Java 5 对 Java 类文件的格式做了修订,添加了 Signature 属性,用来包含不在 JVM 类型系统中的类型信息。比如以 java.util.List 接口为例,在其类文件中的 Signature 属性的声明是 <E:Ljava/lang/Object;>Ljava/lang/Object;Ljava/util/Collection<TE;>;; ,这就说明 List 接口有一个类型参数 E。在运行时刻,JVM 会读取 Signature 属性的内容并提供给反射 API 来使用。

比如在代码中声明了一个域是 List类型的,虽然在运行时刻其类型会变成原始类型 List,但是仍然可以通过反射来获取到所用的实际的类型参数。

复制代码
Field field = Pair.class.getDeclaredField("myList"); //myList 的类型是 List<string>
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) type;
Type[] actualTypes = paramType.getActualTypeArguments();
for (Type aType : actualTypes) {
if (aType instanceof Class) {
Class clz = (Class) aType;
System.out.println(clz.getName()); // 输出 java.lang.String
}
}
} </string>

动态代理

熟悉设计模式的人对于代理模式可 能都不陌生。 代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。代理对象则可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持。这种模式在 RMI EJB 中都得到了广泛的使用。传统的代理模式的实现,需要在源代码中添加一些附加的类。这些类一般是手写或是通过工具来自动生成。JDK 5 引入的动态代理机制,允许开发人员在运行时刻动态的创建出代理类及其对象。在运行时刻,可以动态创建出一个实现了多个接口的代理类。每个代理类的对象都会关联一个表示内部处理逻辑的 InvocationHandler 接 口的实现。当使用者调用了代理对象所代理的接口中的方法的时候,这个调用的信息会被传递给 InvocationHandler 的 invoke 方法。在 invoke 方法的参数中可以获取到代理对象、方法对应的 Method 对象和调用的实际参数。invoke 方法的返回值被返回给使用者。这种做法实际上相 当于对方法调用进行了拦截。熟悉 AOP 的人对这种使用模式应该不陌生。但是这种方式不需要依赖 AspectJ 等 AOP 框架。

下面的代码用来代理一个实现了 List 接口的对象。所实现的功能也非常简单,那就是禁止使用 List 接口中的 add 方法。如果在 getList 中传入一个实现 List 接口的对象,那么返回的实际就是一个代理对象,尝试在该对象上调用 add 方法就会抛出来异常。

复制代码
public List getList(final List list) {
return (List) Proxy.newProxyInstance(DummyProxy.class.getClassLoader(), new Class[] { List.class },
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("add".equals(method.getName())) {
throw new UnsupportedOperationException();
}
else {
return method.invoke(list, args);
}
}
});
}

这里的实际流程是,当代理对象的 add 方法被调用的时候,InvocationHandler 中的 invoke 方法会被调用。参数 method 就包含了调用的基本信息。因为方法名称是 add,所以会抛出相关的异常。如果调用的是其它方法的话,则执行原来的逻辑。

使用案例

Java 反射 API 的存在,为 Java 语言添加了一定程度上的动态性,可以实现某些动态语言中的功能。比如在 JavaScript 的代码中,可以通过 obj“set” + propName 来根据变量propName 的值找到对应的方法进行调用。虽然在Java 源代码中不能这么写,但是通过反射API 同样可以实现类似 的功能。这对于处理某些遗留代码来说是有帮助的。比如所需要使用的类有多个版本,每个版本所提供的方法名称和参数不尽相同。而调用代码又必须与这些不同的版本都能协同工作,就可以通过反射API 来依次检查实际的类中是否包含某个方法来选择性的调用。

Java 反射 API 实际上定义了一种相对于编译时刻而言更加松散的契约。如果被调用的 Java 对象中并不包含某个方法,而在调用者代码中进行引用的话,在编译时刻就会出现错误。而反射 API 则可以把这样的检查推迟到运行时刻来完成。通过把 Java 中的字节代码增强、类加载器和反射 API 结合起来,可以处理一些对灵 活性要求很高的场景。

在 有些情况下,可能会需要从远端加载一个 Java 类来执行。比如一个客户端 Java 程序可以通过网络从服务器端下载 Java 类来执行,从而可以实现自动更新 的机制。当代码逻辑需要更新的时候,只需要部署一个新的 Java 类到服务器端即可。一般的做法是通过自定义类加载器下载了类字节代码之后,定义出 Class 类的对象,再通过 newInstance 方法就可以创建出实例了。不过这种做法要求客户端和服务器端都具有某个接口的定义,从服务器端下载的是 这个接口的实现。这样的话才能在客户端进行所需的类型转换,并通过接口来使用这个对象实例。如果希望客户端和服务器端采用更加松散的契约的话,使用反射 API 就可以了。两者之间的契约只需要在方法的名称和参数这个级别就足够了。服务器端 Java 类并不需要实现特定的接口,可以是一般的 Java 类。

动态代理的使用场景就更加广泛了。需要使用 AOP 中的方法拦截功能的地方都可以用到动态代理。Spring 框架的 AOP 实现默认也使用动态代理。不过 JDK 中的动态代理只支持对接口的代理,不能对一个普通的 Java 类提供代理。不过这种实现在大部分的时候已经够用了。

参考资料


感谢张凯峰对本文的策划与审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2011 年 4 月 08 日 00:0089943

评论

发布
暂无评论
发现更多内容

探索区块链Baas平台的奥秘,源中瑞公共服务平台开发技术

源中瑞-龙先生

区块链 源中瑞 Baas

Github霸榜数月!原来是阿里大牛最新的Java性能优化实战笔记

钟奕礼

Java 编程 程序员 架构 面试

TcaplusDB君 · 行业新闻汇编(4月17日)

TcaplusDB

数据库 nosql 数据 TcaplusDB

边缘计算是流行词还是风口?开发者怎样选开源项目?

华为云开发者社区

开源 开发者 5G 边缘计算 EdgeGallery 社区

第 0 期架构训练营模块 2 作业

架构实战营

还有人搞不懂数据仓库与数据库的区别?

大数据技术指南

数据仓库 4月日更

计算机原理学习笔记 Day8

穿过生命散发芬芳

计算机原理 4月日更

Impala架构详解

五分钟学大数据

4月日更 impala

程序员3年CRUD从8K涨到20K,这4个月我到底经历了什么?

码农之家

编程 程序员 互联网 面试 职场

这才是大数据的正确打开方式

华为云开发者社区

大数据 数据仓库 云原生 数据治理 灾备

阿里高工熬夜14天码出这份Java10w字的面试手册!却遭GitHub封杀

Java架构之路

Java 程序员 架构 面试 编程语言

阿里P8整理出SQL笔记:收获不止SOL优化抓住SQL的本质

Java架构之路

Java 程序员 架构 面试 编程语言

ThreadPoolExecutor源码解读(一)重新认识ThreadPoolExecutor(核心参数、生命周期、位运算、ThreadFactory、拒接策略)

徐同学呀

线程池 Java源码 JUC ThreadPoolExecutor

架构师实战营 模块二作业(微信朋友圈高性能复杂度架构分析)

代廉洁

架构实战营

openLooKeng如何应对“野蛮零散”的大数据

openLooKeng

大数据 开源 openLooKeng

CopyOnWriteArrayList源码解读之CopyOnWrite思想的利与弊

徐同学呀

Java源码 JUC CopyOnWriteArrayList

技术实践丨列存表并发更新时的锁等待问题原理

华为云开发者社区

事务 update 元组 列存表

Spring Bean创建过程的Hook

邱学喆

BeanPostProcessor @Autowired注入原理 @Resource注入原理 @Value注入原理

堪称神作!阿里数位专家联合写的“大厂高频Java面试手册”

码农之家

Java 编程 程序员 互联网 面试

架构师实战营 模块二总结

代廉洁

架构实战营

阿里P8重磅总结:看完别说不会了哦,SpringBoot「完结篇」

比伯

Java 编程 程序人生 计算机 架构】

MySQL 索引概要

学个球

MySQL 索引

Anolis OS 8.2 RC2 发行,支持飞腾、海光、兆芯、鲲鹏等芯片

阿里云基础软件团队

为极客时间增加自动提醒功能,督促用户回来上课

克比

聪明人的训练(十七)

Changing Lin

4月日更

架构实战营 模块二作业

fazinter

架构实战营

MySQL存储过程的异常处理

Sakura

4月日更

2021互联网大厂高频面试专题500道:并发编程/Spring/MyBatis(附答案解析)

比伯

Java 编程 架构 程序人生 计算机

阿里高工熬夜18天码出Java150K字面试宝典,却遭Github全面封杀

Java架构之路

Java 程序员 架构 面试 编程语言

关于ReentrantReadWriteLock,首个获取读锁的线程单独记录问题讨论(firstReader和firstReaderHoldCount)

徐同学呀

AQS Java源码 JUC

FutureTask源码解读,阻塞获取异步计算结果(阻塞、取消、装饰器、适配器、Callable)

徐同学呀

Java源码 JUC Future

Java深度历险(七)——Java反射与动态代理-InfoQ