0%

代理模式

前言

代理模式(Proxy Pattern)是一种常见的设计模式,也是 GoF 提出的 23 种设计模式中的一种,属于结构型设计模式。它使用代理对象完成用户请求,屏蔽用户对真实对象的访问。代理模式用处很多,本文主要介绍如何使用代理模式实现延迟加载面向切面编程,并着重介绍动态代理的几种实现方式。本文涉及到的示例代码以上传到 GitHub 上。

角色及职责

代理模式中的几个角色:

  • Subject,接口,定义了代理类和被代理类对外暴露的方法。代理类和被代理类都需要实现该接口。
  • RealSubject,真实对象(被代理类),真正实现功能的对象。
  • Proxy,代理类,用来封装真实对象。

0d73af744af2fd7ecfd3326888c3d69b.png

适用场景

代理模式通常用于解决以下几类问题:

  • 需要控制对象的访问权限
  • 需要扩展一个对象的功能

对此,代理模式使用 Proxy 封装真实对象,以达到控制真实对象的访问权限,并在此基础上提供额外功能。代理模式有以下几个典型的应用场景:

  • 远程代理,在分布式对象通信中,使用一个本地对象代表远程对象(分属不同地址空间)。本地对象是远程对象的代理,本地对象上的方法调用将导致远程对象上的方法调用。一个例子是 ATM 实现,其中 ATM 可以保存位于远程服务器上的银行信息的代理对象。
  • 虚拟代理,如果一个对象很复杂或很重,可以使用虚拟代理。比如当图像很大时,可以按需的去加载用到的部分(亦即延迟加载)。
  • 保护代理,可以控制资源的访问权限。

延迟加载

我们先来看看如何使用代理模式进行延迟加载。假设现在有一个数据库查询类 DBQuery,它执行查询返回一个字符串。由于数据查询需要连接数据库,所以在构造时会比较耗时。

1
2
3
4
5
6
7
8
9
public class DBQuery {
public DBQuery() {
Thread.sleep(1000); // create datebase connection
}

public String query() {
return "query result";
}
}

正常情况下,如果在程序启动的时候我们就去 new 这个对象,一旦存在大量类似的操作,系统的启动速度必定会受影响。而采用代理类替代 DBQuery,可以做到轻量级启动。

首先需要创建一个接口,对应着代理模式角色中的 Subject:

1
2
3
public interface IDBQuery {
String query();
}

真实对象 DBQuery 和代理类 DBQueryProxy 都需要实现这个接口。在代理类内部封装了真实对象,实际调用时使用委派(Delegation)调用被代理类的方法。这也是代理模式的核心所在。

1
2
3
4
5
6
7
8
9
10
11
public class DBQueryProxy implements IDBQuery {
private DBQuery dbQuery;

@Override
public String query() {
if (dbQuery == null) {
dbQuery = new DBQuery();
}
return dbQuery.query();
}
}

这样在启动时初始化代理类,由于代理类 DBQueryProxy 的构造函数什么也没做,所以启动是相当迅速的(下述代码第 1 步)。而真正查询数据库(第 2 步)时才创建 DBQuery 对象执行耗时操作,从而实现了需要的时候才加载

1
2
3
4
public static void main(String[] args) {
IDBQuery query = new DBQueryProxy(); // 1
query.query(); // 2
}

AOP

现在来考虑一件事,如果需要在查询语句的前后添加日志该怎么做?在没有代理类之前,我们可能只能在真实对象中添加两行日志。但严格来说,日志记录不属于 DBQuery 的职责,它破坏了类的纯粹性(违背了迪米特法则)。所以现在有了代理类 DBQueryProxy,我们理所因当的应该将日志记录放在代理类中。即如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class DBQueryProxy implements IDBQuery {
@Override
public String query() {
if (dbQuery == null) {
dbQuery = new DBQuery();
log.info("Create database connection.");
}
String result = dbQuery.query();
log.info("Query response is {}.", result);
return result;
}
}

