跳到主要内容

代理模式

1. 模式概述

1.1 定义

代理模式(Proxy Design Pattern)是一种结构型设计模式,它通过提供一个代理对象来控制对另一个对象的访问。代理对象作为客户端和目标对象之间的中介,可以在访问目标对象前后添加额外的处理逻辑,如权限控制、日志记录、性能监控等。

代理模式在不改变原始类(或者叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

1.2 核心思想

  • 控制访问:代理对象控制对目标对象的访问
  • 透明性:客户端无需知道代理的存在,可以像使用目标对象一样使用代理对象
  • 增强功能:在不修改目标对象的前提下,通过代理对象增强功能

1.3 现实生活类比

代理模式在现实生活中有很多例子:

  • 海外代购:您不直接购买海外商品,而是通过代购代理
  • 房产中介:您不直接与房东交易,而是通过中介代理
  • 律师代理:您不直接出庭,而是通过律师代理
  • 明星经纪人:粉丝不直接联系明星,而是通过经纪人代理

1.4 软件开发中的应用

代理模式中引入了一个新的代理对象,代理对象在客户端对象和目标对象之间起到了中介的作用,它去掉客户不能看到的内容和服务或增加客户需要的额外的新服务。

2. 模式结构

2.1 角色定义

代理模式包含三个核心角色:

  1. 抽象主题(Subject)类:声明了真实主题和代理主题的共同接口,确保在任何使用真实主题的地方都可以用代理主题去替换。

  2. 真实主题(Real Subject)类:实现了抽象主题类中的具体业务,是代理对象所代表的真实对象,也是最终要引用的对象。

  3. 代理类(Proxy):代理也实现了抽象主题中的具体业务,其内部包含着对真实主题的引用,控制着对真实主题的访问。

2.2 类图关系

  • 代理类持有对真实主题的引用
  • 代理类和真实主题都实现相同的接口
  • 客户端通过代理类访问真实主题

3. 实现方式

3.1 静态代理

静态代理在编译时就确定了代理关系,需要为每个目标类创建对应的代理类。

优点

  • 可以在不修改目标类的前提下,扩展目标类的功能
  • 实现简单,易于理解
  • 编译时就能确定代理关系

缺点

  • 会产生大量代理类,代码冗余
  • 接口变更时,目标对象和代理对象都需要修改
  • 每个目标类都需要对应的代理类
/**
* 抽象主题类 - 定义共同接口
*/
public interface IUserDao {
void save();
void update();
void delete();
}

/**
* 真实主题类 - 实现具体业务逻辑
*/
public class UserDaoImpl implements IUserDao {
@Override
public void save() {
System.out.println("保存用户数据到数据库");
}

@Override
public void update() {
System.out.println("更新用户数据");
}

@Override
public void delete() {
System.out.println("删除用户数据");
}
}

/**
* 代理类 - 控制对真实主题的访问
* 必须有目标类的引用
*/
public class UserDaoProxy implements IUserDao {
private IUserDao target;

public UserDaoProxy(IUserDao target) {
this.target = target;
}

@Override
public void save() {
System.out.println("开启事务");
try {
target.save();
System.out.println("提交事务");
} catch (Exception e) {
System.out.println("回滚事务");
throw e;
}
}

@Override
public void update() {
System.out.println("开启事务");
try {
target.update();
System.out.println("提交事务");
} catch (Exception e) {
System.out.println("回滚事务");
throw e;
}
}

@Override
public void delete() {
System.out.println("开启事务");
try {
target.delete();
System.out.println("提交事务");
} catch (Exception e) {
System.out.println("回滚事务");
throw e;
}
}
}

/**
* 测试静态代理
*/
@Test
public void testStaticProxy() {
// 创建目标对象
IUserDao target = new UserDaoImpl();

// 创建代理对象
IUserDao proxy = new UserDaoProxy(target);

// 通过代理对象调用方法
proxy.save();
proxy.update();
proxy.delete();
}

3.2 JDK 动态代理

JDK 动态代理基于 Java 反射机制实现,通过 java.lang.reflect.Proxy 类在运行时动态生成代理类。它要求目标对象必须实现接口,代理对象会实现相同的接口,并将方法调用委托给 InvocationHandler 处理。

核心类说明

java.lang.reflect.Proxy:主要方法为:

static Object newProxyInstance(
ClassLoader loader, // 目标对象使用的类加载器
Class<?>[] interfaces, // 目标对象实现的接口类型
InvocationHandler h // 事件处理器
)
// 返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。

java.lang.reflect.InvocationHandler:主要方法为:

Object invoke(Object proxy, Method method, Object[] args)
// 在代理实例上处理方法调用并返回结果。

实现示例

package com.e6yun.project.structural.proxy.example2;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* 代理工厂 - 动态生成代理对象
*/
public class ProxyFactory {
// 维护一个目标对象
private Object target;

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

// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类使用的类加载器
target.getClass().getInterfaces(), // 目标对象实现的接口类型
new InvocationHandler() {
/**
* proxy 代理对象
* method 对应于代理对象上调用的接口方法实例
* args 代理对象调用接口方法时传递的实际参数
* @return 返回目标对象的方法的返回值,没有返回值返回 null 就可以了
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开启事务");
try {
// 执行目标对象的方法
Object result = method.invoke(target, args);
System.out.println("提交事务");
return result;
} catch (Exception e) {
System.out.println("回滚事务");
throw e;
}
}
} // 事件处理器
);
}
}
package com.e6yun.project.structural.proxy.example2;

import com.e6yun.project.structural.proxy.example1.IUserDao;
import com.e6yun.project.structural.proxy.example1.UserDaoImpl;
import org.junit.Test;

public class TestProxy {
@Test
public void testDynamicProxy() {
// 创建目标对象
IUserDao userDao = new UserDaoImpl();
System.out.println("目标对象类型: " + userDao.getClass());

// 创建代理工厂
ProxyFactory proxyFactory = new ProxyFactory(userDao);

// 获取代理对象
IUserDao proxyInstance = (IUserDao) proxyFactory.getProxyInstance();
System.out.println("代理对象类型: " + proxyInstance.getClass());

// 通过代理对象调用方法
proxyInstance.save();
proxyInstance.update();
proxyInstance.delete();
}
}

3.3 CGLIB 动态代理

CGLIB(Code Generation Library)是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。CGLIB 为没有实现接口的类提供代理,为 JDK 的动态代理提供了很好的补充。

工作原理

  • 最底层是字节码
  • ASM 是操作字节码的工具
  • CGLIB 基于 ASM 字节码工具操作字节码(即动态生成代理,对方法进行增强)
  • Spring AOP 基于 CGLIB 进行封装,实现 CGLIB 方式的动态代理
  • 通过继承目标对象,实现方法重写,实现目标对象功能的扩展

依赖配置

使用 CGLIB 需要引入依赖,如果已经有了 spring-core 的 jar 包,则无需引入:

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

实现示例

package com.e6yun.project.structural.proxy.example3;

/**
* 用户实体类
*/
public class User {
private String name;
private Integer age;

public User() {}

public User(String name, Integer age) {
this.name = name;
this.age = age;
}

// getter 和 setter 方法
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }

@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
package com.e6yun.project.structural.proxy.example3;

import java.util.ArrayList;
import java.util.List;

/**
* 目标类 - 没有实现接口
*/
public class UserServiceImpl {
public List<User> findUserList() {
List<User> users = new ArrayList<>();
users.add(new User("张三", 22));
users.add(new User("李四", 21));
return users;
}

public void addUser(User user) {
System.out.println("添加用户: " + user);
}
}
package com.e6yun.project.structural.proxy.example3;

import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
* CGLIB 动态代理实现
*/
public class UserLogProxy implements MethodInterceptor {

/**
* 生成 CGLIB 动态代理类方法
* @param target 需要被代理的目标类
* @return 代理类对象
*/
public Object getLogProxy(Object target) {
// 增强器类,用来创建动态代理类
Enhancer enhancer = new Enhancer();

// 设置父类(目标类)的字节码对象
enhancer.setSuperclass(target.getClass());

// 设置回调
enhancer.setCallback(this);

// 创建动态代理对象并返回
return enhancer.create();
}

/**
* 实现回调的方法
* @param o 代理对象
* @param method 目标对象中的方法的 Method 实例
* @param args 实际参数
* @param methodProxy 代理类对象中的方法的 method 实例
* @return 方法执行结果
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Calendar instance = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

System.out.println(sdf.format(instance.getTime()) + " [" + method.getName() + "] 开始执行");

// 执行目标方法
Object result = methodProxy.invokeSuper(o, args);

System.out.println(sdf.format(instance.getTime()) + " [" + method.getName() + "] 执行完成");

return result;
}
}
@Test
public void testCglibProxy() {
// 创建目标对象
UserServiceImpl userService = new UserServiceImpl();
System.out.println("目标对象类型: " + userService.getClass());

// 创建代理对象
UserServiceImpl logProxy = (UserServiceImpl) new UserLogProxy().getLogProxy(userService);
System.out.println("代理对象类型: " + logProxy.getClass());

// 通过代理对象调用方法
List<User> userList = logProxy.findUserList();
System.out.println("查询结果: " + userList);

logProxy.addUser(new User("王五", 25));
}

4. 动态代理原理深入

4.1 JDK 动态代理类生成原理

Java 虚拟机加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下 3 件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口。

由于虚拟机规范对这三点要求并不具体,所以实际的实现是非常灵活的,关于第一点,**获取类的二进制字节流(class 字节码)**就有很多途径:

  • 从本地获取
  • 从网络中获取
  • 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流。

所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到 JVM 中使用。

4.2 代理类的调用过程

这里可以借助 Alibaba 的线上监控诊断产品 Arthas,对动态生成的代理类代码进行查看。

5. 三种代理方式对比

代理方式性能适用场景限制优点缺点
静态代理代理类少,接口稳定需要手动创建代理类实现简单,性能好代码冗余,维护困难
JDK动态代理有接口目标类必须实现接口自动生成代理类只能代理接口方法
CGLIB动态代理无接口不能代理final类可以代理类方法不能代理final方法

6. 实际应用场景

6.1 Spring AOP 中的代理

Spring AOP 就是基于代理模式实现的,通过动态代理在方法调用前后织入切面逻辑。

@Aspect
@Component
public class LogAspect {
@Around("@annotation(com.example.Log)")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("方法执行前");
Object result = point.proceed();
System.out.println("方法执行后");
return result;
}
}

6.2 MyBatis 中的代理

MyBatis 的 Mapper 接口就是通过动态代理实现的,代理对象将方法调用转换为 SQL 执行。

@Mapper
public interface UserMapper {
List<User> selectAll();
}

6.3 数据库连接池

数据库连接池使用代理模式来管理连接的生命周期。

6.4 远程代理

RPC 框架中的远程代理,隐藏网络通信细节。

6.5 缓存代理

缓存代理可以缓存目标对象的结果,避免重复计算。

public class CacheProxy implements IUserDao {
private IUserDao target;
private Map<String, Object> cache = new HashMap<>();

public CacheProxy(IUserDao target) {
this.target = target;
}

@Override
public User findById(String id) {
String key = "findById_" + id;
if (cache.containsKey(key)) {
System.out.println("从缓存获取数据");
return (User) cache.get(key);
}

User user = target.findById(id);
cache.put(key, user);
return user;
}
}

6.6 虚拟代理(延迟加载)

虚拟代理可以延迟创建开销大的对象。

public class ImageProxy implements Image {
private String filename;
private RealImage realImage;

public ImageProxy(String filename) {
this.filename = filename;
}

@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}

6.7 保护代理

保护代理可以控制对敏感对象的访问。

public class ProtectionProxy implements SensitiveOperation {
private SensitiveOperation target;
private String userRole;

public ProtectionProxy(SensitiveOperation target, String userRole) {
this.target = target;
this.userRole = userRole;
}

@Override
public void sensitiveMethod() {
if ("admin".equals(userRole)) {
target.sensitiveMethod();
} else {
throw new SecurityException("权限不足");
}
}
}

7. 优缺点分析

7.1 优点

  • 控制访问:代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
  • 功能增强:代理对象可以扩展目标对象的功能
  • 解耦合:代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度
  • 透明性:客户端无需知道代理的存在,可以像使用目标对象一样使用代理对象

7.2 缺点

  • 增加复杂度:增加了系统的复杂度
  • 性能开销:动态代理会带来一定的性能开销
  • 调试困难:代理对象的存在可能使调试变得困难

8. 常见问题解答

Q: 什么时候使用代理模式?

A: 当需要控制对对象的访问,或在访问前后添加额外处理时,如权限控制、日志记录、性能监控等。

Q: 代理模式和装饰器模式的区别?

A:

  • 代理模式:控制访问,关注的是访问控制
  • 装饰器模式:增强功能,关注的是功能扩展

Q: 如何选择代理方式?

A:

  • 有接口:优先使用 JDK 动态代理
  • 无接口:使用 CGLIB 动态代理
  • 简单场景:可以考虑静态代理

Q: 代理模式在 Spring 中的应用?

A: Spring AOP 就是基于代理模式实现的,通过动态代理在方法调用前后织入切面逻辑。

9. 最佳实践

  1. 合理选择代理方式:根据具体场景选择合适的代理实现
  2. 避免过度使用:不要为了使用代理而使用代理
  3. 性能考虑:在性能敏感的场景中,考虑使用静态代理
  4. 调试友好:在开发阶段,可以通过配置禁用代理来简化调试

10. 总结

代理模式是一种非常重要的设计模式,它在很多框架和库中都有广泛应用。理解代理模式不仅有助于我们更好地使用这些框架,也能帮助我们在自己的项目中做出更好的设计决策。

通过代理模式,我们可以:

  • 在不修改原有代码的情况下增强功能
  • 控制对对象的访问
  • 实现横切关注点的分离
  • 提高代码的可维护性和可扩展性

11. 学习路径建议

11.1 入门阶段

  1. 理解基本概念:什么是代理模式,为什么需要代理
  2. 掌握静态代理:理解代理的基本原理
  3. 实践简单场景:如日志记录、权限控制

11.2 进阶阶段

  1. 学习动态代理:JDK动态代理和CGLIB动态代理
  2. 理解原理:代理类的生成机制和调用过程
  3. 性能优化:选择合适的代理方式

11.3 高级阶段

  1. 框架应用:Spring AOP、MyBatis等框架中的代理应用
  2. 源码分析:深入理解代理模式的实现细节
  3. 最佳实践:在实际项目中合理使用代理模式

12. 实践建议

12.1 选择代理方式的决策树

是否需要代理?
├─ 是
│ ├─ 目标对象是否实现接口?
│ │ ├─ 是 → 使用JDK动态代理
│ │ └─ 否 → 使用CGLIB动态代理
│ └─ 代理类是否很少且稳定?
│ ├─ 是 → 考虑静态代理
│ └─ 否 → 使用动态代理
└─ 否 → 不需要代理

12.2 性能考虑

  • 高频调用:优先考虑静态代理
  • 动态扩展:使用动态代理
  • 内存敏感:避免过度使用CGLIB代理

12.3 调试建议

  • 在开发阶段,可以通过配置禁用代理来简化调试
  • 使用Arthas等工具查看生成的代理类
  • 在代理方法中添加详细的日志记录

13. 扩展阅读

13.1 相关设计模式

  • 装饰器模式:与代理模式相似,但关注点不同
  • 适配器模式:解决接口不兼容问题
  • 外观模式:为复杂子系统提供简单接口

13.2 相关技术

  • AOP(面向切面编程):基于代理模式实现
  • RPC(远程过程调用):使用代理模式隐藏网络细节
  • ORM框架:使用代理模式实现延迟加载

13.3 推荐资源

  • 《设计模式:可复用面向对象软件的基础》
  • Spring Framework 官方文档
  • MyBatis 官方文档

通过系统学习代理模式,您将能够:

  1. 更好地理解和使用Spring、MyBatis等主流框架
  2. 在实际项目中做出更好的架构设计决策
  3. 提高代码的可维护性和可扩展性
  4. 为学习其他设计模式打下坚实基础