关于这一部分的知识点 我们先从静态代理说起
静态代理
什么是代理? 代理就是:不操作目标对象而是通过一个中间人(代理)来间接操作
最常见的现实例子就是租房问题:你要租房子 不需要直接找到房东 而是通过租房中介 中介帮你去和房东沟通 那么这里面就有几个角色
- 房源:一般是一个我们定义的接口
- 房东:拥有房源 所以是一个实现接口的类
- 中介:也就是代理类 将房东作为自身的属性 方便后续调用租房的动作 而且其中可以添加一些中介能做而房东做不了的动作 在代码之中的体现就是代理类的独有功能
- 租客:作为一个启动类 主要动作就是找中介然后租房
比如说以下的例子
定义一个核心业务接口 在这里也就是出租房屋这件事情
1 | //出租房子 |
我们从现实角度出发思考 房东肯定拥有这个核心业务的能力 那么在代码之中的体现就是其实现了这个Rent接口
1
2
3
4
5
6
7//房东
public class Landlord implements Rent {
@Override
public void rentHouse() {
System.out.println("把房子租给你了");
}
}
中介是代理房东出租房子的 那么假设交易完成 其最终是完成出租房子这个动作的人 那么理应其也实现Rent接口 同时它还和房东有所联系 这一点也要在代码之中有所体现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//中介
public class Agent implements Rent {
private Landlord landlord; // 中介手里有房东的联系方式
public Agent(Landlord landlord) {
this.landlord = landlord;
}//有参构造器
@Override
public void rentHouse() {
// 前置增强:中介在出租前做的额外工作
System.out.println("带你看房介绍房子情况");
System.out.println("帮你谈价格");
System.out.println("准备租赁合同");
// 核心:真正调用房东的出租方法
landlord.rentHouse();
// 后置增强:出租完成后的事情
System.out.println("收取中介费");
System.out.println("把钥匙交给你 合同归档");
}
}
我们可以看到在我们定义的中介类之中 其将房东作为自己的私有属性 并且除了核心业务之外 它还能实现很多额外操作
那么最后需要定义的是我们的租客类
1 | //租客 |
以上部分就是静态代理的整个逻辑了
那么其实总结以下可以看出相关的特性
- 代理类和目标类实现同一接口
- 代理类持有目标对象的引用
- 代理类在调用目标方法前后可以添加额外动作
- 客户端调用代理类而不是直接调用目标对象
- 代理类是在编译期写死的(这也就是为什么说是’静态‘)
其中最核心的一点就在于可以在核心业务的前后添加额外动作 最常见的场景就是日志编写
静态代理的不足
刚刚核心业务只有一个就是出粗房屋的这一动作 假设现在核心业务有多个
比如下列的情况
1 | public class Landlord implements Rent { |
那么如果我们还是使用静态代理 就会非常繁琐
1 | public class Agent implements Rent { |
不仅仅是代码很长 而且维护困难:如果接口新增一个方法 那么代理类必须跟着一起修改 其无法适应接口变化 也无法在运行时灵活生成
那么接下来就轮到今天的主角 动态代理出场了
动态代理
假设刚刚租房的场景之中 一条街有一百个房东 每个房东都要实现租房 维修 收租这些业务 那么按照静态代理的方式 我们就要写一百个中介来实现 但是这很显然是不合适的
动态代理解决这个问题的方式就是:不再手动编写中介这个给类 而是让java在程序运行的时候 根据需求现场打印一个万能的中介机器人
如果说我们想像静态代理一样将对象之间进行绑定 那么在面对多对象的工作的时候 就会导致代码良非常复杂且不够灵活
那么这时候我们就可以使用动态的方式生成对象 之后再进行代理
这其中该涉及两个重要的方法
那么这其中就要涉及三个重要的组成元素
- 调控这个代理系统的接口
- 接收代理对象的方法
- 生成代理对象的方法
我们注意来分析一下如何实现这三个重要组成部分
InvocationHandler——调控这个代理系统的接口
java.lang.reflect.InvocationHandle
在Java中,InvocationHandler 是一个接口,它是动态代理机制中的核心部分。动态代理允许开发者在运行时创建一个代理类的实例,这个代理类可以用来代替一个真实的对象,从而实现对真实对象的方法调用的控制
这个接口的作用就是处理代理实例上面的方法调用并返回结果 当一个方法再代理实例上面被调用的时候 方法调用会被编码分批额到这个接口的ivoke方法下
本质上其就是附加在代理实例上的一个对象 代理实例上的每一次方法调用 都会被代理实例直接转发给这个 handler 的 invoke 方法
invoke——接收代理对象的方法
1 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; |
这是InvocationHandle接口的唯一方法
其中的三个参数分别代表
Object proxy—— 代理实例本身(即动态生成的那个对象)注意一点:在invoke方法体之中 尽量避免直接调用proxy的任何方法 否则会由于再次进入invoke的原因导致触发递归调用 最终栈溢出Method method—— 被调用的接口方法(java.lang.reflect.Method对象)代表本次调用的哪个接口方法Object[] args—— 调用该方法时传入的实际参数数组
在invoke方法之中 我们需要通过method.invoke(目标对象,args)来真正调用真实业务对象的方法 method知识方法的描述 必须绑定一个具体的实例(也就是被代理的目标对象)才能执行
这个方法调用是在后续生成的代理类再每次代理对象的方法被调用之时自动调用的
注意 invoke必须返回一个object 这个返回值会被代理类原封不动的返回给调用者
newProxyInstance——生成代理对象的方法
Proxy.newProxyInstance这个方法负责再运行的时候动态生成代理类并且创建实例
1 | public static Object newProxyInstance(ClassLoader loader, |
其中有三个参数 我们来逐一解析
loader——类加载器
用于加载动态生成的类 几乎总是写成接口类.class.getClassLoader()
类加载器用于将JVM动态生成的代理类加载进内存 只要使用和目标接口相同的类即可interface——要实现的接口数组
传入Class<?>[](接口的 Class 对象数组) 传入生成的代理对象需要具备的方法
可以传入多个接口new Class[] { Hello.class, Runnable.class }那么此时生成的代理实例就会同时是Hello和Runnable类型 值得注意的是 此处传入的必须是接口对象 不能是普通类h——调用处理器
这一步就是将这我们前文提到的InvocationHandle绑定到即将生成的代理实例之上 之后代理实例之上的每一个方法调用 就会被转发到h.invoke方法几种给处理
实例分析
在这一场景的动态代理之中 接口和目标类我们直接复用静态代理里面的素材即可 无需改动 在这里我们稍微多添加一点业务
1 | //接口 |
定义房东
1 | //目标对象 房东 |
接下来我们要利用InvocationHandler实现万能中介的功能
1 | import java.lang.reflect.InvocationHandler; |
我们可以清晰的看到 这里面的房东引用并没有写死是谁 而是用了一个object 当使用构造方法的时候 再指定具体是哪一个房东来进行的这次租房委托
不同的房东要实现的业务不同 所以我们也不能直接写死是调用哪个方法 此时使用invoke就可以达成目的
1 | import java.lang.reflect.Proxy; |
我们来看生成万能中介的这一段核心代码
1 | Rent proxyAgent = (Rent) Proxy.newProxyInstance( |
- 类加载器部分:告诉JVM使用和方通同一个的类加载器来加载接下来再内存之中创建的代理类
- 接口数组部分:告诉JVM这个代理对象需要实现的方法 直接从房东langdlord上getInterfaaces下来即可
- 绑定规则就是将我们前面定义的中介的规则赋予它
我们来看后面调用方法这一块的内容 比如
1 | proxyAgent.rentHouse(); |
这一步时 JVM检测proxyAgent是一个动态代理 于是将这个请求打包成(proxy实例,rentHouse方法对象,参数null)丢进第三步绑定handler.invoke(…)方法之中
handler接收到请求之后 根据定义的手册反射调用房东的真是业务
这样一来 就完成了这个动态代理的过程
小结
我们从租房出发 完整走完了java代理模式的两种实现方式
- 静态代理:简单直观 但是再面对多业务场景时代码复杂 维护成本过高
- 动态代理:再运行时动态生成代理对象 增强逻辑只写一次 可以服用 但是由于反射调用 性能会略顶于静态代理
我们通过以上内容的学习可以看到整个动态代理的流程大概是这样
1 | 租客调用 proxyAgent.rentHouse() |
我们需要知道 动态代理的本质是再运行的时候动态生成字节码并加载 让代理类不再存在于源代码之中 而是由JVM再内存之中现场创建
参考文章