代理设计模式:程序员的"替身文学"终极指南
什么是代理设计模式?
代理模式就像娱乐圈的职业背锅侠!当国际巨星(真实对象)不想亲自处理狗仔队、粉丝接机和奇葩甲方时,就会雇佣一个经纪人(代理对象)来当"人形盾牌"。
🌰 举个有味道的栗子 🌰
真实场景还原:
-
广告商来电:"我要找你拍马桶广告!"
👉 经纪人:"先打钱,我再看看档期!"(访问控制:过滤不靠谱需求) -
粉丝夜袭:"我要给哥哥送活体章鱼!"
👉 经纪人:"哥在火星拍戏,地址给你寄到月球吧!"(保护机制:物理隔离危险) -
本尊摆烂:"今天我只想躺平吃螺蛳粉..."
👉 经纪人:"收到!官宣哥哥在筹备元宇宙演唱会!"(延迟加载:实际歌单还没开始写)
💡 模式精要 💡
代理模式 = 本体功能 + 增强Buff
(突然经纪人破门而入:"你说谁是工具人?下个月演出费分我20%!")
代理架构解剖课
核心三要素
public interface Star {
void perform();
}
// 原始类,目标类
public class RealStar implements Star {
public void perform() {
System.out.println("本尊开唱!");
}
}
// 代理类
public class StarProxy implements Star {
private RealStar star;
public StarProxy(RealStar star) {
this.star = star;
}
public void perform() {
System.out.println("收演出费 500w");
checkSecurity();
// 原始方法
star.perform();
sendFlower();
}
private void checkSecurity() {
System.out.println("10个保镖开路");
}
private void sendFlower() {
System.out.println("安排粉丝献花");
}
}
classDiagram
class Star {
+perform(): void
}
class RealStar {
+perform(): void
}
class StarProxy {
-star: RealStar
+perform(): void
-checkSecurity(): void
-sendFlower(): void
}
Star <|-- RealStar
Star <|-- StarProxy
StarProxy o-- RealStar : "代理"
静态代理:手工打造替身
代码健身房
public class User {
private String name;
private String password;
//...
}
public interface UserService {
void register(User user);
boolean login(String name, String password);
}
public class UserServiceImpl implements UserService {
private Map<String, User> userMap = new HashMap<String, User>();
@Override
public void register(User user) {
System.out.println("正在执行用户注册...");
userMap.put(user.getName(), user);
System.out.println("用户注册成功: " + user.getName());
}
@Override
public boolean login(String name, String password) {
System.out.println("正在执行用户登录...");
User user = userMap.get(name);
if (user != null && user.getPassword().equals(password)) {
System.out.println("用户登录成功: " + name);
return true;
}
System.out.println("用户登录失败: " + name);
return false;
}
}
public class UserServiceProxy implements UserService{
private UserServiceImpl userServiceImpl;
public UserServiceProxy(UserServiceImpl userService) {
this.userServiceImpl = userService;
}
@Override
public void register(User user) {
long start = System.currentTimeMillis();
// 注册前的处理
System.out.println("代理开始处理注册请求...");
validateUserData(user);
// 调用真实对象的方法
userServiceImpl.register(user);
// 注册后的处理
System.out.println("代理结束处理注册请求...");
System.out.println("注册耗时: " + (System.currentTimeMillis() - start) + "ms");
}
@Override
public boolean login(String name, String password) {
long start = System.currentTimeMillis();
// 登录前的处理
System.out.println("代理开始处理登录请求...");
// 调用真实对象的方法
boolean result = userServiceImpl.login(name, password);
// 登录后的处理
System.out.println("代理结束处理登录请求...");
System.out.println("登录耗时: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
private void validateUserData(User user) {
// ...
}
}
优劣分析表
| 👍 优点 | 👎 缺点 |
|---|---|
| 职责清晰(本体专注业务) | 代码量指数级增长 |
| 扩展方便(新增功能不修改本体) | 接口变更时代理类需要同步修改 |
| 客户端无感知 | 容易形成类爆炸 |
动态代理:替身生成器
JDK动态代理:接口特工队
// 接口
interface UserService {
void addUser(String name);
void deleteUser(String name);
}
// 实现类
class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户: " + name);
}
@Override
public void deleteUser(String name) {
System.out.println("删除用户: " + name);
}
}
// JDK动态代理处理器
class JDKInvocationHandler implements InvocationHandler {
private Object target; // 被代理的目标对象
public JDKInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("======方法执行前======");
System.out.println("方法名: " + method.getName());
// 调用目标对象的方法
Object result = method.invoke(target, args);
System.out.println("======方法执行后======");
return result;
}
}
// 测试类
public class JDKProxyTest {
public static void main(String[] args) {
// 测试JDK动态代理
UserService userService = new UserServiceImpl();
InvocationHandler handler = new JDKInvocationHandler(userService);
UserService proxyService = (UserService) Proxy.newProxyInstance(
userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
handler
);
proxyService.addUser("张三");
}
}
方法 Proxy.newProxyInstance 有三个参数,分别解释如下:
userService.getClass().getClassLoader()
- 类型:
ClassLoader - 该参数指定了代理类的类加载器。类加载器用于加载代理类到 JVM 中。通常可以使用原始对象的类加载器(
userService.getClass().getClassLoader()),因为代理类需要和原始对象使用同样的类加载器来保证它能够正确加载接口。 userService.getClass().getClassLoader()获取了userService对象的类加载器,用于加载代理对象。
userService.getClass().getInterfaces()
- 类型:
Class<?>[] - 该参数指定了代理对象需要实现的接口。这里
userService.getClass().getInterfaces()获取userService对象的所有接口,并将它们传给代理类。这样,生成的代理对象就会实现userService的所有接口。 - 例如,如果
userService实现了UserService接口,那么生成的代理对象也会实现这个接口。
handler
- 类型:
InvocationHandler - 这个参数是一个处理器,用于定义代理对象方法调用的具体逻辑。
InvocationHandler是一个接口,它的invoke方法会在代理对象的方法被调用时触发。可以在invoke方法中添加一些额外的逻辑,例如日志记录、性能监控、权限控制等。handler是一个实现了InvocationHandler接口的对象,它会在代理对象的方法被调用时执行。
通过 Proxy.newProxyInstance,你能创建一个代理对象,该对象会实现 userService 接口,并且所有方法调用会通过 handler.invoke 来委托。这个机制通常用在需要动态改变行为的场景,比如 AOP(面向切面编程)、日志记录、事务控制等。
例如,如果 UserService 有一个 getUserInfo() 方法,代理对象的 getUserInfo() 方法会被调用,实际的逻辑会被委托给 handler.invoke,在其中可以先执行一些额外的操作,然后再调用真实的 userService.getUserInfo() 方法。
CGLib动态代理:继承魔法师
// 目标类(无需实现接口)
public class UserService {
public void addUser(String name) {
System.out.println("添加用户: " + name);
}
public void deleteUser(String name) {
System.out.println("删除用户: " + name);
}
}
// CGLIB方法拦截器
class CglibMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
System.out.println("======方法执行前======");
System.out.println("方法名: " + method.getName());
// 调用目标对象的方法
Object result = proxy.invokeSuper(obj, args);
System.out.println("======方法执行后======");
return result;
}
}
// 测试类
public class CglibProxyTest {
public static void main(String[] args) {
// 测试CGLIB动态代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperClass(UserService.class);
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new CglibMethodInterceptor());
UserService proxyService2 = (UserService) enhancer.create();
proxyService2.addUser("李四");
}
}
动态代理对比表
| 特性 | JDK动态代理 | CGLib动态代理 |
|---|---|---|
| 代理方式 | 基于接口 | 基于继承 |
| 性能 | 调用快,生成慢 | 生成快,调用稍慢 |
| 依赖 | 内置JDK | 需要第三方库 |
| 目标类要求 | 必须实现接口 | 可以是普通类 |
| 典型应用场景 | Spring AOP默认 | @Configuration类 |
Spring中的代理魔法
AOP配置示例
@Configuration
@EnableAspectJAutoProxy
public class ProxyConfig {
@Bean
public UserService userService() {
return new UserServiceImpl();
}
@Bean
public PerformanceAspect performanceAspect() {
return new PerformanceAspect();
}
}
@Aspect
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logPerformance(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("🏁 开始执行: " + pjp.getSignature());
Object result = pjp.proceed();
System.out.println("🏁 执行完成 耗时: " +
(System.currentTimeMillis() - start) + "ms");
return result;
}
}
MethodBeforeAdvice详解
- 接口定义与继承关系:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}
- 它继承自BeforeAdvice接口,BeforeAdvice是一个标记接口(没有定义任何方法的接口)
- BeforeAdvice用于标识这是一个前置通知
- before方法参数详解:
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
Method method:增强功能所给的那个原始方法。- 可以获取方法名:
method.getName() - 可以获取方法返回类型:
method.getReturnType() - 可以获取方法参数类型:
method.getParameterTypes() - 可以获取方法注解:
method.getAnnotations()
- 可以获取方法名:
Object[] args:原始方法的参数数组- 包含了被代理方法的所有参数值
- 可以在执行方法前修改参数值
- 如果方法没有参数,则为空数组
- 可以通过索引访问具体参数:
args[0],args[1]等
@Nullable Object target:被代理的目标对象- 可能为null,所以加了@Nullable注解
- 表示实现这个接口的目标类实例
- 可以获取目标类信息:
target.getClass()
public interface UserService {
void addUser(String name);
}
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户: " + name);
}
}
public class LogBeforeAdvice implements MethodBeforeAdvice {
/**
* 在方法执行前的拦截器方法
* 用于打印方法执行前的相关信息,包括方法名称、方法参数和目标对象的类名
*
* @param method 被拦截的方法对象,用于获取方法名称等信息
* @param args 方法的参数数组,用于展示方法的输入参数
* @param target 目标对象,即方法所属的实例对象,用于获取对象的类名
* @throws Throwable 如果在执行过程中出现异常,则抛出此异常
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("====== 方法执行前 ======");
System.out.println("方法名称: " + method.getName());
System.out.println("方法参数: " + Arrays.toString(args));
System.out.println("目标对象: " + target.getClass().getName());
}
}
@Configuration
public class AopConfig {
@Bean
public UserService userService() {
return new UserServiceImpl();
}
/**
* 创建UserService的代理Bean
* 此方法旨在通过ProxyFactoryBean生成一个UserService代理实例,该实例在执行方法前添加日志记录功能
*
* @return ProxyFactoryBean 实例,配置了目标对象和前置日志切面
*/
@Bean
public ProxyFactoryBean userServiceProxy() {
// 实例化ProxyFactoryBean
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
// 设置目标对象为userService
proxyFactoryBean.setTarget(userService());
// 添加前置通知,用于在方法调用前进行日志记录
proxyFactoryBean.addAdvice(new LogBeforeAdvice());
return proxyFactoryBean;
}
}
这种方式将增强功能单独分离开,大大增加了增强功能的维护性,创建其他目标类的代理对象时,只需指定目标对象即可。例如:
// 1. 性能监控的前置通知
public class PerformanceBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
// 记录方法执行开始时间
long startTime = System.currentTimeMillis();
// 将开始时间存储在ThreadLocal中,以便后续统计
ThreadLocal<Long> timeThreadLocal = new ThreadLocal<>();
timeThreadLocal.set(startTime);
System.out.println("开始执行方法: " + method.getName());
System.out.println("开始时间: " + new Date(startTime));
}
}
// 2. Spring配置类 - 可以灵活切换不同的通知
@Configuration
public class AopConfig {
@Bean
public UserService userService() {
return new UserServiceImpl();
}
// 使用性能监控通知
@Bean
public ProxyFactoryBean performanceProxy() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(userService());
proxyFactoryBean.addAdvice(new PerformanceBeforeAdvice());
return proxyFactoryBean;
}
}
根据以上内容,我们可以总结出Spring动态代理开发的四个步骤
- 目标对象
- 增强功能
- 切入点
- 组装
MethodInterceptor详解
public class UserServiceImpl implements UserService {
@Override
public String getUser(String name) {
System.out.println("查询用户: " + name);
return "User: " + name;
}
@Override
public void addUser(String name) {
System.out.println("添加用户: " + name);
}
}
public class MethodInterceptorDemo implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取方法名
String methodName = invocation.getMethod().getName();
// 获取参数
Object[] args = invocation.getArguments();
try {
// 方法执行前的处理
System.out.println("====== 方法执行前 ======");
System.out.println("方法名称: " + methodName);
System.out.println("方法参数: " + Arrays.toString(args));
// 记录开始时间
long startTime = System.currentTimeMillis();
// 执行目标方法
Object result = invocation.proceed();
// 方法执行后的处理
System.out.println("====== 方法执行后 ======");
System.out.println("返回值: " + result);
System.out.println("执行时间: " + (System.currentTimeMillis() - startTime) + "ms");
return result;
} catch (Exception e) {
// 异常处理
System.out.println("方法执行异常: " + e.getMessage());
throw e;
}
}
}
@Configuration
public class AopConfig {
@Bean
public UserService userService() {
return new UserServiceImpl();
}
@Bean
public ProxyFactoryBean userServiceProxy() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
// 设置目标对象
proxyFactoryBean.setTarget(userService());
// 设置拦截器
proxyFactoryBean.addAdvice(new MethodInterceptorDemo());
return proxyFactoryBean;
}
}
public class InterceptorTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AopConfig.class);
UserService userService = context.getBean("userServiceProxy", UserService.class);
// 测试方法调用
userService.addUser("张三");
System.out.println("------------------------");
String user = userService.getUser("李四");
context.close();
}
}
MethodInterceptor可以影响原始方法的返回值,invoke方法的返回值不直接返回即可。
前面我们基于MethodBeforeAdvice实现前置通知,MethodInterceptor实现环绕通知,Spring中还可以基于AfterReturningAdvice实现后置通知,ThrowsAdvice实现异常通知。
代理模式应用场景
代理模式(Proxy Pattern)是一种结构性设计模式,它通过为其他对象提供代理(或占位符),使得我们可以控制对该对象的访问。它的核心思想是通过代理对象来间接操作原始对象,从而使得我们可以增强或延迟原始对象的行为。代理模式广泛应用于各种场景,特别是在分布式系统、缓存管理、权限控制等领域。
1. 业务系统的非功能性需求开发
代理模式在处理系统的非功能性需求时,尤其在关注点分离上非常有效。常见的非功能性需求包括监控、统计、鉴权、限流、事务、幂等、日志等。代理模式能够帮助我们将这些非功能性需求与业务逻辑解耦。
应用场景示例:
- 日志记录:对于每个请求,系统可能需要记录操作日志。通过代理模式,我们可以在代理类中实现日志记录功能,业务逻辑类只需要专注于核心业务操作,而不必涉及日志管理。
- 监控和统计:可以使用代理类对请求的性能进行监控或统计。在代理类中记录请求的执行时间,成功或失败的次数等信息。
- 鉴权和限流:在系统中某些操作可能需要权限验证,代理类可以在执行原始方法之前,进行权限检查;同样,代理类可以控制请求的频率,限制流量等。
例如,Spring AOP(面向切面编程)就是基于代理模式实现的。通过动态代理技术(例如 JDK 动态代理或 CGLIB),Spring 可以在不修改目标类代码的情况下,动态地为方法添加横切关注点,如事务管理、缓存、权限控制等。
2. 代理模式在 RPC、缓存中的应用
2.1 RPC 框架中的应用
RPC(Remote Procedure Call)框架可以看作是一种远程代理模式。客户端通过代理对象调用远程服务器上的方法,而无需了解底层的网络通信、数据编解码、异常处理等复杂细节。代理对象隐藏了这些实现细节,使得远程调用看起来就像本地调用一样。
示例:
在一个分布式系统中,我们可能有如下的 RPC 代理类:
public class RpcProxy {
public static <T> T create(Class<T> serviceClass) {
// 创建代理对象,底层通过网络请求调用远程服务
return (T) Proxy.newProxyInstance(serviceClass.getClassLoader(),
new Class<?>[] { serviceClass },
new RpcInvocationHandler(serviceClass));
}
}
在客户端代码中,开发者可以像调用本地方法一样调用远程方法,而不需要关心底层的网络通信细节。
2.2 缓存的应用
缓存是提高性能的常见手段之一。代理模式可以在应用程序中实现缓存代理,从而减少不必要的计算和数据库查询。
示例:
假设我们有一个获取用户信息的接口,如果用户信息已经缓存,就直接从缓存中返回结果,否则从数据库查询,并将结果存入缓存。通过代理模式,我们可以在代理类中处理缓存逻辑:
public class CacheProxy implements UserService {
private final UserService userService;
private final Cache cache;
public CacheProxy(UserService userService, Cache cache) {
this.userService = userService;
this.cache = cache;
}
@Override
public User getUserById(String userId) {
User cachedUser = cache.get(userId);
if (cachedUser != null) {
return cachedUser; // 从缓存中获取数据
}
User user = userService.getUserById(userId); // 调用原始服务
cache.put(userId, user); // 将结果放入缓存
return user;
}
}
在这个例子中,CacheProxy 代理了 UserService 的实现,它在执行 getUserById 时,首先检查缓存,如果缓存中有数据就返回,如果没有则调用实际的 userService 获取数据并缓存。
3. 代理模式的其他应用场景
3.1 懒加载
懒加载是指只有在需要时才加载资源,而不是在一开始就加载。这通常用于资源消耗较大的操作(例如加载大型文件或连接数据库)。代理模式可以帮助我们实现懒加载,通过代理类来延迟加载实际资源。
3.2 虚拟代理
虚拟代理常常用于资源的按需加载,例如视频、图像等媒体资源。在资源加载之前,代理类可以创建一个空的占位符对象,等到资源真正需要时才进行加载。
public class ImageProxy implements Image {
private final 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();
}
}
在这个例子中,ImageProxy 延迟加载 RealImage,只有当 display() 方法被调用时,才真正加载图像资源。
总结
代理模式的应用非常广泛,尤其是在解决系统的横向需求时,它可以使得我们将各种跨切关注点(如权限验证、缓存、日志、监控等)从业务逻辑中剥离出来,保持代码的清晰和可维护性。同时,代理模式也能为我们提供灵活的资源管理策略,比如懒加载和虚拟代理,优化系统性能和资源的使用。在实践中,尤其是基于框架(如 Spring)进行开发时,代理模式的运用是极其普遍的,能够帮助开发者高效地实现复杂的功能需求。
性能优化小贴士
-
代理选择策略:
- 优先使用JDK动态代理(接口场景)
- 需要代理类时再用CGLib
- Spring Boot 2.x后默认使用CGLib
-
缓存代理实例:
public class ProxyFactory { private static Map<Class<?>, Object> proxyCache = new ConcurrentHashMap<>(); public static <T> T getProxy(Class<T> clazz) { return (T) proxyCache.computeIfAbsent(clazz, k -> new CglibProxy().createProxy(clazz)); } } -
避免过度代理:
- 使用@Configuration(proxyBeanMethods = false)
- 合理设置@Scope
常见面试灵魂拷问
Q1:动态代理在Spring中是如何应用的?
A:Spring AOP的核心实现方式,通过JDK动态代理(接口)和CGLib(类)生成代理对象,结合Pointcut和Advice实现切面编程。
Q2:什么时候会导致代理失效?
A:同类内部方法调用(绕过代理)、final方法、静态方法、私有方法等场景。
Q3:如何选择动态代理方式?
A:目标类实现接口优先用JDK代理,否则用CGLib。可通过@EnableAspectJAutoProxy(proxyTargetClass=true)强制使用CGLib。
最后友情提醒:使用代理模式就像找经纪人——既要保证TA能帮你挡子弹,也要小心别被TA架空成了傀儡皇帝!👑