这样就可以将日志功能与 DBQuery 解耦,我们做到了将业务功能与系统级功能解耦,但这还不够。我们实现解耦的方式是采用的静态代理,即代码在编译前就得写死了。虽然静态代理可以做到在不修改目标对象的功能前提下,对目标功能扩展,但它的缺点也显而易见,就是代码的冗余。一个 DBQuery,就需要一个代理类,那如果有一千个 DBQuery 难道要写一千个代理类吗?

AOP(Aspect Oriented Programming),面向切面编程的出现就是为了解决这类问题。它通过预编译方式和运行期动态代理实现程序功能的统一维护。最为人熟知的应该就是 Spring AOP 了。在 Spring 中提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发。业务对象只做它应该做的 —— 完成业务逻辑 —— 仅此而已。它们并不负责其它的系统级关注点,诸如日志记录,性能统计,安全控制,事务处理,异常处理等。通过这种分离我们可以做到改变系统级服务代码时不影响业务代码的逻辑。

关于预编译的方式就不多说了,它的典型代表是 AspectJ,它需要专门的 Java 编译器将系统级代码织入业务代码中。除此之外,还可以通过运行时织入,也就是动态代理。Spring AOP 就构建在动态代理基础之上

动态代理

动态代理使用字节码动态生成加载技术,在运行时生成并加载类。与静态代理相比,动态代理有诸多好处。首先,不需要为真实主题创建一个形式上完全一样的封装类,假如主题接口中方法很多,为每个方法重写一个代理方法也是非常麻烦的事,一旦接口发生变更,意味着真实主题与代理类都要修改,不利于系统维护;其次,使用动态代理的生成方法可以指定代理类的执行逻辑,从而提升系统的灵活性。

生成动态代理的方式很多,包括 JDK 自带的动态代理、CGLIBJavassist 等,本文只介绍前两种的使用方式。

JDK 动态代理

JDK 从 1.3 开始提供了对动态代理的支持,它可以动态的创建代理类并动态的处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。

调用处理器需要实现 InvocationHandler 接口,重写 invoke 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DynamicProxyHandler implements InvocationHandler {
private static final Logger log = LoggerFactory.getLogger(DynamicProxyHandler.class);
private Object proxied;

public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("Dynamic proxy {} class {}() method.", proxied.getClass().getSimpleName(), method.getName());
Object invoke = method.invoke(proxied, args);
log.info("result is {}.",invoke);
return invoke;
}
}

下面代码在 main 方法中生成了一个实现了 IDBQuery 接口的代理类,代理类的内部逻辑由 DynamicProxyHandler 决定。

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
IDBQuery proxy = (IDBQuery) Proxy.newProxyInstance(
IDBQuery.class.getClassLoader(),
new Class[]{IDBQuery.class},
new DynamicProxyHandler(new DBQuery()));
proxy.query();
}

// [main] INFO DynamicProxyHandler - Dynamic proxy DBQuery class query() method.
// [main] INFO DynamicProxyHandler - result is query result.

通过调用静态方法 Proxy.newProxyInstance() 可以创建动态代理,该方法需要得到一个类加载器(通常可以从已经被加载的对象中获取其类加载器,然后传递给它),一个你希望该代理实现的接口列表(不是类或抽象类),以及 InvocationHandler 接口的一个实现。动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的构造器传入一个“实际”对象的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发。

invoke 方法中传入了代理对象(即参数 proxy)以备你需要区分请求的来源,但在多数情况下你并不关心这一点。然而,在 invoke 内部,在代理上调用方法需要格外当心,因为对接口的调用将被重定向为对代理的调用。通常,你会执行被代理的操作,然后使用 method.invoke() 将请求转发给被代理对象,并传入必需的参数。

CGLIB 动态代理

Java 提供的动态代理使用简单,它内置在 JDK 中,因此不需要引入第三方 Jar 包,但相对功能较弱。除此之外,CGLIB 动态代理库为 JDK 的动态代理提供了很好的补充,并且性能更好。

