设计模式学习笔记 - 代理模式(Proxy)

概述

  代理,我们在各式各样的技术博客文章、书籍、框架中都见到过。这说明代理模式的运用是十分频繁。
  从字面意思上去理解十分简单:代表某事某物进行某种动作、行为。而这个词在不同的领域中,描述不同的事物。生活中与代理相关的事物是挺常见的,比如:代购商,房产中介,XXX代言人,VPN,网游代练等等。

我查阅了很多图书以及网上各种资料,它们都是这样来描述代理模式的。

在《设计模式 - 可复用面向对象软件的基础》中是这样描述代理模式的意图:

为其他对象提供一种代理控制对这个对象的访问

在《Thinking In Java》中,是这样描述代理的:

代理是继承与组合之间的中庸之道,
因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。
我们使用代理时可以拥有更多的控制力,因为我们可以选择只提供在成员对象中的方法的某个子集。

而网上有的资料是这样描述的:

当无法直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问

我们可以发现所有对代理模式的描述都有一个比较明显的共通点:控制访问。这个是代理模式的核心理念。

应用形式

根据不同的需求,代理模式又可以分为多种类型。

  • 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可以在另一台主机中,远程代理又被称为“大使”(Ambassador)。

  • 虚代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。

  • 保护代理(Protect Proxy):控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。

  • 智能引用代理(Smart Reference Proxy): 取代了简单的指针,它在访问对象时执行一些附加操作。
      1.对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它。
      2.当第一次引用一个持久对象时,将它装入内存。
      3.在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。

代理模式的结构、组成与实现

结构

代理模式相较其他模式之下,它的结构显得更加简单。下面是代理模式抽象描述下的UML图。

组成

对,结构正如这个图所示这么简单,3个类1个接口,除了客户端类,构成代理模式正是以下这3个元素:

  • Subject(抽象主题):它声明了真实主题和代理的共同接口,这样依赖在任何使用真实主题的敌方都可以使用对应的代理,客户端通常是针对抽象主题进行编程。

  • Proxy(代理):它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;通常,在代理中,客户端在调用所引用的真实主题操作之前或之后还附加执行一些内务处理(Housekeeping task)。

  • Real Subject(真实主题):它定义了代理所代表的真实对象,在真实主题中实现了真实的业务操作,客户端可以通过代理间接调用真实主题中定义的操作。

实现

抽象主题

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

真实主题

1
2
3
4
5
6
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("---------真实主题中业务逻辑操作---------");
}
}

代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Proxy implements Subject {
private RealSubject realSubject = new RealSubject();

@Override
public void request() {
preprocess();
realSubject.request();
postprocess();
}

private void preprocess() {
System.out.println("*********前置附加操作*********");
}

private void postprocess() {
System.out.println("*********后置附加操作*********");
}
}

客户端

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
Subject proxy = new Proxy();
proxy.request();
}
}

Java动态代理

既然我这里要做的是有关代理模式的笔记,那现在顺便就把Java中的动态代理要点给一块记下来了。

上面所演示的是最基本的静态代理,很简单;当然,代理模式的其他应用形式则复杂得多,不过不用特殊技术实现的话,它们还是属于静态代理。而在Java中,JDK提供了动态代理的实现方案。那么问题来了:动态代理比静态代理有什么优势呢?

这里还是用一个实际例子进行说明:一个业务类,在每个业务方法执行之后打印其所消耗的时间。

起始阶段

首先我们来看业务接口,业务接口Service中有两个业务方法doSomethingAdoSomethingB

1
2
3
4
5
public interface Service {
void doSomethingA();

void doSomethingB();
}

