一起学设计模式 - 代理模式

文章目录
  1. 1. 概述
  2. 2. 案例
    1. 2.1. 静态代理
    2. 2.2. 动态代理
      1. 2.2.1. JDK动态代理
    3. 2.3. CGLIB动态代理
  3. 3. 总结
  4. 4. - 说点什么

代理模式(Proxy Pattern)属于结构型模式的一种,给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象。

概述

身处华夏大地的码农都知道,因为国内有个牛逼的GFW,所以导致无法访问 Google,不能访问Google怎么查资料呢?不能查资料怎么提升(装逼)呢,古语有云(扯犊子的):上有政策,下有对策,我们可以借助梯子(设置代理)的方法访问。

代理

  • 用户向代理服务器发起请求
  • 代理把HTTP请求发给目标服务器
  • 目标服务器接收响应并返回给代理
  • 代理服务器把HTTP响应发回给用户

在软件开发中,也有一种设计模式可以提供与之类似的功能。由于某些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称之为代理的第三者来实现间接访问,该方案对应的设计模式被称为代理模式

代理模式是一种广泛应用的结构型设计模式,常见的代理形式包括远程代理、安全代理、虚拟代理、缓冲代理、智能引用代理等。

结构图

代理模式结构图

存在的角色

  • Subject(抽象主题角色):声明了RealSubjectProxySubject的共同接口,客户端通常需要针对接口角色进行编程
  • ProxySubject(代理主题角色):包含了对真实(委托)对象(RealSubject)的引用,可以控制对RealSubject的使用,负责在需要的时候创建和删除,并对RealSubject的使用加以约束
  • RealSubject(真实主题角色):代理对象所代表的真实对象,也是最终引用的对象

案例

实现方式

  • 静态代理:代理类是在编译时就实现好的。Java 编译完成后代理类是一个实际的 class 文件。
  • 动态代理:代理类是在运行时生成的。编译完之后并没有实际的 class 文件,而是在运行时动态生成的类字节码,并加载到JVM中。

静态代理

UML图如下:

静态代理

1.定义Subject(抽象主题角色)

1
2
3
interface Subject {
void request();
}

2.创建RealSubject(真实主题角色),代理类中引用的对象

1
2
3
4
5
6
7
class RealSubject implements Subject {

@Override
public void request() {
System.out.println("Google 搜索 battcn ");
}
}

3.创建ProxySubject(代理主题角色),采用延迟加载的方式初始化RealSubject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ProxySubject implements Subject {

private Subject realSubject;

@Override
public void request() {
System.out.println("向代理服务器发起请求");
//用到时候才加载
if (realSubject == null) {
realSubject = new RealSubject();
}
realSubject.request();
System.out.println("代理服务器响应请求");
}
}

4.创建StaticProxyClient(测试类)

1
2
3
4
5
6
7
8
public class StaticProxyClient {

public static void main(String[] args) {
// 使用代理
Subject subject = new ProxySubject();
subject.request();
}
}

5.运行结果

1
2
3
向代理服务器发起请求
Google 搜索 battcn
代理服务器响应请求

RealSubject(真实主题角色)必须是事先创建好在的,并将其作为ProxySubject(代理主题角色)的内部属性。但是实际使用时,一个RealSubject(真实主题角色)色必须对应一个ProxySubject(代理主题角色),大量使用会导致类的急剧膨胀;此外,如果事先并不知道RealSubject角色,该如何使用ProxySubject呢?这个问题可以通过Java的动态代理类来解决。

动态代理

动态代理是指在运行时动态生成代理类。即,代理类的字节码将在运行时生成并载入当前代理的ClassLoader

常见的几种动态代理

  • JDK自带:内置在JDK中,无需要引入第三方 Jar 包,使用简单但相对功能较弱
  • CGLIB/Javassist:都是高级的字节码生成库,总体性能比 JDK 自带的动态代理好,且功能十分强大
  • ASM:低级的字节码生成工具,几乎在使用 Java bytecode 编程,对开发人员要求及高,但也是性能最好的一种动态代理生成工具。不过由于它使用繁琐,且性能也没有数量级的提升,与 CGLIB/Javassist 等高级字节码生成工具相比,ASM 程序的维护性较差,如果不是在对性能有苛刻要求的场合,还是推荐 CGLIB 或者 Javassist。

对比静态代理的好处

  • 不需要为RealSubject写一个形式上完全一样的封装类,利于维护
  • 使用动态代理的生成方法甚至可以在运行时制定代理类的执行逻辑,从而提升系统的灵活性。

JDK动态代理

以上述所示代码中的 ProxySubject 为例,使用JDK动态代理生成动态类,替换上例中的 ProxySubject

UML图如下:

JDK动态代理

