一 概述
1.1 什么是代理模式
为其他对象提供一种代理以控制对这个对象的访问。
实例:王二狗公司老板突然在发工资的前一天带着小姨子跑路了,可怜二狗一身房贷,被迫提起劳动仲裁,劳动局就会为其指派一位代理律师全权负责二狗的仲裁事宜。那这里面就是使用了代理模式,因为在劳动仲裁这个活动中,代理律师会全权代理王二狗。
1.2 解决什么问题
下面是一些使用场景,不过太抽象,暂时可以不要在意,随着你的不断进步你终究会明白的。
- 远程代理 :为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率
- 虚拟代理:通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销
- 缓冲代理:为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间
- 保护代理:可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限
- 智能引用:要为一个对象的访问(引用)提供一些额外的操作时可以使用
1.3 代理模式的分类
代理模式分为静态代理与动态代理
那么,静态代理与动态代理的区别是什么呢?所谓静态代理,其实质是预先确定了代理与被代理者的关系。映射到编程领域的话,就是指代理类与被代理类的依赖关系在编译期间就确定了。
但是,如果我们需要很多的代理,每一个都这么去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例来完成具体的功能。总的来说,根据代理类的创建时机和创建方式的不同,我们可以将代理分为静态代理和动态代理两种形式。
二 代理模式实现原理(静态代理)
代理模式主要包含三个角色,即:抽象主题 (Subject)、委托类 (被代理,Proxied) 以及代理类 (Proxy)。
- 抽象主题(Subject):可以是接口,也可以是抽象类
- 委托类(proxied):真实主题角色,业务逻辑的具体执行者
- 代理类(Proxy):内部含有对真实对象 RealSubject 的引用,负责对真实主题角色的调用,并在真实主题角色处理前后做预处理和后处理
下面就是王二狗劳动仲裁的代码实现:
定义一个代表诉讼的接口( 抽象主题 Subject)
public interface ILawSuit {
void submit(String proof); // 提起诉讼
void defend(); // 法庭辩护
}
王二狗诉讼类型,实现 ILawSuit 接口( 委托类 proxied)
public class SecondDogWang implements ILawSuit {
@Override
public void submit(String proof) {
System.out.println(String.format("老板欠薪跑路,证据如下:%s",proof));
}
@Override
public void defend() {
System.out.println(String.format("铁证如山,%s还钱","马旭"));
}
}
代理律师诉讼类,实现 ILawSuit 接口( 代理类Proxy)
public class ProxyLawyer implements ILawSuit {
ILawSuit plaintiff; // 持有要代理的那个对象
public ProxyLawyer(ILawSuit plaintiff) {
this.plaintiff=plaintiff;
}
@Override
public void submit(String proof) {
plaintiff.submit(proof);
}
@Override
public void defend() {
plaintiff.defend();
}
}
产生代理对象的静态代理工厂类
public class ProxyFactory {
public static ILawSuit getProxy(){
return new ProxyLawyer(new SecondDogWang());
}
}
这样就基本构建了静态代理关系了,然后在客户端就可以使用代理对象来进行操作了。
public static void main(String[] args) {
ProxyFactory.getProxy().submit("工资流水在此");
ProxyFactory.getProxy().defend();
}
输出结果如下:
老板欠薪跑路,证据如下:工资流水在此
铁证如山,马旭还钱
可以看到,代理律师全权代理了王二狗的本次诉讼活动。那使用这种代理模式有什么好处呢,我们为什么不直接让王二狗直接完成本次诉讼呢?现实中的情况比较复杂,但是我可以简单列出几条:这样代理律师就可以在提起诉讼等操作之前做一些校验工作,或者记录工作。例如二狗提供的资料,律师可以选择的移交给法庭而不是全部等等操作,就是说可以对代理的对做一些控制。例如二狗不能出席法庭,代理律师可以代为出席。。。
三 动态代理
动态代理本质上仍然是代理,情况与上面介绍的完全一样,只是代理与被代理人的关系是动态确定的,例如王二狗的同事牛翠花开庭前没有确定她的代理律师,而是在开庭当天当庭选择了一个律师,映射到编程领域为这个关系是在运行时确定的。
那既然动态代理没有为我们增强代理方面的任何功能,那我们为什么还要用动态代理呢,静态代理不是挺好的吗?凡是动态确定的东西大概都具有灵活性,强扩展的优势。
上面的例子中如果牛翠花也使用静态代理的话,那么就需要再添加两个类。一个是牛翠花诉讼类,一个是牛翠花的代理律师类,还的在代理静态工厂中添加一个方法。而如果使用动态代理的话,就只需要生成一个诉讼类就可以了,全程只需要一个代理律师类,因为我们可以动态的将很多人的案子交给这个律师来处理。
-
动态代理是在实现阶段不用关心代理类,而在运行阶段才指定哪一个对象
-
动态代理类的源码是在程序运行期间由 JVM 根据反射等机制动态生成的
代理类型 | 使用场景 |
---|---|
JDK 动态代理 | 如果目标对象实现了接口,采用 JDK 的动态代理 |
CGLIB 动态代理 | 如果目标对象没有实现了接口,必须采用 CGLIB 动态代理 |
3.1 JDK 动态代理
在 java 的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler 接口、另一个则是 Proxy 类,这个类和接口是实现我们动态代理所必须用到的。
InvocationHandler 接口是给动态代理类实现的,负责处理被代理对象的操作的,而 Proxy 是用来创建动态代理类实例对象的,因为只有得到了这个对象我们才能调用那些需要代理的方法。
接下来我们看下实例,牛翠花动态指定代理律师是如何实现的。
1、构建一个牛翠花诉讼类
public class CuiHuaNiu implements ILawSuit {
@Override
public void submit(String proof) {
System.out.println(String.format("老板欠薪跑路,证据如下:%s",proof));
}
@Override
public void defend() {
System.out.println(String.format("铁证如山,%s还牛翠花血汗钱","马旭"));
}
}
2、构建一个动态代理类
public class DynProxyLawyer implements InvocationHandler {
private Object target; // 被代理的对象
public DynProxyLawyer(Object obj){
this.target=obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("案件进展:"+method.getName());
Object result=method.invoke(target,args);
return result;
}
}
3、修改静态工厂方法
public class ProxyFactory {
......
public static Object getDynProxy(Object target) {
InvocationHandler handler = new DynProxyLawyer(target);
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), handler);
}
}
4、客户端使用
public static void main(String[] args) {
ILawSuit proxy= (ILawSuit) ProxyFactory.getDynProxy(new CuiHuaNiu());
proxy.submit("工资流水在此");
proxy.defend();
}
输出结果为:
案件进展:submit
老板欠薪跑路,证据如下:工资流水在此
案件进展:defend
铁证如山,马旭还牛翠花血汗钱
3.1.1 JDK 动态代理原理
首先 JDK 的动态代理实现方法是依赖于接口的,首先使用接口来定义好操作的规范。然后通过 Proxy 类产生的代理对象调用被代理对象的操作,而这个操作又被分发给 InvocationHandler 接口的 invoke 方法具体执行:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
此方法的参数含义如下:
- proxy:代表动态代理对象
- method:代表正在执行的方法
- args:代表当前执行方法传入的实参
- 返回值:表示当前执行方法的返回值
例如上面牛翠花案例中,我们使用 Proxy 类的 newProxyInstance() 方法生成的代理对象 proxy 去调用了 proxy.submit (“工资流水在此”); 操作,那么系统就会将此方法分发给 invoke()。其中 proxy 对象的类是系统帮我们动态生产的,其实现了我们的业务接口 ILawSuit。
3.2 CGLib 动态代理
由于 JDK 只能针对实现了接口的类做动态代理,而不能对没有实现接口的类做动态代理,所以 CGLib 横空出世!CGLib(Code Generation Library)是一个强大、高性能的 Code 生成类库,它可以在程序运行期间动态扩展类或接口,它的底层是使用 java 字节码操作框架 ASM 实现。
1、引入 CGLib 库
cglib-nodep-3.2.6.jar:使用 nodep 包不需要关联 asm 的 jar 包,jar 包内部包含 asm 的类。
2、定义业务类,被代理的类没有实现任何接口
public class Frank {
public void submit(String proof) {
System.out.println(String.format("老板欠薪跑路,证据如下:%s",proof));
}
public void defend() {
System.out.println(String.format("铁证如山,%s还Frank血汗钱","马旭"));
}
}
3、定义拦截器
在调用目标方法时,CGLib 会回调 MethodInterceptor 接口,进行方法拦截,来实现你自己的代理逻辑,类似于 JDK 中的 InvocationHandler 接口。
public class cgLibDynProxyLawyer implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] params,
MethodProxy methodProxy) throws Throwable {
if (method.getName().equals("submit"))
System.out.println("案件提交成功,证据如下:"+ Arrays.asList(params));
Object result = methodProxy.invokeSuper(o, params);
return result;
}
}
4、定义动态代理工厂,生成动态代理
public class ProxyFactory {
public static Object getGcLibDynProxy(Object target){
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(new cgLibDynProxyLawyer());
Object targetProxy= enhancer.create();
return targetProxy;
}
}
5、客户端调用
public static void main(String[] args) {
Frank cProxy= (Frank) ProxyFactory.getGcLibDynProxy(new Frank());
cProxy.submit("工资流水在此");
cProxy.defend();
}
输出结果:
案件提交成功,证据如下:[工资流水在此]
老板欠薪跑路,证据如下:工资流水在此
铁证如山,马旭还Frank血汗钱
可见,通过 CGLib 对没有实现任何接口的类做了动态代理,达到了和前面一样的效果。这里只是简单的讲解了一些 CGLib 的使用方式,有兴趣的可以进一步了解其比较高级的功能,例如回调过滤器(CallbackFilter)等。
3.2.1 CGLib 动态代理原理
CGLib 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是 final 的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用 java 反射的 JDK 动态代理要快。
CGLib 底层:使用字节码处理框架 ASM,来转换字节码并生成新的类。不鼓励直接使用 ASM,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。
CGLib 缺点:对于 final 方法,无法进行代理。
3.3 动态代理在 AOP 中的应用
什么是 AOP? 维基百科上如是说:
定义:In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns。
AOP 是一种编程范式,其目标是通过隔离切面耦合来增加程序的模块化。
首先声明,AOP 是 OOP 的补充,其地位及其重要性远不及 OOP,总体来说 OOP 面向名词领域而 AOP 面向动词领域,例如对一个人的设计肯定是使用 OOP,例如这个人有手,脚,眼睛瞪属性。而对这个人上厕所这个动作就会涉及到 AOP,例如上厕所前需要先确定一下拿没拿手纸等。
要理解 AOP 就首先要理解什么是切面耦合(cross-cutting concerns)。例如有这样一个需求,要求为一个程序中所有方法名称以 test 开头的方法打印一句 log,这个行为就是一个典型的 cross-cutting 场景。首先这个打印 log 和业务毫无关系,然后其处于分散在整个程序当中的各个模块,如果按照我们原始的方法开发,一旦后期需求变动将是及其繁琐的。所以我们就需要将这个切面耦合封装隔离,不要将其混入业务代码当中。
例如在王二狗的案子中,我们希望在案子起诉后打印一句成功的 log,如果不使用代理的话,我们是需要将 log 写在相应的业务逻辑里面的,例如王二狗诉讼类 SecondDogWang 里面的 submit() 方法中。使用了动态代理后,我们只需要在 InvocationHandler 里面的 invoke() 方法中写就可以了,不会侵入业务代码当中,在以后的维护过程中对业务毫无影响,这是我们希望看到的。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("submit"))
System.out.println("案件提交成功,证据如下:"+ Arrays.asList(args));
Object result=method.invoke(target,args);
return result;
}
输出结果为:
案件提交成功,证据如下:[工资流水在此]
老板欠薪跑路,证据如下:工资流水在此
铁证如山,马旭还牛翠花血汗钱
所以 AOP 主要可以用于:日志记录,性能统计,安全控制,事务处理,异常处理等场景下。
四 总结
静态代理比动态代理更符合 OOP 原则,在日常开发中使用也较多。动态代理在开发框架时使用较多,例如大名鼎鼎的 Spring。
最后希望王二狗和牛翠花他们可以顺利拿回自己的血汗钱。