再来的就是实现业务接口的业务类ServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ServiceImpl implements Service {
@Override
public void doSomethingA() {
System.out.println("ServiceImpl.doSomethingA");

try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public void doSomethingB() {
System.out.println("ServiceImpl.doSomethingB");

try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

不使用代理

在没有接触到代理模式之前,我们可以这样做:在每个业务方法中加入这些统计时间的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ServiceImpl implements Service {
@Override
public void doSomethingA() {
long start = System.currentTimeMillis();

System.out.println("ServiceImpl.doSomethingA");

long end = System.currentTimeMillis();
System.out.println("doSomethingA - duration: " + (end - start));
}

@Override
public void doSomethingB() {
long start = System.currentTimeMillis();

System.out.println("ServiceImpl.doSomethingB");

long end = System.currentTimeMillis();
System.out.println("doSomethingB - duration: " + (end - start));
}
}

这样做确实是可以的,但是随之而来的就有2个问题:

  • 统计时间这些与业务毫不相关的代码都置于业务方法中,业务方法的代码逻辑会显得混乱起来。
  • 如果这个业务类的方法越来越多,这样冗余代码会越来越多。

使用静态代理

我们肯定要摒弃上面那种解决方案,再来就是运用代理模式来实现这个需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ServiceProxy implements Service {
private Service service = new ServiceImpl();

@Override
public void doSomethingA() {
long start = System.currentTimeMillis();

service.doSomethingA();

long end = System.currentTimeMillis();
System.out.println("duration: " + (end - start));
}

@Override
public void doSomethingB() {
long start = System.currentTimeMillis();

service.doSomethingB();

long end = System.currentTimeMillis();
System.out.println("duration: " + (end - start));
}
}

代码也十分简单,这里就将统计时间的代码放到了静态代理ServiceProxy中,而ServiceImpl类还是跟起始阶段一样。静态代理把业务逻辑代码与其他额外操作的代码分离了,这解决了之前代码逻辑混乱的问题,而代码冗余过多的问题还是没能解决的。

使用JDK提供的动态代理

JDK所提供的动态代理是由Java的反射技术所支撑起来的,所以这种方式是动态的,因为决定一个代理的许多因素都是在运行时指定的。

在《Thinking In Java》中是这样描述动态代理的:
Java的动态代理比代理的思想更向前迈进一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。

因为是JDK提供的这一特性,那肯定少不了动态代理相关的API了:java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。

Proxy类中最主要的是运用newProxyInstance方法创建代理对象。

1
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

调用这个方法创建代理对象之前需要指定3个参数:

  • loader:加载代理类字节码到JVM中所用的类加载器。
  • interfaces:代理类所要实现的所有接口。
  • h:动态代理处理方法调用时所重定向的调用处理器。

而实现InvocationHandler接口从而自定义调用处理器时最主要就是实现其中的invoke方法,它作为回调方法,代理对象会调用它。

1
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

这个方法同样是有3个参数:

  • proxy:正在调用该方法的代理对象。
  • method:相应于在代理对象上调用的接口方法。
  • args:调用代理对象上的接口方法时所用到的参数。

动态代理的工作原理:在动态代理所做的所有调用都被重定向到单一的调用处理器上。
这一点十分重要,根据这个特性,就能解决代理中冗余代码过多的问题。而在实现调用处理器时,通常都会向其构造器传递一个“实际”对象的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发,说通俗一点就是调用真正的业务逻辑方法。

这里还是继续使用上面那个例子的代码,最主要的就是编写调用处理器类以及修改一下客户端。

调用处理器,在invoke方法里,在调用委托对象目标方法的之前和之后都执行了与计时相关的代码,最后返回执行委托对象目标方法的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PrintDurationHandler implements InvocationHandler {
private Object delegator;

public PrintDurationHandler(Object delegator) {
this.delegator = delegator;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
long start = System.currentTimeMillis();

// 调用目标方法
Object result = method.invoke(delegator, args);

long end = System.currentTimeMillis();
System.out.println(method.getName() + " - duration: " + (end - start));

return result;
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
Service proxy = (Service) Proxy.newProxyInstance(
Service.class.getClassLoader(),
new Class<?>[] {Service.class},
new PrintDurationHandler(new ServiceImpl())
);

proxy.doSomethingA();
proxy.doSomethingB();
}
}

现在,之前使用静态代理那种解决方案所出现的代码冗余过多的问题就得以解决了,因为客户端调用任何代理对象中的方法,它们都会首先被转发到PrintDurationHandler类中的invoke方法。所以,现在增加再多的业务方法,我们只需关心业务方法的代码,而无需再为每个增多的业务方法编写计时代码。

使用CGLib动态代理

我们也非常清楚地看到,若想使用JDK动态代理,我们所提供的委托对象必须实现某些接口。这是使用JDK动态代理的一个限制条件。

如果我们所提供的委托对象没有实现任何接口呢?幸好,还有另一种解决方案,那就是使用CGLib动态代理

CGLib全称Code Generation Library,它是一个强大的,高性能,高质量的Code生成类库。它的底层主要是由ASM框架实现的,还有的就是,很多博客技术文章都说CGLib动态代理的性能要比JDK动态代理的要高,关于这个我就不打破沙锅问到底,现在我主要还是先把CGLib动态代理这种技术用上再说。

既然要使用CGlib动态代理这种由第三方开发的工具,那我们首先就要导入jar包:

1.使用Maven导入,只需配置该依赖即可。

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.2</version>
</dependency>

2.如果是纯手工导入jar包,则必须导入以下几个包。

cglib-3.2.2.jarCGlib核心功能,asm-5.0.4CGLib底层实现。

那么接下来,我们肯定要了解使用哪些API才能实现我们所需。类比、模仿在学习新知识时非常重要;类似实现JDK动态代理,
在实现CGLib动态代理时最主要用到的两个API:net.sf.cglib.proxy.Enhancer类和net.sf.cglib.proxy.MethodInterceptor接口。

Enhancer类相当于之前Proxy类,它也提供了创建代理对象的方法create。同样的,MethodInterceptor接口的作用和InvocationHandler接口差不多。

Enhancer类中用于给用户创建代理对象的方法create有4种重载形式。

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
30
31
32
33
34
35
36
37
/**
* Generate a new class if necessary and uses the specified
* callbacks (if any) to create a new object instance.
* Uses the no-arg constructor of the superclass.
* @return a new instance
*/
public Object create() {

/**
* Helper method to create an intercepted object.
* For finer control over the generated instance, use a new instance of <code>Enhancer</code>
* instead of this static method.
* @param type class to extend or interface to implement
* @param callback the callback to use for all methods
*/
public static Object create(Class type, Callback callback)

/**
* Helper method to create an intercepted object.
* For finer control over the generated instance, use a new instance of <code>Enhancer</code>
* instead of this static method.
* @param type class to extend or interface to implement
* @param interfaces array of interfaces to implement, or null
* @param callback the callback to use for all methods
*/
public static Object create(Class superclass, Class interfaces[], Callback callback)

/**
* Helper method to create an intercepted object.
* For finer control over the generated instance, use a new instance of <code>Enhancer</code>
* instead of this static method.
* @param type class to extend or interface to implement
* @param interfaces array of interfaces to implement, or null
* @param filter the callback filter to use when generating a new class
* @param callbacks callback implementations to use for the enhanced object
*/
public static Object create(Class superclass, Class[] interfaces, CallbackFilter filter, Callback[] callbacks)

MethodInterceptor接口中只有一个方法intercept,直接翻译过来我们可以称它为拦截方法。

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
/**
* General-purpose {@link Enhancer} callback which provides for "around advice".
* @author Juozas Baliuka <a href="mailto:baliuka@mwm.lt">baliuka@mwm.lt</a>
* @version $Id: MethodInterceptor.java,v 1.8 2004/06/24 21:15:20 herbyderby Exp $
*/
public interface MethodInterceptor
extends Callback
{
/**
* All generated proxied methods call this method instead of the original method.
* The original method may either be invoked by normal reflection using the Method object,
* or by using the MethodProxy (faster).
* @param obj "this", the enhanced object
* @param method intercepted Method
* @param args argument array; primitive types are wrapped
* @param proxy used to invoke super (non-intercepted method); may be called
* as many times as needed
* @throws Throwable any exception may be thrown; if so, super method will not be invoked
* @return any value compatible with the signature of the proxied method. Method returning void will ignore this value.
* @see MethodProxy
*/
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;

}

API了解得差不多了,想必现在你也可以根据它们写出相应的代码来了吧,代码和之前JDK动态代理差不多。

JDK动态代理中最主要的是实现调用处理器,而在CGLib动态代理中要实现的则是方法拦截器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PrintDurationInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
long start = System.currentTimeMillis();

// 调用目标方法
Object result = proxy.invokeSuper(obj, args);

long end = System.currentTimeMillis();
System.out.println(method.getName() + " - duration: " + (end - start));

return result;
}
}

而在拦截方法里,同样是调用目标方法,然后在之前和之后都执行了计时代码。
而其中所有区别的是,我们无须再通过构造函数传入委托对象(目标对象)。

在创建代理对象之前,我们先来创建一个没有实现任何接口的业务类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyService {
public void doSomethingA() {
System.out.println("MyService.doSomethingA");

try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void doSomethingB() {
System.out.println("MyService.doSomethingB");

try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

最后就是编写客户端,通过Enhancer类的create方法创建业务类MyService的代理对象并调用业务方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
enhancer.setCallback(new PrintDurationInterceptor());

MyService proxy = (MyService) enhancer.create();
System.out.println("proxy = " + proxy);

proxy.doSomethingA();
proxy.doSomethingB();
}
}