1.JDK 的动态代理需要实现一个处理方法调用的Handler,用于实现代理方法的内部逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SubjectHandler implements InvocationHandler {
private Subject subject;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("向代理服务器发起请求");
//如果第一次调用,生成真实主题
if (subject == null) {
subject = new RealSubject();
}
subject.request();
//返回真实主题完成实际的操作
System.out.println("代理服务器响应请求");
//如果返回值可以直接 return subject.request();
return null;
}

public static Subject createProxy() {
return (Subject) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), new Class[]{Subject.class}, new SubjectHandler()
);
}
}

2.创建JdkProxyClient(测试类)

1
2
3
4
5
6
7
public class JdkProxyClient {

public static void main(String[] args) {
Subject proxy = SubjectHandler.createProxy();
proxy.request();
}
}

3.运行结果

1
2
3
向代理服务器发起请求
动态代理 Google 搜索 battcn
代理服务器响应请求

以上代码生成了一个实现了Subject接口的代理类,代理类的内部逻辑由 SubjectHandler决定。生成代理类后,由 newProxyInstance()方法返回该代理类的一个实例。至此,一个完整的动态代理完成了。

CGLIB动态代理

在Java中,动态代理类的生成主要涉及对 ClassLoader 的使用。以 CGLIB 为例,使用 CGLIB 生成动态代理,首先需要生成 Enhancer类实例,并指定用于处理代理业务的回调类。在 Enhancer.create() 方法中,会使用 DefaultGeneratorStrategy.Generate() 方法生成动态代理类的字节码,并保存在 byte 数组中。接着使用 ReflectUtils.defineClass() 方法,通过反射,调用ClassLoader.defineClass() 方法,将字节码装载到 ClassLoader 中,完成类的加载。最后使用 ReflectUtils.newInstance() 方法,通过反射,生成动态类的实例,并返回该实例。基本流程是根据指定的回调类生成 Class 字节码—通过 defineClass() 将字节码定义为类—使用反射机制生成该类的实例。

UML图如下:

CGLIB动态代理

1.依赖CGLIB的包

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.1</version>
</dependency>

2.定义反射类及重载方法

1
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
class SubjectProxy implements MethodInterceptor {

private Object target;

/**
* 创建代理对象
*
* @param target 目标对象
* @return
*/
public Object getInstance(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
// 回调方法
enhancer.setCallback(this);
// 创建代理对象
return enhancer.create();
}


@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("向代理服务器发起请求");
methodProxy.invokeSuper(o, objects);
System.out.println("代理服务器响应请求");
return null;
}
}

3.创建CglibProxyClient(测试类)

1
2
3
4
5
6
7
8
public class CglibProxyClient {

public static void main(String[] args) {
SubjectProxy proxy = new SubjectProxy();
Subject subject = (RealSubject) proxy.getInstance(new RealSubject());
subject.request();
}
}

4.运行结果

1
2
3
向代理服务器发起请求
CGLIB 动态代理 Google 搜索 battcn
代理服务器响应请求

总结

应用场合

  • 远程代理:为对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。比如说 WebService,当我们在应用程序的项目中加入一个 Web 引用,引用一个 WebService,此时会在项目中声称一个 WebReference 的文件夹和一些文件,这个就是起代理作用的,这样可以让那个客户端程序调用代理解决远程访问的问题;
  • 虚拟代理:对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理,这样就可以达到性能的最优化。比如打开一个网页,这个网页里面包含了大量的文字和图片,但我们可以很快看到文字,但是图片却是一张一张地下载后才能看到,那些未打开的图片框,就是通过虚拟代里来替换了真实的图片,此时代理存储了真实图片的路径和尺寸;
  • 安全代理:控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候;
  • 缓冲代理:为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,从而可以避免某些方法的重复执行,优化
    系统性能。
  • 智能引用:指当调用真实的对象时,代理处理另外一些事。比如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它,或当第一次引用一个持久对象时,将它装入内存,或是在访问一个实际对象前,检查是否已经释放它,以确保其他对象不能改变它。这些都是通过代理在访问一个对象时附加一些内务处理;

相关模式

  • 适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口
  • 装饰器模式为了增强功能,而代理模式是为了加以控制。

结束语

设计模式是前人工作的总结和提炼。通常,被人们广泛流传的设计模式都是对某一特定问题的成熟的解决方案。如果能合理地使用设计模式,不仅能使系统更容易地被他人理解,同时也能使系统拥有更加合理的结构。

参考文献:https://www.ibm.com/developerworks/cn/java/j-lo-proxy-pattern/

参考文献:https://www.zybuluo.com/pastqing/note/174679

- 说点什么

全文代码:https://gitee.com/battcn/design-pattern/tree/master/Chapter10/battcn-proxy

  • 个人QQ:1837307557
  • battcn开源群(适合新手):391619659

微信公众号:battcn(欢迎调戏)