Java 基础 (六) | 什么是动态代理?
微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。
最近在复习 Java 相关,回顾了下代理模式。代理模式在 Java 领域很多地方都有应用,它分为静态代理和动态代理,其中 Spring AOP 就是动态代理的典型例子。动态代理又分为接口代理和 cglib (子类代理),结合我的理解写了几个 demo 分享给你们,这是昨晚修仙到 3 点写出来的文章,不点在看,我觉得说不过去了。
代理模式在我们日常中很常见,生活处处有代理:
- 看张学友的演唱会很难抢票,可以找黄牛排队买
- 嫌出去吃饭麻烦,可以叫外卖
无论是黄牛、外卖骑手都得帮我们干活。但是他们不能一手包办(比如黄牛不能帮我吃饭),他们只能做我们不能或者不想做的事。
- 找黄牛可以帮我排队买上张学友的演唱会门票
- 外卖骑手可以帮我把饭送到楼下
所以,你看。代理模式其实就是当前对象不愿意做的事情,委托给别的对象做。
静态代理
我还是以找黄牛帮我排队买张学友的演唱会门票的例子,写个 demo 说明。现在有一个 Human 接口,无论是我还是黄牛都实现了这个接口。
public interface Human {
void eat();
void sleep();
void lookConcert();
}
例如,我这个类,我会吃饭和睡觉,如以下类:
public class Me implements Human{
@Override
public void eat() {
System.out.println("eat emat ....");
}
@Override
public void sleep() {
System.out.println("Go to bed at one o'clock in the morning");
}
@Override
public void lookConcert() {
System.out.println("Listen to Jacky Cheung's Concert");
}
}
有黄牛类,例如:
public class Me implements Human{
@Override
public void eat() {
}
@Override
public void sleep() {
}
@Override
public void lookConcert() {
}
}
现在我和黄牛都已经准备好了,怎么把这二者关联起来呢?我们要明确的是黄牛是要帮我买票的,买票必然就需要帮我排队,于是有以下黄牛类:注意这里我们不关心,黄牛的其他行为,我们只关心他能不能排队买票。
public class HuangNiu implements Human{
private Me me;
public HuangNiu() {
me = new Me();
}
@Override
public void eat() {
}
@Override
public void sleep() {
}
@Override
public void lookConcert() {
// 添加排队买票方法
this.lineUp();
me.lookConcert();
}
public void lineUp() {
System.out.println("line up");
}
}
最终的 main 方法调用如下:
public class Client {
public static void main(String[] args) {
Human human = new HuangNiu();
human.lookConcert();
}
}
结果如下:
由此可见,黄牛就只是做了我们不愿意做的事(排队买票),实际看演唱会的人还是我。客户端也并不关心代理类代理了哪个类,因为代码控制了客户端对委托类的访问。客户端代码表现为 Human human = new HuangNiu();
由于代理类实现了抽象角色的接口,导致代理类无法通用。比如,我的狗病了,想去看医生,但是排队挂号很麻烦,我也想有个黄牛帮我的排队挂号看病,但是黄牛它不懂这只狗的特性(黄牛跟狗不是同一类型,黄牛属于 Human 但狗属于 Animal 类)但排队挂号和排队买票相对于黄牛来说它两就是一件事,这个方法是不变的,现场排队。那我们能不能找一个代理说既可以帮人排队买票也可以帮狗排队挂号呢?
答案肯定是可以的,可以用动态代理。
基于接口的动态代理
如静态代理的内容所描述的,静态代理受限于接口的实现。动态代理就是通过使用反射,动态地获取抽象接口的类型,从而获取相关特性进行代理。因动态代理能够为所有的委托方进行代理,因此给代理类起个通用点的名字 HuangNiuHandle。先看黄牛类可以变成什么样?
public class HuangNiuHandle implements InvocationHandler {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object methodObject = null;
System.out.println("line up");
methodObject = method.invoke(proxyTarget, args);
System.out.println("go home and sleep");
return methodObject;
}
}
这个时候的客户端代码就变成这样了
public class Client {
public static void main(String[] args) {
HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
Human human = (Human) huangNiuHandle.getProxyInstance(new Me());
human.eat();
human.run();
human.lookConcert();
System.out.println("------------------");
Animal animal = (Animal) huangNiuHandle.getProxyInstance(new Dog());
animal.eat();
animal.run();
animal.seeADoctor();
}
}
使用动态代理有三个要点,
-
必须实现 InvocationHandler 接口,表明该类是一个动态代理执行类。
-
InvocationHandler 接口内有一实现方法如下: public Object invoke(Object proxy, Method method, Object[] args) 。使用时需要重写这个方法
-
获取代理类,需要使用 Proxy.newProxyInstance(Clas loader, Class<?>[] interfaces, InvocationHandler h) 这个方法去获取Proxy对象(Proxy 类类型的实例)。
注意到 Proxy.newProxyInstance 这个方法,它需要传入 3 个参数。解析如下:
// 第一个参数,是类的加载器
// 第二个参数是委托类的接口类型,证代理类返回的是同一个实现接口下的类型,保持代理类与抽象角色行为的一致
// 第三个参数就是代理类本身,即告诉代理类,代理类遇到某个委托类的方法时该调用哪个类下的invoke方法
Proxy.newProxyInstance(Class loader, Class<?>[] interfaces, InvocationHandler h)
再来看看 invoke 方法,用户调用代理对象的什么方法,实质上都是在调用处理器的
invoke 方法,通过该方法调用目标方法,它也有三个参数:
// 第一个参数为 Proxy 类类型实例,如匿名的 $proxy 实例
// 第二个参数为委托类的方法对象
// 第三个参数为委托类的方法参数
// 返回类型为委托类某个方法的执行结果
public Object invoke(Object proxy, Method method, Object[] args)
调用该代理类之后的输出结果:
由结果可知,黄牛不仅帮了(代理)我排队买票,还帮了(代理)我的狗排队挂号。所以,你看静态代理需要自己写代理类(代理类需要实现与目标对象相同的接口),还需要一一实现接口方法,但动态代理不需要。
注意,我们并不是所有的方法都需要黄牛这个代理去排队。我们知道只有我看演唱会和我的狗去看医生时,才需要黄牛,如果要实现我们想要的方法上面添加特定的代理,可以通过 invoke 方法里面的方法反射获取 method 对象方法名称即可实现,所以动态代理类可以变成这样:
public class HuangNiuHandle implements InvocationHandler {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object methodObject = null;
if ("lookConcert".equals(method.getName()) ||
"seeADoctor".equals(method.getName())) {
System.out.println("line up");
// 调用目标方法
methodObject = method.invoke(proxyTarget, args);
} else {
// 不使用第一个proxy参数作为参数,否则会造成死循环
methodObject = method.invoke(proxyTarget, args);
}
return methodObject;
}
}
结果如下:可以看到我们只在特定方法求助了黄牛
由此可见,动态代理一般应用在记录日志等横向业务。
值得注意的是:
-
基于接口类的动态代理模式,必须具备抽象角色、委托类、代理三个基本角色。委托类和代理类必须由抽象角色衍生出来,否则无法使用该模式。
-
动态代理模式最后返回的是具有抽象角色(顶层接口)的对象。在委托类内被 private 或者 protected 关键修饰的方法将不会予以调用,即使允许调用。也无法在客户端使用代理类转换成子类接口,对方法进行调用。也就是说上述的动态代理返回的是委托类(Me)或 (Dog)的就接口对象 (Human)或 (Animal)。
-
在 invoke 方法内为什么不使用第一个参数进行执行回调。在客户端使用getProxyInstance(new Child( ))时,JDK 会返回一个 proxy 的实例,实例内有InvokecationHandler 对象及动态继承下来的目标 。客户端调用了目标方法,有如下操作:首先 JDK 先查找 proxy 实例内的 handler 对象 然后执行 handler 内的 invoke 方法。
根据 public Object invoke 这个方法第一个参数 proxy 就是对应着 proxy 实例。如果在 invoke 内使用 method.invoke(proxy,args) ,会出现这样一条方法链,目标方法→invoke→目标方法→invoke...,最终导致堆栈溢出。
基于子类的动态代理
为了省事,我这里并没有继承父类,但在实际开发中是需要继承父类才比较方便扩展的。与基于接口实现类不同的是:
- CGLib (基于子类的动态代理)使用的是方法拦截器 MethodInterceptor ,需要导入 cglib.jar 和 asm.jar 包
- 基于子类的动态代理,返回的是子类对象
- 方法拦截器对 protected 修饰的方法可以进行调用
代码如下:
public class Me {
public void eat() {
System.out.println("eat meat ....");
}
public void run() {
System.out.println("I run with two legs");
}
public void lookConcert() {
System.out.println("Listen to Jacky Cheung's Concert");
}
protected void sleep() {
System.out.println("Go to bed at one o'clock in the morning");
}
}
Dog 类
public class Dog {
public void eat() {
System.out.println("eat Dog food ....");
}
public void run() {
System.out.println("Dog running with four legs");
}
public void seeADoctor() {
System.out.println("The dog go to the hospital");
}
}
黄牛代理类,注意 invoke() 这里多了一个参数 methodProxy ,它的作用是用于执行目标(委托类)的方法,至于为什么用 methodProxy ,官方的解释是速度快且在intercep t内调用委托类方法时不用保存委托对象引用。
public class HuangNiuHandle implements MethodInterceptor {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Enhancer.create(target.getClass(), target.getClass().getInterfaces(), this);
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object methodObject = null;
if ("lookConcert".equals(method.getName()) ||
"seeADoctor".equals(method.getName())) {
System.out.println("line up");
// 调用目标方法
methodObject = methodProxy.invokeSuper(proxy, args);
} else {
methodObject = method.invoke(proxyTarget, args);
}
return methodObject;
}
}
client 类
public class Client {
public static void main(String[] args) {
HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
Me me = (Me) huangNiuHandle.getProxyInstance(new Me());
me.eat();
me.run();
me.sleep();
me.lookConcert();
System.out.println("------------------");
Dog dog = (Dog) huangNiuHandle.getProxyInstance(new Dog());
dog.eat();
dog.run();
dog.seeADoctor();
}
}
结果:
注意到 Me 类中被 protected 修饰的方法 sleep 仍然可以被客户端调用。这在基于接口的动态代理中是不被允许的。
静态代理与动态代理的区别
静态代理需要自己写代理类并一一实现目标方法,且代理类必须实现与目标对象相同的接口。
动态代理不需要自己实现代理类,它是利用 JDKAPI,动态地在内存中构建代理对象(需要我们传入被代理类),并且默认实现所有目标方法。
源码下载:https://github.com/turoDog/review_java.git
最后
如果看到这里,喜欢这篇文章的话,请转发、点赞。微信搜索「一个优秀的废人」,欢迎关注。
回复「1024」送你一套完整的 java、python、c++、go、前端、linux、算法、大数据、人工智能、小程序以及英语教程。
回复「电子书」送你 50+ 本 java 电子书。