漫谈代理模式
定义与目的
代理模式为某对象提供一种代理(一个替身或者占位符),从而控制对这个对象的访问。
原文:Provide a surrogate or placeholder for another object to control access to it.
多数写设计模式的书中都如此直译描述代理模式的定义与作用,而在《设计模式之美》中做了另一番阐述:
在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
感觉后者站在开发者角度看问题,相对来说更容易理解。
代理模式主要目的是控制访问,而非加强功能。在代理类中附加的是与原始类无关的功能,将非功能性附加需求与业务功能解耦,放到代理中统一处理,方便开发人员只关注业务功能方面的开发。而装饰器模式是对功能的增强,装饰器附加的是根原始类相关的增强功能。
应用场景
常用的使用场景有:
- 非功能性需求开发:监控、统计、限流、事务、日志等需求。
- 远程代理:RPC框架也可以看作是一种远程代理模式。Java本身提供了RMI相关jar包用于远程调用,WebService是RPC的一种实现。
- 缓存代理:为开销大的结果提供暂时存储。如通过请求参数,当存在缓存标识则从缓存中直接获取数据返回,反之从数据库等处获取数据并处理后返回。如Spring中的
@Cacheable
方法。
在《Head First 设计模式》中,基于访问控制,做了另一种细分,在此列出来可作了解,大致包含如下,:
- 远程代理:控制访问远程对象。可以作为另一个JVM上对象的本地代表,调用代理方法,利用网络转发到远程执行,并且结果通过网络返回给代理,再由代理转给调用的客户。在Java中有RMI用于实现远程调用,WebService是RPC框架的一种实现。
- 虚拟代理:控制访问创建开销大的资源。在真正需要时才创建,创建过程中由虚拟代理代替对象,创建后将请求直接委托给对象。
- 保护代理:基于权限控制对资源的访问,如鉴权,也可以归到上面的非功能性需求开发。
- 防火墙代理:控制网络资源的访问,用于保护主题免受侵害。常出现在公司的防火墙系统。
- 写入时复制代理:虚拟代理的变体,通过延迟对象的复制,直到客户端真的需要时的方式用来控制对象的复制。实现可参考Java的 CopyOnWriteArrayList 相关。
- 智能引用代理:当主题被引用时,进行额外的动作。类似上面的非功能性需求开发。如通过Spinrg AOP拦截添加相关功能。
- 同步代理:多线程中为主题提供安全的访问。常出没于 JavaSpaces , 为分布式环境内的潜在对象集合提供同步访问控制。(这块接触不多,不是特别了解,有需要的自己查找相关资料)
- 复杂隐藏代理:用来隐藏一个类的复杂度,并进行访问控制。有时还也称为外观代理。与外观模式的区别是,外观模式只提供另一组接口。(这块接触不多,不是特别了解,有需要的自己查找相关资料)
代理模式的UML类图
代理模式的UML类图如下:
由上图可见,代理模式一般包括3个角色:
- 抽象主题角色(ISubject):负责声明真实主题与代理的共同接口方法。可以是接口或者抽象类。
- 真实主题角色(RealSubject):也称被代理类,负责执行系统的真实业务逻辑。
- 代理主题角色(SubjectProxy):也称代理类,由于内部持有真实主题角色的引用,所以可以完全代理真实主题角色,同时可以在调用真实对象的方法前后增加一些新的处理代码。
某些情况下一个对象不适合或不能直接引用另一个类,而代理对象(代理主题角色)可以在客户端与目标对象(真实主题对象)之间起到中介作用。
通用写法
下面是代理模式的通用写法:
// 1.创建抽象主题角色
public interface ISubject {
public void doSomeThing();
}
// 2. 创建真实主题角色
public class RealSubject implements ISubject{
@Override
public void doSomeThing() {
PrintUtill.println("真正的对象做一些事情");
}
}
// 3.创建代理主题角色
public class SubjectProxy implements ISubject{
private ISubject subject;
// 初始化时传入真实主题角色的引用
public SubjectProxy(ISubject subject) {
this.subject = subject;
}
@Override
public void doSomeThing() {
before();
this.subject.doSomeThing();
after();
}
private void before() {
PrintUtill.println("真正对象执行操作之前的附加功能>>>>>>>>>");
}
private void after() {
PrintUtill.println("真正对象执行操作之后的附加功能>>>>>>>>>");
}
}
// 4. 客户端--用于调用测试
public class SubejctClient {
public static void main(String[] args) {
SubjectProxy subjectProxy = new SubjectProxy(new RealSubject());
subjectProxy.doSomeThing();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
通过代码可见这是基于接口的实现方式,也是静态代理的通用实现方式。
代理模式除了基于接口实现,还可以基于继承实现:
- 基于接口的实现:参照基于接口而非实现编程的设计思想,将原始类对象替换为代理类对象的时候,为了让代码改动尽量少,故代理类和原始类需要实现相同的接口。
- 基于继承的实现:如果原始类(即被代理类)没有实现接口,且不是我们开发的维护的(如来自第三方类库),无法直接修改原始类,给它重新定义一个接口。针对这种无法直接修改原始类的外部类的修改,一般采用继承的方式。
动态代理
在通用实现中,我们看到了静态代理的写法。同时可以看出这种直接创建业务代码的代理类还是存在一些问题的:
- 需要在代理类中将被代理类中的所有方法都重新实现一遍,并且为每个方法添加类似的代码逻辑。
- 若需要添加附加功能不止一个,需要对每个被代理类都重新创建一个代理类。
浙江导致项目中类型数量成倍增加,从而增加了代码维护成本,且每个代理类中的代码都是些模板式的“重复”代码,也增加了不必要的开发成本。
基于上述问题,我们可以引入动态代理(Dynamic Proxy):
不事先为被代理类创建代理类,而是在运行时动态创建对应的代理类,然后在系统中用代理类代替被代理类。
Java中存在两种动态代理实现,JDk动态代理与CGLIB动态代理。
- JDk动态代理,可变相当作是一种基于接口的实现,被代理类必需实现相应接口,因为最终代理类需要实现相应接口。具体实现依赖Java的反射语法。
- CGLIB动态代理,可变相看作基于继承的实现,最终实现的虚拟类会继承被代理类。实现上利用ASM开源包,通过修改被代理类的字节码生成子类。
在《深入理解JAVA虚拟机》(第三版)中关于动态代理的“动态”的描述如下:
动态代理中所说的“动态”,是针对使用Java代码实现编写了代理类的“静态”代理而言的,它的优势是不在于省去了编写代理类那一点编码工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中。
–9.2.3
JDK动态代理
关键涉及两个:Proxy(可理解为调度器)和 InvocationHandler(调度处理器)。
Proxy 通过反射创建动态代理类,其代理类会继承Proxy类。
InvocationHandler
是一个接口,里面只有一个invoke
方法。该接口用于实现代理的行为,即用于提供被代理类方法调用发生时所需附加的功能。
当代理的方法被调用时,便会被转发给 InvocationHandler
接口的具体实现类,调用 invoke
方法。此方法中既有附加的新功能,又有被代理类的方法调用。
这里以在支付服务中新增日志为例,具体代码实例如下:
(1) 基础支付类
// 支付接口,
public interface PayService {
void pay();
void pay(int a);
}
// 微信支付实现
public class WXPayService implements PayService{
@Override
public void pay() {
PrintUtill.println("微信支付>>>>>>WXPayService>>>>>>>>>pay>>>>>>>>>>>");
}
@Override
public void pay(int a) {
PrintUtill.println("微信支付>>>>>>WXPayService>>>>>>>>>pay>>>>>>>>>>>"+a);
}
}
// 客户端
public class LoggerDynamicClient {
public static void main(String[] args) {
// 用于生成在运行时产生的代理类--非必需
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
// 创建日志调度处理器
LoggerDynamicProxyHandler proxy = new LoggerDynamicProxyHandler();
// 为 微信支付 添加日志
PayService pay = (PayService) proxy.getInstance(new WXPayService());
pay.pay();
PrintUtill.printlnRule();
// 为 用户服务 添加日志 -实现与支付服务类似。
BaseService userService = (BaseService) proxy.getInstance(new UserService());
userService.add();
// showProxyClass();
}
// 代理类文件生成方式一,可指定要生成的代理类以及代理类名称
public static void showProxyClass() {
String path = "./$Proxy0.class";
// 生成字节码的方法,在运行时产生一个描述代理类的字节码byte[]数组。
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0",
WXPayService.class.getInterfaces());
FileOutputStream out = null;
try {
out = new FileOutputStream(path);
out.write(classFile);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
(2) 代理实现
public class LoggerDynamicProxyHandler implements InvocationHandler {
private SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-DD hh:mm:ss");
private Object target;
// 返回一个实现了接口,并且代理了真实对象实例行为的对象
public Object getInstance(Object target) {
this.target = target;
Class<?> clazz = target.getClass();
// newProxyInstance 初始化代理,基于被代理者的类加载器、实现的接口,以及当前代理类
// 返回一个实现了PayService接口的,并且代理了new WXPayService()实例行为的对象
return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
}
// 用于实现代理的行为
// proxy:动态生成的匿名代理类
// method:调用的方法
// args:真实主题类method的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object obj = method.invoke(target,args);
after();
return obj;
}
private void before() {
PrintUtill.println("日志动态代理开始>>>>>>>>>>>>>" + sdf.format(System.currentTimeMillis()) + ">>>>>>>>>>>");
}
private void after() {
PrintUtill.println("日志动态代理完成>>>>>>>>>>>>>" + sdf.format(System.currentTimeMillis()) + ">>>>>>>>>>>");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
通过在上述mian函数中添加如下一句话,可查看在运行时产生的代理类。
// 用于生成在运行时产生的代理类--非必需
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
2
实现原理
JDK动态代理采用字节重组,重新生成对象来代替被代理对象,以达到动态代理的目的。JDK动态代理生成对象的方式如下:
(1) 通过反射获取被代理对象的引用以及它的所有接口。
(2) 通过Proxy类重新生成一个新类,同时新的类实现被代理类实现的所有接口。
(3) 动态生成Java代码,新加的业务逻辑方法由一定的逻辑代码调用(如InvocationHandler的实现类)。
(4) 编译新生成的Java类代码.class文件
(5) 重新加载到JVM中运行。
以上过程就是字节码重组。其中(2)-(4)并不是先生成.java文件再生成.class文件,大致的生成过程是根据Class文件的格式规范去拼装字节码。
CGLIB动态代理
CGLIB动态代理需要引入单独Jar包,具体版本根据自己需要引入,这里仅作参考:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.7</version>
</dependency>
2
3
4
5
CGLIB动态代理的实现类似JDK的动态代理,都需要实现相应的调度处理器接口。
这里依旧以上述支付服务添加日志为例,原则上不需要抽象主题角色,仅需创建真实主题角色即可。这里仅展示CGLIB的关键实现部分。
// 客户端
public class LoggerCGlibProxyClient {
public static void main(String[] args) {
LoggerCGlibProxyInterceptor proxy = new LoggerCGlibProxyInterceptor();
PayService pay = (PayService) proxy.getInstance(WXPayService.class);
pay.pay();
}
}
// 调度处理器接口实现
public class LoggerCGlibProxyInterceptor implements MethodInterceptor {
public Object getInstance(Class target) {
// 创建一个字节码增强器,可以用来为无接口的类创建代理
Enhancer enhancer = new Enhancer();
// 设置要代理的业务类(即:为下面生成的代理类指定父类)
enhancer.setSuperclass(target);
// 设置回调方法实现类:调用被代理类的方法时,会通过调用该实现类的intercept的方法,从而实现代理的行为。
enhancer.setCallback(this);
// create方法生成Target的代理类,并返回代理类的实例
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("windcoder.com日志开始...");
//代理类调用父类的方法
proxy.invokeSuper(obj, args);
System.out.println("windcoder.com日志结束...");
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
CGLIB之所以比JDK执行代理方法的效率高。是因为其是用了FastClass机制。该原理简单来说是:
- 为代理类和被代理类个生成一个类,该类会为代理类或者被代理类的方法分配一个index(int类型,类似索引)。
- 将index当作入参,FastClass可以直接定位到相应方法并直接调用执行,省去了反射调用。
查看CGLIB生成代理类的方式,可在main函数中添加如下代码
// 设置输出目录,方便之后查看CGLIB生成的class---第二个参数是路径,可自行修改
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./tmp/");
2
JDk与CGLIB两种动态代理比较
- JDK 实现了被代理对象的接口,CGLIB 继承了被代理对象。
- JDK 和 CGLIB 均在运行时生成字节码。JDK 直接榭 Class 字节码,CGLIB 通过 ASM 框架生成 Class 字节码。CGLIB 实现更复杂,生成代理类比JDk效率低。
- JDk 通过反射机制调用代理方法,CGLIB 通过 FastClass 机制直接调用方法,CGLIB执行效率比JDK高。
- CGLIB 无法代理 final 修饰的类。
总结
静态代理与动态代理区别
- 静态代理需要手动完成代理操作,若被代理类新增方法,则代理类需同步增加,违背开闭原则。
- 动态代理采用在运行时动态生成代码字节码的方式,没有了对被代理类扩展的限制,遵循开闭原则。
- 若动态代理要对目标类的增强逻辑进行扩展,结合策略模式,只需新增策略类即可,无需修改代理类的代码。
代理类优点
- 代理模式能将代理对象与被代理对象分离。在一定程度上降低了系统的耦合性,扩展性好。
- 可以起到保护目标对象的作用(即控制访问)。
- 可以增强被代理类的功能(如增加一些非功能性需求的功能)。
代理模式的缺点
- 代理模式会造成系统设计中类的数量增加。
- 在客户端增加一个代理对像,会导致处理请求的速度变慢。
- 增加了系统的复杂性(适用于多数设计模式)。
代理模式暂时写这些,相关具体的实现原理并没深入,可自行查找相关资料了解。代理模式在框架中使用率较高,比如Spring的AOP底层就是基于动态代理实现的,Spring 中代理的选择原则如下:
- 当 Bean 有实现接口时,使用 JDk 动态代理。
- 当 Bean 没有实现接口时, 选择CGLIB 动态代理。
- 可通过配置强制使用 CGLIB 。
关于代理模式之前也写过几次,有兴趣可以参考看一下:
若想深入了解Spring AOP的内容,若自己阅读源码难度较大,推荐看一下《小马哥讲Spring AOP编程思想》。
参考资料:
- 设计模式之美
- 《Head First 设计模式》
- 《设计模式就该这么学》
- 《Java设计模式及实践》
除特别注明外,本站所有文章均为 windcoder 原创,转载请注明出处来自: mantandailimoshi

暂无数据