设计模式【3.2】-- JDK动态代理源码分析有多香?
前面文章有说到代理模式:http://aphysia.cn/archives/dynamicagentdesignpattern
那么回顾一下,代理模式怎么来的?假设有个需求:
在系统中所有的
controller类调用方法之前以及之后,打印一下日志。
假设原来的代码:
public class Person{
public void method(){
// 表示自己的业务逻辑
process();
}
}
如果在所有的类里面都添加打印方法,这样肯定是不现实的,如果我有几百个这样的类,写到崩溃,况且重复代码太多,冗余,还耦合到一块了,要是我下次不打日志了,做其他的,那几百个类又全部改一遍。
public class Person{
public void method(){
log();
// 表示自己的业务逻辑
process();
log();
}
}
静态代理
怎么样写比较优美呢?静态代理 这时候出场了,先把方法抽象成为接口:
public class IProxy(){
public void method();
}
让具体的类去实现 IProxy,写自己的业务逻辑,比如:
public class Person implements IProxy(){
public void method(){
// 表示自己的业务逻辑
process();
}
}
然后弄个代理类,对方法进行增强:
public class PersonProxy implements IProxy{
private IProxy target;
public PersonProxy(IProxy target){
this.target = target;
}
@Override
public void method() {
log();
target.method();
log();
}
}
调用的时候,把真实的对象放到代理类的构造器里面,就可以得到一个代理类,对它的方法进行增强,好处就是,如果下次我要改,不打日志,做其他事情,我改代理类就可以了,不用到处改我的目标类的方法,而坏处还是很明显,要增强哪一个类,就要为它写一个代理类,这样好像不是很合理。
动态代理
怎么样能让他自动生成代理对象呢? 动态代理做的就是这个事情,它可以 动态 的根据我们提供的类,生成代理类的对象。
最主要的,是在运行时,动态生成,只要传入我们要代理增强的类相关的信息,比如类对象本身,类加载器,类的接口等,就可以生成,不用提前知道它是 A 类,B 类还是 C 类。
动态代理主要有三种实现方法,今天我们重点分析 JDK 动态代理:
- JDK 代理:使用 JDK 提供的官方的 Proxy
- 第三方 CGlib 代理:使用 CGLib 的 Enhancer 类创建代理对象
- javassit:Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。
JDK 动态代理
使用步骤
- 新建一个接口
- 新建一个类,实现该接口
- 创建代理类,实现
java.lang.reflect.InvocationHandler接口
代码如下:
IPlayDao.java(玩的接口)
public interface IPlayDao {
void play();
}
StudentDao.java(实现了买东西,玩的接口的学生类)
public class StudentDao implements IPlayDao {
@Override
public void play() {
System.out.println("我是学生,我想出去玩");
}
}
MyProxy.java 代理类:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MyProxy {
private Object target;
public MyProxy(Object target){
this.target=target;
}
public Object getProxyInstance(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
// 一个接口可能很多方法,要是需要针对某一个方法,那么需要在函数里判断 method
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始事务 2");
// 执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务 2");
return returnValue;
}
}
);
}
}
测试类(Test.java)
public class Test {
public static void main(String [] args){
// 保存生成代理类的字节码文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
IPlayDao target =new StudentDao();
System.out.println(target.getClass());
IPlayDao proxy = (IPlayDao) new MyProxy(target).getProxyInstance();
System.out.println(proxy.getClass());
// 执行方法 【代理对象】
proxy.play();
}
}
由于加了这句代码,我们可以把生成的代理类的字节码文件保存下来, 其实通过输出也可以看到,两个对象不是同一个类,代理类是动态生成的:
```java
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

源码分析
跟着源码一步步看,先从调用的地方 Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h):

进入方法里面,省略各种异常处理,主要剩下了生成代理类字节码 以及 通过构造函数反射构造新对象:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
// 判空
Objects.requireNonNull(h);
// 安全检查
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* 查找或者生成代理对象
*/
Class<?> cl = getProxyClass0(loader, intfs);
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
// 获取构造器
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 反射构造代理对象
return cons.newInstance(new Object[]{h});
}
上面注释里面说查找或者生成代理对象,为什么有查找?因为并不是每一次都生成,生成的代理对象实际上会缓存起来,如果没有,才会生成,看源码 Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces):
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length> 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// 调用缓存代理类的 cache 来获取类加载器
return proxyClassCache.get(loader, interfaces);
}
如果由实现给定接口的给定加载器定义的代理类存在,这将简单地返回缓存的副本; 否则,它将通过 ProxyClassFactory 创建代理类,proxyClassCache 其实就是个 weakCache:
private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
初始化的时候,proxyClassCache 指定了两个属性,一个是 KeyFactory, 另外一个是 ProxyClassFactory, 从名字就是猜到 ProxyClassFactory 是代理类工厂:
public WeakCache(BiFunction<K, P, ?> subKeyFactory,
BiFunction<K, P, V> valueFactory) {
this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
this.valueFactory = Objects.requireNonNull(valueFactory);
}
记住这里的 subKeyFactory,实际上就是传入的 ProxyClassFactory,那前面 proxyClassCache.get(loader, interfaces); 到底是怎么操作的?

上面调用到了 subKeyFactory.apply(key, parameter),这个 subKeyFactory 实际上是我们传的 ProxyClassFactory, 进入里面去看:
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// 生成的代理类前缀
private static final String proxyClassNamePrefix = "$Proxy";
// 下一个生成的代理类的名字的计数器,一般是 $Proxy0,$Proxy1
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* 检验类加载器是否能通过接口名称加载该类
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + "is not visible from class loader");
}
/*
* 判断接口类型
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + "is not an interface");
}
/*
* 判断是否重复
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface:" + interfaceClass.getName());
}
}
// 代理包名字
String proxyPkg = null;
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
/*
* 记录非公共代理接口的包,以便在同一个包中定义代理类。确认所有非公共代理接口都在同一个包中。
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
// 如果没有非公共代理接口,请使用 com.sun.proxy 包
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* 为要生成的代理类选择一个名称。
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/*
* 生成指定的代理类。(这里是重点!!!)
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* 这里的 ClassFormatError 意味着 (禁止代理类生成代码中的错误) 提供给代理类创建的参数有其他一些无效方面 (比如超出了虚拟机限制)。
*/
throw new IllegalArgumentException(e.toString());
}
}
}
上面调用一个方法生成代理类,我们看看 IDEA 反编译的代码:
public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
// 生成文件
final byte[] var4 = var3.generateClassFile();
// 判断是否要写入磁盘!!!
if (saveGeneratedFiles) {
// 开启高权限写入
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
int var1 = var0.lastIndexOf(46);
Path var2;
if (var1> 0) {
Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
Files.createDirectories(var3);
var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
} else {
var2 = Paths.get(var0 + ".class");
}
Files.write(var2, var4, new OpenOption[0]);
return null;
} catch (IOException var4x) {
throw new InternalError("I/O exception saving generated file:" + var4x);
}
}
});
}
return var4;
}
生成代理文件实际上和我们想的差不多,就是一些 hashCode(),toString(),equals(), 原方法,代理方法等:

这与我们之前看到的文件一致:

然后之所以我们代码要设置写入磁盘,是因为这个变量, 控制了写磁盘操作:
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
为什么只支持接口实现,不支持继承普通类?
因为代理类继承了 Proxy 类,并且实现了接口,Java 不允许多继承,所以不能代理普通类的方式,并且在静态代码块里面,用反射方式获取了所有的代理方法。
JDK 代理看起来像是个黑盒,实际上,每一句代码,都有其缘由。其实本质上也是动态的为我们的原始类,动态生成代理类。
生成的代理类里面其实对原始类进行增强(比如 play() 方法)的时候, 调用了 super.h.invok() 方法,其实这里的 h 是什么呢?
h 是父类的 h, 生成的代理类的父类是 Proxy,Proxy 的 h,就是我们传入的 InvocationHandler:
public final void play() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
生成的代码里面通过反射调用到的其实是我们自己重写的那部分逻辑,所以就有了增强的功能,不得不说,这种设计确实巧妙:

动态代理有多香
动态代理是Java语言里面的一个很强大的特性,可以用来做一些切面,比如拦截器,登录验证等等,但是它并不是独立存在的,任何一个知识点都不能独立说明语言的强大,重要的是它们的组合。
动态代理要实现强大的功能,一般需要和反射,注解等一起合作,比如对某些请求进行拦截,拦截后做一些登录验证,或者日志功能。最重要的一点,它能够在减少耦合度的前提下实现增强。
【作者简介】:
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。
设计模式【3.2】-- JDK动态代理源码分析有多香?的更多相关文章
- 设计模式之JDK动态代理源码分析
这里查看JDK1.8.0_65的源码,通过debug学习JDK动态代理的实现原理 大概流程 1.为接口创建代理类的字节码文件 2.使用ClassLoader将字节码文件加载到JVM 3.创建代理类实例 ...
- jdk 动态代理源码分析
闲来无事,撸撸源码 使用方法 直接看代码吧.. package com.test.demo.proxy; import java.lang.reflect.InvocationHandler; imp ...
- JDK动态代理源码分析
先抛出一个问题,JDK的动态代理为什么不支持对实现类的代理,只支持接口的代理??? 首先来看一下如何使用JDK动态代理.JDK提供了Java.lang.reflect.Proxy类来实现动态代理的,可 ...
- 动态代理学习(二)JDK动态代理源码分析
上篇文章我们学习了如何自己实现一个动态代理,这篇文章我们从源码角度来分析下JDK的动态代理 先看一个Demo: public class MyInvocationHandler implements ...
- java 1.8 动态代理源码分析
JDK8动态代理源码分析 动态代理的基本使用就不详细介绍了: 例子: class proxyed implements pro{ @Override public void text() { Syst ...
- JDK动态代理源码学习
继上一篇博客设计模式之代理模式学习之后http://blog.csdn.net/u014427391/article/details/75115928,本博客介绍JDK动态代理的实现原理,学习一下JD ...
- JDK动态代理源码解析
动态代理.静态代理优缺点 关于JDK的动态代理,最为人熟知的可能要数Spring AOP的实现,默认情况下,Spring AOP的实现对于接口来说就是使用的JDK的动态代理来实现的,而对于类的 ...
- 深入剖析JDK动态代理源码实现
动态代理.静态代理优缺点优点:业务类只需要关注业务逻辑本身,保证了业务类的重用性.这是代理的共有优点.动态代理只有在用到被代理对象的时候才会对被代理类进行类加载. 而静态代理在编译器就已经开始占内存了 ...
- JDK7动态代理源码分析
IObject proxy = (IObject) Proxy.newProxyInstance(IObject.class.getClassLoader(), new Class[]{IObject ...
- JDK动态代理源码剖析
关键代码: 1.Proxy.newInstance: private static final Class<?>[] constructorParams = { InvocationHan ...
随机推荐
- 使用 `Roslyn` 分析器和修复器对.cs源代码添加头部注释
之前写过两篇关于Roslyn源生成器生成源代码的用例,今天使用Roslyn的代码修复器CodeFixProvider实现一个cs文件头部注释的功能, 代码修复器会同时涉及到CodeFixProvide ...
- 2023/11/16 NOIP 模拟赛
T1 基于1的算术 标签 暴力枚举 思路1 赛时想了个假的 DP,只拿了 77 分,,, 小于 \(10^{15}\) 的仅由 \(1\) 组成的数只有 \(15\) 个,直接枚举即可. 想了一个做法 ...
- Angular 学习笔记 language service
尝试 v10 rc 的时候, 突然 language service 不 work 了. ctrl + shift + p -> Show logs... 这样可以检查和 report issu ...
- windows在cygwin64下使用acme.sh批量签发Let's Encrypt的ssl证书,并用powershell重新分配iis证书
使用前提 本脚本是在使用阿里云Windows服务器的前提,如果使用其他dns服务,请参看acme.sh的dns相关文档 配置好cygwin64.acme.sh并配置好阿里云账户,openssl最好也安 ...
- MySQL linux下安装,配置,免密登录与基本认识
目录 MySQL卸载 环境 查看是否已安装MySQL 卸载mysql服务 查看是否卸载干净 MySQL安装 查看linux版本 选择MySQL版本 获取mysql官方yum源 rpm安装mysql官方 ...
- 没时间做获取天气的实验 三分钟! 给您看清如何通过HTTP GET请求 获取天气预报
1. 注册心知天气账号,获取私钥 下面这篇文章是图示讲解的,好东西,何不转载为快? http://www.taichi-maker.com/homepage/iot-development/iot-p ...
- 无人值守的IDC机房动环综合运维方案
企业数字化转型以及5G.物联网.云计算.人工智能等新业态带动了数据中心的发展,在国家一体化大数据中心及"东数西算"节点布局的推动下,数据中心机房已成为各大企事业单位维持业务正常 ...
- 内核模块踩内存问题定位利器- hardware breakpoint
内核由于共享内存地址空间,如果没有合适的工具,很多踩内存的问题即使复现,也无法快速定位: 在新的内核版本中引入了一个新工具hardware breakpoint,其能够监视对指定的地址的特定类型(读/ ...
- 深入理解虚拟 物理地址转换,页表--基于ARMV8
1. 页表转换寄存器描述符 1.1,页表/页目录结构 基于前言中的内核配置,内核采用39位虚拟地址,因此可寻址范围为2^39 = 512G,采用(linux 默认为五级页表,另外还有PUD,P4D,由 ...
- iOS长按手势列表拖拽功能实现
项目开发中遇到拖拽功能的需求,具体要求是在编辑状态下,首页底部菜单项可以拖动位置,便于位置切换.遇到问题后的初步想法是添加拖拽手势,拖拽到某个位置,判断拖拽cell的中心点是否在另一个cell内,这样 ...