代理模式
代理模式是一种结构型设计模式,其核心思想是通过代理对象
作为中介,控制对目标对象
的访问,就像房产中介替房东处理租房事务一样。代理对象与目标对象实现相同接口(或继承相同类),使客户端可以像调用目标对象一样调用代理对象,同时代理对象能在调用前后添加额外逻辑,比如日志记录、权限校验或缓存处理等。根据代理类生成时机的不同,可分为静态代理(编译时手动编写代理类,适合简单场景)和动态代理(运行时通过反射或字节码技术自动生成代理类,如 Java 的 JDK 代理、CGLIB 代理,常用于 AOP、RPC 框架等需要灵活扩展的场景)。这种模式既能在不修改目标对象的前提下增强功能,又能实现客户端与目标对象的解耦,是框架设计中常用的基础技术。
静态代理
什么是静态代理?
静态代理是一种设计模式,属于代理模式的一种实现方式。它允许我们通过创建一个代理对象来控制对另一个对象(目标对象)的访问。代理对象在客户端和目标对象之间起到中介作用,客户端实际上是与代理对象进行交互,而不是直接访问目标对象。静态代理适用于接口方法较少、代理逻辑简单的场景。
静态代理的核心特点:
- 接口一致性:代理对象和目标对象需要实现相同的接口,这样客户端可以像使用目标对象一样使用代理对象。
- 编译时确定:代理类在编译时就已经确定,不像动态代理那样在运行时生成。
- 功能增强:代理对象可以在调用目标对象的方法前后添加额外的功能,如日志记录、权限验证、事务管理等。
静态代理的优缺点
优点 | 缺点 |
---|---|
实现简单,易于理解和维护。 | 代理类和目标类需要实现相同的接口,导致代码冗余。 |
可以在不修改目标对象的情况下增强其功能。 | 如果接口方法发生变化,代理类和目标类都需要同步修改。 |
符合开闭原则(对扩展开放,对修改关闭)。 | 对于多个目标类,需为每个类单独创建代理类,导致类数量膨胀。 |
案例代码:
//定义了用户服务的接口,包含创建和删除用户的方法。
public interface UserService {
String createUser(String username);
void deleteUser(int id);
}
//实现了 UserService 接口,提供实际的用户管理功能。
public class UserServiceImpl implements UserService{
@Override
public String createUser(String username) {
System.out.println("Creating user: " + username);
return "User " + username + " created with ID: " + (System.currentTimeMillis() % 1000);
}
@Override
public void deleteUser(int id) {
System.out.println("Deleting user with ID: " + id);
}
}
//代理类也实现了 UserService 接口,持有目标对象的引用。在调用目标方法前后添加了日志记录功能。
public class UserServiceProxy implements UserService{
private UserService target;
public UserServiceProxy(UserService userService) {
this.target = userService;
}
@Override
public String createUser(String username) {
System.out.println("静态代理创建用户开始...");
String result = target.createUser(username);
System.out.println("静态代理创建用户结束...");
return result;
}
@Override
public void deleteUser(int id) {
System.out.println("静态代理删除用户开始...");
target.deleteUser(id);
System.out.println("静态代理删除用户结束...");
}
}
动态代理
什么是动态代理?
动态代理是一种在程序运行时动态生成代理类的技术,无需在编译期手动编写代理类。它通过反射机制拦截目标对象的方法调用,在不修改目标类代码的前提下,动态添加通用逻辑(如日志记录、权限校验、事务管理等)。其核心思想是将业务逻辑与增强逻辑分离,实现代码的松耦合和复用。动态代理常见的应用场景有AOP、RPC框架、缓存代理等。
动态代理的优缺点
优点 | 缺点 |
---|---|
1. 无需手动编写代理类,减少代码冗余。 | 1. 基于反射,存在一定性能损耗(JVM 优化后影响较小)。 |
2. 通用逻辑集中管理,扩展性强(如新增切面只需修改处理器)。 | 2. Java 动态代理只能代理接口,无法直接代理类(需配合 CGLIB 等字节码工具)。 |
3. 支持运行时动态切换目标对象,灵活性高。 |
基于接口的动态代理
基于接口的动态代理是利用JDK原生的动态代理机制实现方法调用拦截和增强,是一种常用的动态代理方式。主要利用了Proxy类和InvocationHandler接口进行实现。
下面是一个案例:
首先跟前面提到的静态代理类似,需要有一个接口以及接口的实现类
//定义了用户服务的接口,包含创建和删除用户的方法。
public interface UserService {
String createUser(String username);
void deleteUser(int id);
}
//实现了 UserService 接口,提供实际的用户管理功能。
public class UserServiceImpl implements UserService{
@Override
public String createUser(String username) {
System.out.println("Creating user: " + username);
return "User " + username + " created with ID: " + (System.currentTimeMillis() % 1000);
}
@Override
public void deleteUser(int id) {
System.out.println("Deleting user with ID: " + id);
}
}
新建代理,实现InvocationHandler接口
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 跳过Object类的方法(toString/hashCode/equals等)
if (method.getDeclaringClass() == Object.class) {
return method.invoke(target, args); // 直接调用
}
// 方法调用前的增强逻辑
long startTime = System.currentTimeMillis();
System.out.println("[" + LocalDateTime.now() + "] 开始调用方法: " + method.getName());
if (args != null) {
System.out.print("参数: ");
for (Object arg : args) {
System.out.print(arg + " ");
}
System.out.println();
}
// 调用目标对象的方法
Object result = method.invoke(target, args);
// 方法调用后的增强逻辑
long endTime = System.currentTimeMillis();
System.out.println("[" + LocalDateTime.now() + "] 方法 " + method.getName() + " 调用完成,耗时: " +
(endTime - startTime) + "ms");
if (result != null) {
System.out.println("返回值: " + result);
}
System.out.println("------------------------");
return result;
}
}
创建代理并进行调用测试
public class DynamicProxyExample {
public static void main(String[] args) {
//目标对象
UserServiceImpl userService = new UserServiceImpl();
//InvocationHandler
MyInvocationHandler invocationHandler = new MyInvocationHandler(userService);
//创建代理对象
UserService proxyInstance = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(), new Class<?>[]{UserService.class}, invocationHandler);
//通过代理对象调用方法
String result = proxyInstance.createUser("Jack");
System.out.println("创建用户结果:" + result);
proxyInstance.deleteUser(1008611);
}
}
输出:
[2025-06-08T20:10:33.807] 开始调用方法: createUser
参数: Jack
Creating user: Jack
[2025-06-08T20:10:33.807] 方法 createUser 调用完成,耗时: 63ms
返回值: User Jack created with ID: 807
------------------------
创建用户结果:User Jack created with ID: 807
[2025-06-08T20:10:36.461] 开始调用方法: deleteUser
参数: 1008611
Deleting user with ID: 1008611
[2025-06-08T20:10:36.462] 方法 deleteUser 调用完成,耗时: 0ms
------------------------
在使用 JDK 动态代理时,有几个关键点:
- 核心类与方法:
Proxy
类是 JDK 动态代理的核心工具,通过其静态方法newProxyInstance()
可以动态生成代理对象。 - 方法调用处理:需要实现
InvocationHandler
接口来定义代理逻辑,该接口中的invoke()
方法会在代理对象的方法被调用时触发。 - 反射机制应用:在
invoke()
方法内部,通常会使用Method.invoke()
反射调用目标对象的实际方法,从而将代理逻辑与业务逻辑解耦。 - 代理局限性:JDK 动态代理要求目标对象必须实现至少一个接口,因为生成的代理对象会自动实现相同的接口集合。如果需要代理没有实现接口的类,可以考虑使用 CGLIB 等其他代理技术。
基于类的动态代理
基于类的动态代理需要借助第三方库或框架实现,Java 里常用的有 CGLIB 和 Byte Buddy。这类技术主要是为了打破 JDK 动态代理只能代理接口的限制 —— 也就是说,就算目标类没实现任何接口,也能通过生成它的子类来做代理。
下面是一个基于CGLIB的案例:
首先是一个原始类,定义两个方法
public class UserService {
public String createUser(String username) {
System.out.println("Creating user: " + username);
return "User " + username + " created with ID: " + (System.currentTimeMillis() % 1000);
}
public void deleteUser(int id) {
System.out.println("Deleting user with ID: " + id);
}
}
然后我们创建类动态代理处理器类,实现CGLIB的接口
public class MyInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 过滤掉Object类的方法
if (method.getDeclaringClass() == Object.class) {
return methodProxy.invokeSuper(o, objects);
}
// 方法调用前的增强逻辑
long startTime = System.currentTimeMillis();
System.out.println("[" + LocalDateTime.now() + "] 开始调用方法: " + method.getName());
if (objects != null) {
System.out.print("参数: ");
for (Object arg : objects) {
System.out.print(arg + " ");
}
System.out.println();
}
// 调用目标方法(注意这里使用MethodProxy.invokeSuper)
Object result = methodProxy.invokeSuper(o, objects);
// 方法调用后的增强逻辑
long endTime = System.currentTimeMillis();
System.out.println("[" + LocalDateTime.now() + "] 方法 " + method.getName() + " 调用完成,耗时: " +
(endTime - startTime) + "ms");
if (result != null) {
System.out.println("返回值: " + result);
}
System.out.println("------------------------");
return result;
}
}
创建原始对象并且生产对应的代理对象
public static void main(String[] args) {
// 创建Enhancer对象,用于生成代理类
Enhancer enhancer = new Enhancer();
// 设置父类(目标类)
enhancer.setSuperclass(UserService.class);
// 设置回调函数
enhancer.setCallback(new MyInterceptor());
// 创建代理对象
UserService proxy = (UserService) enhancer.create();
// 通过代理对象调用方法
String result = proxy.createUser("Bob");
System.out.println("主程序接收到创建用户结果: " + result);
proxy.deleteUser(1008611);
}
输出:
[2025-06-08T20:45:00.493] 开始调用方法: createUser
参数: Bob
Creating user: Bob
[2025-06-08T20:45:00.505] 方法 createUser 调用完成,耗时: 81ms
返回值: User Bob created with ID: 505
------------------------
主程序接收到创建用户结果: User Bob created with ID: 505
[2025-06-08T20:45:00.505] 开始调用方法: deleteUser
参数: 1008611
Deleting user with ID: 1008611
[2025-06-08T20:45:00.505] 方法 deleteUser 调用完成,耗时: 0ms
------------------------
注意:CGLIB 通过继承目标类生成代理子类,因此无法代理final
类或final
方法(Java 不允许继承final
类,final
方法无法被重写)。若目标类为final
,需使用ByteBuddy
等更灵活的字节码工具。
动态代理的核心价值在于:非侵入式地为对象添加横切逻辑(如日志、权限、缓存等)
日常开发中动态代理的典型案例
日志记录与操作审计
场景:记录接口调用的入参、返回值和执行时间,用于问题排查或操作审计。直接在业务代码中插入日志会导致代码冗余,且难以统一维护。
优势:日志逻辑与业务代码解耦,无需修改业务方法即可统一添加日志。
动态代理方案:
- 定义日志切面,通过动态代理在方法调用前后添加日志记录。
- 示例代码(基于 Spring AOP,本质是动态代理):
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
// 记录方法入参、调用时间等
System.out.println("调用方法:" + joinPoint.getSignature().getName());
System.out.println("入参:" + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(value = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(Object result) {
// 记录返回值
System.out.println("返回结果:" + result);
}
}
权限校验与访问控制
场景:对敏感接口(如删除、修改操作)进行权限校验,确保用户有权限执行。在每个业务方法中添加权限判断会导致代码重复,且权限规则变更时修改成本高。
优势:权限逻辑集中管理,业务方法只需添加注解即可实现权限控制,符合开闭原则。
动态代理方案:
- 定义权限注解(如
@NeedPermission
),通过动态代理拦截方法调用,校验用户权限。 - 示例逻辑:
// 权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedPermission {
String value(); // 所需权限标识
}
// 权限代理处理器
public class PermissionInvocationHandler implements InvocationHandler {
private final Object target;
private final UserContext userContext; // 用户上下文,存储当前用户权限
public PermissionInvocationHandler(Object target, UserContext userContext) {
this.target = target;
this.userContext = userContext;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 检查方法是否有权限注解
NeedPermission annotation = method.getAnnotation(NeedPermission.class);
if (annotation != null) {
String requiredPermission = annotation.value();
// 校验权限
if (!userContext.hasPermission(requiredPermission)) {
throw new PermissionDeniedException("无权限执行此操作");
}
}
// 权限通过,调用目标方法
return method.invoke(target, args);
}
}
缓存优化与结果复用
场景:对高频访问、结果不常变更的方法(如获取字典数据)添加缓存,减少数据库查询。手动添加缓存逻辑(查缓存→无则查库→存缓存)会污染业务代码,且缓存策略难以统一调整。
优势:缓存逻辑与业务逻辑分离,可通过配置灵活调整缓存策略(如过期时间、缓存类型)。
动态代理方案:
- 通过动态代理拦截方法调用,先查缓存,若无结果再调用原方法,并将结果存入缓存。
- 核心逻辑示例:
public class CacheInvocationHandler implements InvocationHandler {
private final Object target;
private final CacheService cacheService; // 缓存服务
public CacheInvocationHandler(Object target, CacheService cacheService) {
this.target = target;
this.cacheService = cacheService;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 生成缓存键(基于类名、方法名、参数)
String cacheKey = generateCacheKey(method, args);
// 查缓存
Object result = cacheService.get(cacheKey);
if (result != null) {
return result;
}
// 缓存未命中,调用目标方法
result = method.invoke(target, args);
// 存入缓存(设置过期时间)
cacheService.put(cacheKey, result, 3600); // 缓存1小时
return result;
}
}
远程调用与服务代理(RPC 场景)
场景:微服务架构中,调用远程服务时需要封装网络请求细节(如序列化、连接管理)。手动编写远程调用代码(如 HTTP 请求、Socket 通信)会导致代码繁琐,且难以统一维护。
优势:远程调用对业务代码透明,开发者无需关注网络细节,像调用本地方法一样使用远程服务。
动态代理方案:
- 通过动态代理生成远程服务的本地代理对象,调用代理方法时自动转换为远程请求。
- 典型案例:Feign(Spring Cloud 中基于动态代理实现的 HTTP 客户端):
// 定义远程服务接口
@FeignClient(name = "user-service")
public interface UserService {
@GetMapping("/users/{id}")
User getUserById(@PathVariable Long id);
}
// Feign通过动态代理将接口调用转换为HTTP请求
User user = userService.getUserById(1L); // 实际会发送HTTP请求到user-service
懒加载与资源优化
场景:对象包含大型资源(如文件流、数据库连接),需要在真正使用时才初始化,避免资源浪费。提前初始化可能导致内存占用过高,延迟初始化需要复杂的状态管理。
优势:减少资源占用,提升系统启动速度,尤其适用于大型应用或资源密集型场景。
动态代理方案:
- 初始时创建代理对象,代理对象在方法调用时才真正初始化目标对象。
- 核心逻辑示例:
public class LazyLoadingProxy {
private volatile Object realObject; // 实际对象,延迟初始化
public Object getProxy() {
// 使用动态代理,当调用方法时才初始化realObject
return Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class<?>[] { TargetInterface.class },
(proxy, method, args) -> {
if (realObject == null) {
synchronized (this) {
if (realObject == null) {
realObject = new RealObject(); // 初始化实际对象
}
}
}
return method.invoke(realObject, args);
}
);
}
}
从代理看编程思想
代理模式的本质藏着一个很有意思的现实隐喻:就像租房时找中介能帮你过滤无效房源,编程中的代理对象也在客户端和目标对象之间扮演中介
角色,只不过它过滤的是重复逻辑和非核心功能。静态代理与动态代理的选择,本质上是对变化频率
的权衡:
静态代理像老城区的固定中介门店
,适合需求稳定的场景。比如开发中某个接口短期内不会变更,直接手写代理类添加日志,逻辑清晰又好维护,就像老中介对片区房源了如指掌,虽然门店固定,但处理起常规业务效率很高。但如果接口频繁修改,比如新增十个方法,静态代理就得跟着改十次,这就像老中介突然要处理跨区业务,得重新装修门店,成本极高。
动态代理则像互联网租房平台
,擅长应对变化。比如电商大促时需要给所有服务添加限流逻辑,动态代理只需写一个处理器,就能在运行时给不同类生成代理,就像平台能实时对接各种新房源。Spring AOP 用动态代理实现的@Transactional
注解,就是典型应用;不管业务方法怎么变,事务逻辑始终在代理层统一管理,这和平台统一管理不同房东的房源规则是一个道理。遇到没有接口的老破小
类(遗留代码),CGLIB 和 Byte Buddy 就像平台的万能中介
,不用房东(类)有接口(资格证),直接通过继承生成代理子类,把老代码也纳入管理。
代理模式的本质是 “控制访问” 与 “责任分离”:通过中介对象将 “业务逻辑” 与 “非业务逻辑” 解耦,使系统更符合单一职责原则 。无论是静态代理的 “编译期确定” 还是动态代理的 “运行时灵活扩展”,都是对 “封装变化” 这一设计哲学的实践让不变的业务逻辑保持稳定,让变化的增强逻辑通过代理层灵活调整。静态代理适合 “变化少” 的场景,就像老城区的固定中介门店;动态代理适合 “变化多” 的场景,像互联网平台的灵活中介系统。
理解代理模式,不仅能看懂 Spring、MyBatis 等框架的底层原理,还能在写代码时多一种不修改原逻辑,通过代理扩展功能的思路,这才是比代码更重要的编程思维。