1. 代理设计模式
- 代理设计模式是Java中针对接口编程的一种应用模式。
- java中代理分为静态代理和动态代理。
- 一个接口有很多实现类,其中包括真实主题实现类,和代理主题实现类。真实主题实现类执行真正的核心业务,代理主题实现类负责边缘业务。
- 代理设计模式的核心:外部通过操作代理主题,间接的操作真实主题。
- 代理设计模式中,代理主题实际上是个代理对象,通过委托真实主题去执行真正的操作,而代理主题本身可以在委托真实主题执行前后执行其他边缘性业务。
- 在代理设计模式中,外部直接操作代理主题,真实主题是被代理类委托代理的,即代理类代理了真实主题,代理类委托真实主题处理真正业务。
- 代理设计模式本身是对委托机制的一种具体应用场景,它是对委托机制和方法转发的一种模式化应用。
- 代理设计模式实现上和装饰者设计模式比较相似,但还是有区别的,区别等学习装饰者模式时再讲。
2. 到底为什么要使用代理模式?
- 有时候,真实主题的某些成员是隐私,不便暴露给客户端,此时可以使用代理主题去代理真实主题,这样,避免了客户端直接操作真实主题。
- 有时候,真实主题不具备处理某些附加操作的能力,真实主题不愿因承担过多的边缘性业务能力,真实主题只想处理核心功能,此时可通过代理主题去代理真实主题,这样便可以不用改动真实主题逻辑,而直接在代理主题上强化那些边缘性业务即可。
- 有时候,实例化真实主题很慢(比如真实主题去解析大批量文件或者链接数据库等),而真实主题的实例化往往是在系统启动时进行的,这时候就可以使用代理主题,屏蔽掉真实主题耗时的实例化过程,实现真实主题的延迟加载。这样只有在需要调用真实主题的时候再去通过代理主题实例化真实主题并调用真实主题的功能。
- 有时候,只想定义一个接口,不想实现它但它的方法仍然可用;或者想为任何一个目标类增加附加功能,就使用动态代理。动态代理比静态代理更灵活。
3. 代理设计模式案例
通过案例了解下,不使用代理模式时是什么样子的,使用了代理模式是什么样子的。
静态代理是什么样子的,动态代理又是什么样子的。
先准备一个接口,和一个真实主题。真实主题实现对应的接口。
IService
package zeh.test.demo.com.daili.service;
// 业务接口
public interface IService {
// 接口方法
void handleSome();
// 接口方法
void handleOther();
}
PersonService
package zeh.test.demo.com.daili.service;
// 接口实现类,真实主题
// zWX5331241 2021-03-30
public class PersonService implements IService {
@Override
public void handleSome() {
System.out.println("派出一个人去处理一些业务");
}
@Override
public void handleOther() {
System.out.println("派出一个人去处理其他事情");
}
}
3.1 不使用代理模式
Main
package zeh.test.demo.com.daili.nodaili;
import zeh.test.demo.com.daili.service.IService;
import zeh.test.demo.com.daili.service.PersonService;
// 不使用代理
public class Main {
public static void main(String[] args) {
// 不使用任何代理,直接使用真实主题,通过多态进行向上转型
IService service = new PersonService();
// 直接通过真实主题对象去实现某些功能
service.handleSome();
service.handleOther();
}
}
测试结果
派出一个人去处理一些业务
派出一个人去处理其他事情
Process finished with exit code 0
结果分析
不使用代理对象,直接调用真实主题,就是我们普通的面向对象编程的那种套路。
假如我现在想增强一下service对象的功能,比如,我想让service对象再实现一个“派出一头猪去执行刺杀任务”这个需求,现在该怎么办呢?
以下是改造方案:
改造主题接口 IService.java,增加一个方法:
// 改动接口,增加方法
void handleKillTaskByPig();
再改造对应的真实主题实现,实现接口中增强的那个方法:
@Override
public void handleKillTaskByPig() {
System.out.println("我是一头猪,我现在要去刺杀秦始皇");
}
最后开始测试:
public class Main {
public static void main(String[] args) {
// 不使用任何代理,直接使用真实主题,通过多态进行向上转型
IService service = new PersonService();
// 直接通过真实主题对象去实现某些功能
service.handleSome();
service.handleOther();
// 改造了主题接口,和其对应的真实主题的实现类,增加了一个方法
service.handleKillTaskByPig();
}
}
测试结果如下:
派出一个人去处理一些业务
派出一个人去处理其他事情
我是一头猪,我现在要去刺杀秦始皇
Process finished with exit code 0
弊端:
为了增强真实主题的功能,现在改动了主题的接口和实现。
- 在开发中,频繁改动顶层接口不太优雅。
- 增强的这个方法可能只是 PersonService 这一个实现需要,并不是所有的实现类都需要,直接改动了接口,势必需要所有的实现类都得做出相应的改动。
- 如果接口存在很多实现类,则要依次修改所有的实现类。
- 退一万步讲,对于真实主题而言,它的核心功能就是 handleSome 和 handleOther ,它本身并不需要做其他功能的增强。
…..
等等等等。
这种情况如何解决呢?
3.2 静态代理
使用静态代理。
- 通过新增加一个代理类去代理真实主题对象,即委托真实主题去处理核心业务,而代理主题去处理边缘业务,此时代理主题处理的边缘业务对于核心业务而言,就是我们需要增强的功能。
- 既然新增的代理主题是为了增强真实主题对象的功能,那么,代理主题暴露给客户端的API应该和真实主题原有的暴露方式是一致的。因此,代理主题需要和真实主题实现同一个接口,并且覆盖主题接口中的业务方法从而对外暴露业务。
- 代理主题类实现主题接口,需要实现主题接口中的所有方法。需要代理哪些方法就对哪些方法委托真实主题去处理并加入代理逻辑,可以根据诉求代理一个方法或者代理多个方法。
新增代理主题实现类 StaticProxy
package zeh.test.demo.com.daili.statics;
import zeh.test.demo.com.daili.service.IService;
// 代理主题。
// 和真实主题实现相同的业务接口,目的是为了暴露给客户端使用完全相同的API。即代理主题和真实主题暴露给客户端的使用方式应该不被客户端感知。
public class StaticProxy implements IService {
private IService iService;
public StaticProxy(IService iService) {
this.iService = iService;
}
@Override
public void handleSome() {
System.out.println("静态代理 begin");
iService.handleSome();
System.out.println("静态代理 end");
}
@Override
public void handleOther() {
System.out.println("静态代理 begin");
iService.handleOther();
System.out.println("静态代理 end");
}
}
测试静态代理 StaticProxyMain
package zeh.test.demo.com.daili.statics;
import zeh.test.demo.com.daili.service.IService;
import zeh.test.demo.com.daili.service.PersonService;
// 测试静态代理 zWX5331241 2021-03-30
public class StaticProxyMain {
public static void main(String[] args) {
// 控制程序先向代理主题注入真实主题对象,然后直接调用代理主题的方法,当执行代理主题的方法时,线程被转发到真实主题的方法。
IService service = new PersonService();
StaticProxy proxy = new StaticProxy(service);
proxy.handleSome();
proxy.handleOther();
}
}
测试结果
静态代理 begin
派出一个人去处理一些业务
静态代理 end
静态代理 begin
派出一个人去处理其他事情
静态代理 end
Process finished with exit code 0
结果分析
通过静态代理模式,增强了真实主题的功能:
1. 没有改动业务接口。
2. 没有改动实现类。
3. 真实主题实际上没有发生任何改动,只是在它上层加了一层代理类。
4. 真实主题不直接暴露给客户端,直接暴露给客户端的是代理主题。
3.3 静态代理的缺陷
新增一个主题接口 IKill
package zeh.test.demo.com.daili.service;
public interface IKill {
void killPeople();
}
新增一个真实主题实现类 PersonKill
package zeh.test.demo.com.daili.service;
public class PersonKill implements IKill {
@Override
public void killPeople() {
System.out.println("我去杀人啦");
}
}
要想代理IKill接口,则必须新增一个代理主题实现类 StaticProxyForKill
package zeh.test.demo.com.daili.statics;
import zeh.test.demo.com.daili.service.IKill;
public class StaticProxyForKill implements IKill {
private IKill kill;
public StaticProxyForKill(IKill kill) {
this.kill = kill;
}
@Override
public void killPeople() {
System.out.println("增强逻辑:你杀人你想过后果吗?");
kill.killPeople();
System.out.println("增强逻辑:你杀人了,等着抵命吧!");
}
}
测试
package zeh.test.demo.com.daili.statics;
import zeh.test.demo.com.daili.service.IKill;
import zeh.test.demo.com.daili.service.IService;
import zeh.test.demo.com.daili.service.PersonKill;
import zeh.test.demo.com.daili.service.PersonService;
// 测试静态代理
public class StaticProxyMain {
public static void main(String[] args) {
// 控制程序先向代理主题注入真实主题对象,然后直接调用代理主题的方法,当执行代理主题的方法时,线程转发到真实主题的方法。
IService service = new PersonService();
StaticProxy proxy = new StaticProxy(service);
proxy.handleSome();
proxy.handleOther();
IKill kill = new PersonKill();
StaticProxyForKill proxyForKill = new StaticProxyForKill(kill);
proxyForKill.killPeople();
}
}
测试结果
静态代理 begin
派出一个人去处理一些业务
静态代理 end
静态代理 begin
派出一个人去处理其他事情
静态代理 end
增强逻辑:你杀人你想过后果吗?
我去杀人啦
你杀人了,等着抵命吧!
Process finished with exit code 0
结果分析
静态代理存在如下问题:
- 静态代理为了保证客户端对代理对象和真实对象不感知,所以需要和真实主题实现相同的接口,如此一来,如果业务接口做了改动,不仅仅真实主题需要改动,代理主题实现类也需要进行变更。换言之,假设业务接口新增了一个方法,除了所有真实主题实现类需要覆盖该方法外,代理主题类也需要覆盖该方法。
- 代理主题只能代理一种类型的真实主题,比如案例中,StaticProxy只能代理业务接口 IService 的真实主题;如果新增了一个业务接口,完蛋了,需要新增一个对应的代理主题实现。
- 代理主题尽管可以代理真实主题的一个或者多个方法,但是极端情况下,如果主题接口有成千上百个方法,则除了真实主题需要实现这100个方法外,代理主题同样需要实现这100个方法才能完成对其的代理。
总的来说:
如果静态代理想为多种业务去提供增强服务,则需要创建很多代理类;
如果业务接口有很多方法,则代理主题需要覆盖很多方法。
假设有100个接口需要的代理,每个接口有100个业务方法,那么就需要创建100个代理类,每个代理类需要覆盖100个业务方法。
这无形中增加了代码冗余和维护难度,除了维护真实主题外,还需要额外维护一套代理主题类。
4. 死磕动态代理
既然静态代理存在那么多弊端,我们设想下:
如果我们不用去编写代理主题类,而是能有个什么东西默认为我们去生成对应业务的代理主题类、自动的注入对应的真实主题对象、覆盖主题接口中的所有业务方法、并且在我们访问该代理对象的业务方法时能够自动的转发到真实主题对应的方法上,那该有多好啊!
说白了,就是我想找一个大神来帮我做静态代理这一堆事,而不是我自己去挨个编写代码!
很幸运,jdk就提供了这种能力:jdk的动态字节码机制可以为任何接口去生成一个存在于JVM内存中的虚拟实现类,有了这种能力,我们就可以实现上述设想了。
jdk提供的动态主要依赖2个核心类:Proxy类和InvocationHandler类。这两个类后续会做详细接收,此处先使用。
jdk创建动态代理对象有两种方式,下面我们来看下。
先来个接口Service。
package zeh.test.demo.com.daili.service;
// 接口
public interface Service {
void method1();
void method2();
void method3();
}
4.1 创建动态代理-方式1
方式1的步骤:
- 调用Proxy.getProxyClass方法获取目标字节码(代理类)的Class对象。
- 使用InvocationHandler接口创建代理类的回调器。
- 通过代理类的class对象和InvocationHandler回调器创建代理对象。
- 上面已经创建好代理类对象了,接下来就可以直接使用代理对象。
通过方式1的步骤创建Service接口的代理对象
package zeh.test.demo.com.daili.dynamic;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import zeh.test.demo.com.daili.service.Service;
// dk创建代理对象方式1
// 方式1主要是结合反射,开发者通过反射自己控制代理对象的生成
public class DynamicProxyMethod1 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 1. 调用Proxy.getProxyClass方法获取目标字节码(代理类)的Class对象。
Class<Service> proxyClass = (Class<Service>) Proxy.getProxyClass(Service.class.getClassLoader(), Service.class);
// 2. 使用InvocationHandler接口创建代理类的回调器。
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我是jdk的invocationHandler,被调用的方法是:" + method.getName());
return null;
}
};
// 3. 通过代理类的class对象和InvocationHandler回调器创建代理对象。
Service serviceProxy = proxyClass.getConstructor(InvocationHandler.class).newInstance(invocationHandler);
// 4. 上面已经创建好代理类对象了,接下来就可以直接使用代理对象。
serviceProxy.method1();
serviceProxy.method2();
serviceProxy.method3();
}
}
执行结果
我是jdk的invocationHandler,被调用的方法是:method1
我是jdk的invocationHandler,被调用的方法是:method2
我是jdk的invocationHandler,被调用的方法是:method3
4.2 创建动态代理-方式2
创建代理对象有更简单的方式。
方式2的步骤
- 使用InvocationHandler接口创建代理类的回调器。
- 使用Proxy类的静态方法newProxyInstance直接创建代理对象。
- 直接使用代理对象。
通过方式2的步骤创建Service接口的代理对象
package zeh.test.demo.com.daili.dynamic;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import zeh.test.demo.com.daili.service.Service;
// jdk创建代理对象方式2
public class DynamicProxyMethod2 {
public static void main(String[] args) {
// 1. 使用InvocationHandler接口创建代理类的回调器。
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我是jdk的invocationHandler,被调用的方法是:" + method.getName());
return null;
}
};
// 2. 使用Proxy类的静态方法newProxyInstance直接创建代理对象。
Service serviceProxy = (Service) Proxy.newProxyInstance(Service.class.getClassLoader(), new Class[]{Service.class}, invocationHandler);
// 3. 直接使用代理对象。
serviceProxy.method1();
serviceProxy.method2();
serviceProxy.method3();
}
}
执行结果
我是jdk的invocationHandler,被调用的方法是:method1
我是jdk的invocationHandler,被调用的方法是:method2
我是jdk的invocationHandler,被调用的方法是:method3
5. 动态代理结合真实主题案例
上面我们只是单纯的使用jdk的两种方式为一个接口创建了动态代理对象,这个代理对象并没有绑定对应的真实主题。
我们使用代理模式,一般就是需要通过代理主题对象去代理真实主题对象,从而达到对真实主题对象功能扩充的能力。
所以,我们一般需要专门创建一个动态代理主题类,负责为注入进来的真实主题对象产生动态代理对象即可。这样才能达到代理真实主题对象的目的。
不论存在多少种类的业务接口,不论业务接口存在多少方法,都可以通过一个动态代理主题类就可以实现通用的代理方案。
准备的主题接口和真实主题如下。
IKill
package zeh.test.demo.com.daili.service;
public interface IKill {
void killPeople();
void killTrump();
}
IService
package zeh.test.demo.com.daili.service;
// 业务接口
public interface IService {
void handleSome();
void handleOther();
void handleEat();
void handleDrink();
}
PersonKill
package zeh.test.demo.com.daili.service;
public class PersonKill implements IKill {
@Override
public void killPeople() {
System.out.println("我去杀人啦");
}
@Override
public void killTrump() {
System.out.println("杀了川普");
}
}
PersonService
package zeh.test.demo.com.daili.service;
// 接口实现类,真实主题
public class PersonService implements IService {
@Override
public void handleSome() {
System.out.println("派出一个人去处理一些业务");
}
@Override
public void handleOther() {
System.out.println("派出一个人去处理其他事情");
}
@Override
public void handleEat() {
System.out.println("派出一个人去吃饭");
}
@Override
public void handleDrink() {
System.out.println("派出一个人去喝水");
}
}
5.1 动态代理结合真实真题
动态代理工厂 DynamicProxy
package zeh.test.demo.com.daili.dynamic;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 动态代理主题类,不用实现业务接口,实际上是个动态代理工厂,负责根据传入的参数动态生成代理对象
public class DynamicProxy {
// 委托一个真实主题对象,该对象通过Object接收,可以接受任何类型
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}
// 动态生成代理对象,实际上是动态生成代理对象对应的字节码文件,然后根据字节码文件创建的代理对象。
// InvocationHandler实际上是一个回调接口,其中的 invoke 方法实际上是一个通用的回调方法。
public Object getProxyInstance() {
// Proxy.newProxyInstance方法根据传入的真实主题对象的class和对应的接口,生成对应的代理对象。
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("动态代理 begin");
Object returnValue = method.invoke(target, args);
System.out.println("动态代理 end");
return returnValue;
}
});
return proxy;
}
}
测试动态代理 DynamicProxyMain
package zeh.test.demo.com.daili.dynamic;
import zeh.test.demo.com.daili.service.IKill;
import zeh.test.demo.com.daili.service.IService;
import zeh.test.demo.com.daili.service.PersonKill;
import zeh.test.demo.com.daili.service.PersonService;
public class DynamicProxyMain {
public static void main(String[] args) {
IService iService = new PersonService();
IKill iKill = new PersonKill();
System.out.println("真实主题类型:" + iService.getClass());
System.out.println("真实主题类型:" + iKill.getClass());
IService serviceProxy = (IService) new DynamicProxy(iService).getProxyInstance();
IKill killProxy = (IKill) new DynamicProxy(iKill).getProxyInstance();
System.out.println("内存中存在的代理对象类型:" + serviceProxy.getClass());
System.out.println("内存中存在的代理对象类型:" + killProxy.getClass());
// 执行代理方法
serviceProxy.handleSome();
serviceProxy.handleOther();
serviceProxy.handleDrink();
serviceProxy.handleEat();
killProxy.killPeople();
killProxy.killTrump();
}
}
测试结果
真实主题类型:class zeh.test.demo.com.daili.service.PersonService
真实主题类型:class zeh.test.demo.com.daili.service.PersonKill
内存中存在的代理对象类型:class com.sun.proxy.$Proxy0
内存中存在的代理对象类型:class com.sun.proxy.$Proxy1
动态代理 begin
派出一个人去处理一些业务
动态代理 end
动态代理 begin
派出一个人去处理其他事情
动态代理 end
动态代理 begin
派出一个人去喝水
动态代理 end
动态代理 begin
派出一个人去吃饭
动态代理 end
动态代理 begin
我去杀人啦
动态代理 end
动态代理 begin
杀了川普
动态代理 end
Process finished with exit code 0
结果分析
1. 动态代理只需要实现一个动态代理工厂类,用于产生动态代理对象即可。
2. 动态代理工厂可以根据传入的类型,为指定类型的接口生成指定的代理对象。
3. 动态代理对象可以代理任何指定类型的真实主题。
4. 动态代理要求真实主题类至少应该实现一个接口,但不要求代理主题工厂类实现接口。
遗留问题?
JDK提供的动态代理,只能代理接口吗?
换言之,只能为接口生成动态字节码的实现类吗?
如果需要代理一个普通类,该怎么办呢?
解答:
JDK提供的动态代理,只能代理接口。因为动态代理底层生成的代理类的字节码,已经继承了Proxy类,因此,这个动态字节码就不能再继承其他类了。所以,它只能代理接口。
如果需要代理普通类,请使用第三方的cglib。
6. jdk动态代理总结
- jdk动态代理可以实现通用代理,即可以代理任何目标接口的实现类。
- jdk动态代理的原理是底层提供的动态字节码技术,即jdk可以为任何接口都动态产生一个虚拟的实现子类,该子类字节码随着JVM启动存储在内存中,JVM销毁则销毁。
- jdk动态代理只能代理接口,而不能代理普通类,因此,它要求被代理的真实主题类必须至少实现一个接口,否则将无法为其生成代理子类。
文档信息
- 本文作者:Marshall