CGLIB 为没有实现接口的类提供代理,它针对代理的类,动态生成一个子类,然后子类覆盖代理类中的方法。如果是 private 或是 final 类修饰的方法,则不会被重写。通常可以使用 Java 的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB 是一个好的选择。

CGLIB 作为一个开源项目,其代码托管在 GitHub 上。

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
public class CglibProxy implements MethodInterceptor {
private Object target;

public CglibProxy(Object target) {
this.target = target;
}

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("cglib proxy begin");
Object result = method.invoke(target, objects);
System.out.println(result + "\ncglib proxy end");
return result;
}

public static Object getProxy(Object target) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(new CglibProxy(target));
return enhancer.create();
}

public static void main(String[] args) {
DBQuery proxy = (DBQuery) CglibProxy.getProxy(new DBQuery());
proxy.query();
}
}

// cglib proxy begin
// query result
// cglib proxy end

与 JDK 自带的代理类似,基于 CGLIB 的代理类需要实现 MethodInterceptor 接口,重写 intercept 方法。使用 Enhancer 生成需要被代理的类的子类的实例。

在 Spring 的 AOP 编程中,如果加入容器的目标对象有实现接口,使用 JDK 代理。如果目标对象没有实现接口,则使用 CGLIB 代理。

Hibernate 对动态代理的应用

动态代理的一个经典应用就是 Hibernate 框架。当 Hibernate 加载实体 Bean 时,并不会一次性将数据库所有的数据都装载,而是采用延迟加载的机制,以提高系统的性能。这里以属性的延迟加载为例,展示 Hibernate 是如何使用动态代理的。

从数据库载入 ID 为 1 的 User 用户,并且打印它的类名、父类名、实现的接口名。最后调用 getName 方法取得数据库数据。演示代码采用的 Hibernate 版本为 3.2.6,不同的 Hibernate 版本实现会有差异。

1
2
3
4
5
6
7
8
User user = (User)sessionFactory.getSession().load(User.class, 1);
System.out.println("Class name: " + user.getClass().getName());
System.out.println("Superclass name: " + user.getClass().getSuperclass().getName());
for (Class<?> anInterface : user.getClass().getInterfaces()) {
System.out.println("Interface name: " + anInterface.getName());
}

System.out.println("\n" + user.getName());

程序输出如下:

1
2
3
4
5
6
Class name: com.github.s1mplecc.gof.proxy.User$EnhancerByCGLIB$N8wfgVhe
Superclass name: com.github.s1mplecc.gof.proxy.User
Interface name: org.hibernate.proxy.HibernateProxy

Hibernate: select user0_.id as id1_0_0_, user0_.name as name2_0_0_ from t_user user0_ where user0_.id=?
Geym

从输出来看,load 方法加载出的 User 类并不是我们定义的 User 类,而是名为 com.github.s1mplecc.gof.proxy.User$EnhancerByCGLIB$N8wfgVhe 的类。从名称上可以推测它是使用 CGLIB 的 Enhancer 生成的动态类。该类的父类才是我们自定义的 User 类,此外,它还实现了 HibernateProxy 接口。由此可见,Hibernate 使用一个动态代理生成的子类替代用户定义的类。只有在真正使用对象数据时,才去数据库加载实际的数据。从输出结果来看,在调用 getName 方法之前从未输出过一条 SQL 语句,这说明 User 对象被加载时根本没有访问数据库。Hibernate 正是用这种方式实现了延迟加载。

另外,由于 CGLIB 停止维护很长一段时间了,Hibernate 从 3.5.5 版本开始弃用 CGLIB 而使用 Javassist。感兴趣的同学可以自行下去研究。

参考

  • Proxy pattern —— Wiki
  • 《Java 编程思想》
  • 《Java 程序性能优化 —— 让你的 Java 程序更快、更稳定》