一、先聊聊代理模式:找个“替身”干活
想象一下,你是个大明星,每天要接很多通告(业务逻辑)。但你不能事事亲为,比如谈合同、安排行程、应付粉丝这些琐事,你会交给经纪人(代理)去处理。经纪人在帮你做事(调用核心方法)前后,可以加上他自己的操作,比如筛选好的合同、安排最优路线、礼貌地接待粉丝(这就是增强逻辑)。
在程序世界里,这就是“代理模式”。它让一个类(代理类)代表另一个类(真实类)去执行方法,并且可以在执行前后“加戏”。代理分为两种:
- 静态代理:在程序运行前,代理类的.class文件就已经存在了。就像你专门为这位明星雇了一个经纪人,一对一服务。缺点很明显,如果明星多了(类多了),经纪人也要成倍增加,很麻烦。
- 动态代理:在程序运行时,通过反射机制“动态”地创建出代理类。就像一个经纪人公司,来了哪个明星,就临时根据他的特点(实现的接口)生成一个专属的“临时经纪人”,这个经纪人知道所有明星的公共工作流程(接口方法)。
我们今天的主角就是动态代理,它解决了静态代理需要为每个类编写代理的繁琐问题。
二、Java动态代理的“三板斧”:InvocationHandler与Proxy类
Java自身提供了一套创建动态代理的机制,核心就两个家伙:java.lang.reflect.InvocationHandler 和 java.lang.reflect.Proxy。
InvocationHandler(调用处理器): 这是“加戏”逻辑的编剧和导演。你所有想在方法前后做的额外操作,比如记录日志、检查权限、管理事务,都在这里写。Proxy(代理类工厂): 这是“临时经纪人”的生产车间。你告诉它:“我要一个能代理某个明星(实现了某些接口的对象)的经纪人,他的行为规则按这个剧本来(InvocationHandler)。” 它就能给你造出来。
光说不练假把式,我们直接上代码。下面的例子,我们来模拟一个“用户服务”,并在其方法调用前后加上日志。
技术栈:Java SE
// 技术栈:Java SE
// 1. 首先,定义一个明星(业务接口)
public interface UserService {
void addUser(String name);
void deleteUser(String id);
}
// 2. 真实的明星本人(接口实现类,也就是被代理的目标对象)
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
// 这里是核心业务逻辑,比如操作数据库
System.out.println("【真实方法】添加用户: " + name);
// 模拟业务操作
// ... 连接数据库,执行insert ...
}
@Override
public void deleteUser(String id) {
System.out.println("【真实方法】删除用户,ID: " + id);
// ... 连接数据库,执行delete ...
}
}
// 3. 编剧和导演(实现InvocationHandler接口)
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class LogInvocationHandler implements InvocationHandler {
// 持有被代理的真实目标对象,就像经纪人要知道为哪个明星服务
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
/**
* 这是最核心的方法!所有对代理对象方法的调用,都会转到这个invoke方法来处理。
* @param proxy 动态生成的代理对象本身(这个例子中我们不太直接用)
* @param method 被调用的方法(比如addUser)
* @param args 调用方法时传入的参数(比如"张三")
* @return 方法的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 【前置增强】在调用真实方法前“加戏”
System.out.println("[日志] 开始执行方法: " + method.getName() + ", 参数: " + java.util.Arrays.toString(args));
// 调用真实对象的方法,这才是核心业务
Object result = method.invoke(target, args);
// 【后置增强】在调用真实方法后“加戏”
System.out.println("[日志] 方法执行完毕: " + method.getName());
// 返回真实方法的执行结果
return result;
}
}
// 4. 客户端(使用代理)
import java.lang.reflect.Proxy;
public class DynamicProxyDemo {
public static void main(String[] args) {
// 创建真实目标对象(明星本人)
UserService realService = new UserServiceImpl();
// 创建调用处理器,并传入真实对象(告诉编剧为谁写剧本)
InvocationHandler handler = new LogInvocationHandler(realService);
// 使用Proxy的工厂方法创建代理对象(经纪人公司造出临时经纪人)
// 参数1:类加载器,通常用目标类的加载器
// 参数2:代理类需要实现的接口列表,这里就是UserService接口
// 参数3:调用处理器实例,里面包含了增强逻辑
UserService proxyService = (UserService) Proxy.newProxyInstance(
realService.getClass().getClassLoader(), // 类加载器
realService.getClass().getInterfaces(), // 实现的接口
handler // 调用处理器
);
// 现在,我们调用的是代理对象的方法,而不是真实对象
System.out.println("--- 通过代理对象调用addUser ---");
proxyService.addUser("张三");
System.out.println("\n--- 通过代理对象调用deleteUser ---");
proxyService.deleteUser("001");
// 你也可以直接调用真实对象,对比一下
System.out.println("\n--- 直接调用真实对象addUser ---");
realService.addUser("李四");
}
}
运行上面的DynamicProxyDemo的main方法,你会看到类似下面的输出:
--- 通过代理对象调用addUser ---
[日志] 开始执行方法: addUser, 参数: [张三]
【真实方法】添加用户: 张三
[日志] 方法执行完毕: addUser
--- 通过代理对象调用deleteUser ---
[日志] 开始执行方法: deleteUser, 参数: [001]
【真实方法】删除用户,ID: 001
[日志] 方法执行完毕: deleteUser
--- 直接调用真实对象addUser ---
【真实方法】添加用户: 李四
看!当我们通过proxyService(代理对象)调用方法时,日志被自动加上了。而直接调用realService(真实对象)则没有。这个“临时经纪人”完美地完成了任务。
关键点回顾:
- Java动态代理只能基于接口生成代理。
Proxy.newProxyInstance的第二个参数就是接口数组。如果你的类没有实现接口,Java内置的动态代理就无能为力了(这时Spring会转向CGLIB,我们后面讲)。 - 代理对象是在运行时动态生成的,你看不到它的
.java源文件,但JVM会生成对应的.class字节码。 - 所有对代理对象方法的调用,都被“拦截”并转交给了
InvocationHandler.invoke方法,由它来决定如何调用真实方法以及如何“加戏”。
三、Spring AOP:动态代理的“集大成者”
AOP(面向切面编程)是Spring框架的核心功能之一,它的底层实现主要就依赖于我们刚讲的动态代理技术。AOP的目标是将那些分散在各个方法中的“横切关注点”(比如日志、事务、安全)集中管理,让业务代码更纯净。
在Spring AOP中,有几个关键概念和我们上面的例子是对应的:
- 连接点(Joinpoint): 程序执行过程中明确的点,如方法调用、异常抛出。对应我们例子中
addUser、deleteUser这些方法。 - 切点(Pointcut): 一个表达式,用来匹配哪些连接点需要被增强。比如“所有
UserService接口中以add开头的方法”。 - 通知(Advice): 就是“加戏”的具体内容,也就是我们
InvocationHandler.invoke方法里写的那些日志代码。Spring定义了多种通知类型:前置通知(@Before)、后置通知(@After)、返回通知(@AfterReturning)、异常通知(@AfterThrowing)、环绕通知(@Around,功能最强大,相当于整个invoke方法)。 - 切面(Aspect): 切点 + 通知 = 切面。它定义了“在什么地方(切点)做什么事(通知)”。
Spring在创建Bean(比如我们的UserService)时,如果发现它匹配了某个切面规则,就不会直接返回原始对象,而是会返回一个动态生成的代理对象。后续所有对该Bean的方法调用,都会经过这个代理,从而执行我们定义的增强逻辑。
Spring AOP的两种代理策略:
- JDK动态代理: 就是上面我们演示的,基于接口的代理。如果目标类实现了至少一个接口,Spring默认优先使用它。
- CGLIB代理: 通过继承目标类来创建子类作为代理。如果目标类没有实现接口,Spring会自动使用CGLIB。你也可以强制Spring对所有情况都使用CGLIB(通过配置
proxy-target-class=true)。
我们来用Spring的方式重写上面的日志例子,感受一下AOP的优雅。
技术栈:Spring Boot + Spring AOP
// 技术栈:Spring Boot + Spring AOP
// 首先,确保pom.xml引入了 spring-boot-starter-aop 依赖
// 1. 业务接口和实现类(和之前几乎一样)
public interface UserService {
void addUser(String name);
void deleteUser(String id);
}
@Service // Spring管理的Bean
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("【业务方法】添加用户: " + name);
}
@Override
public void deleteUser(String id) {
System.out.println("【业务方法】删除用户,ID: " + id);
}
}
// 2. 定义切面(Aspect)—— 替代了我们手写的InvocationHandler
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect // 声明这是一个切面类
@Component // 同样需要被Spring管理
public class LogAspect {
/**
* 定义切点(Pointcut):匹配UserService接口下的所有方法
* execution(...)是切点表达式语言,这里表示:
* 执行(任何返回值) com.example.service.UserService.*(任何方法)(任何参数)
*/
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void pointcut() {} // 方法体通常为空,它只是一个标记
/**
* 环绕通知(Around Advice):功能最强大的通知,可以控制是否执行目标方法
* 它类似于我们之前手写的InvocationHandler.invoke方法
* @param joinPoint 封装了被拦截方法的详细信息
* @return 目标方法的返回值
* @throws Throwable
*/
@Around("pointcut()") // 引用上面定义的切点
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法名和参数
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 【前置增强】
System.out.println("[Spring AOP日志] 方法 " + methodName + " 开始,参数: " + java.util.Arrays.toString(args));
// 执行目标方法(相当于 method.invoke(target, args))
Object result = joinPoint.proceed();
// 【后置增强】
System.out.println("[Spring AOP日志] 方法 " + methodName + " 结束");
// 返回结果
return result;
}
// 你也可以定义更细粒度的通知,比如只在前置执行
@Before("pointcut()")
public void beforeLog() {
System.out.println("[Spring AOP日志] 方法即将执行...");
}
}
// 3. 使用端(Controller或其它Service)
@RestController
public class UserController {
@Autowired // Spring会自动注入一个代理后的UserService
private UserService userService;
@GetMapping("/add")
public String addUser() {
userService.addUser("王五"); // 这次调用会被AOP拦截增强
return "success";
}
}
启动Spring Boot应用并访问/add接口,控制台会打印出增强的日志。你看,我们不再需要手动创建Proxy和InvocationHandler,只需要通过几个注解(@Aspect, @Pointcut, @Around)声明式地定义切面和通知,Spring在背后就为我们安排好了一切。业务类UserServiceImpl的代码非常干净,没有任何日志代码的污染。
四、深入理解:场景、优缺点与注意事项
应用场景 动态代理和AOP的应用极其广泛,是构建健壮、可维护系统的利器:
- 日志记录: 为重要操作自动记录入参、结果、执行时间。
- 事务管理: Spring的
@Transactional注解就是基于AOP实现的,在方法开始前开启事务,结束后提交或回滚。 - 权限校验: 在方法执行前,判断当前用户是否有权限。
- 性能监控: 统计方法耗时,用于性能分析和优化。
- 缓存: 方法执行前先查缓存,存在则直接返回,不存在则执行方法并存入缓存。
- 异常统一处理: 捕获特定异常,进行转换或记录。
- 接口调用审计: 记录谁在什么时候调用了哪个接口。
技术优缺点
- 优点:
- 解耦: 将非核心业务逻辑(如日志、事务)从核心业务代码中剥离,使代码职责单一,更易于维护和复用。
- 灵活与动态: 动态代理在运行时生成,无需修改原有代码即可增加新功能,符合“开闭原则”。
- 减少重复代码: 像日志这种每个方法都可能需要的代码,写一次切面,就能应用到所有匹配的方法上。
- 缺点与局限:
- 性能开销: 动态代理通过反射调用方法,比直接调用会慢一点。但对于大多数应用,这点开销远小于其带来的好处。CGLIB通过生成子类,通常比JDK反射调用快,但创建代理对象稍慢。
- 只能拦截public方法: Spring AOP默认只能拦截代理对象的public方法。内部方法调用(即同一个类中A方法调用B方法)是不会被代理拦截的,因为内部调用走的是
this引用,而不是代理对象。 - 理解成本: AOP引入了新的抽象概念(切点、通知等),并存在“魔法”般的自动代理行为,初学者需要时间理解其原理,否则调试时会感到困惑。
- JDK代理的接口限制: 如前所述,JDK动态代理要求目标类必须实现接口。
注意事项
- “this”调用问题: 这是最常见的坑。在同一个Bean内部,方法A调用方法B,方法B的增强会失效。因为A调用的是
this.B(),而this是真实对象,不是代理对象。解决方案:① 将方法B抽到另一个Bean中;② 使用AspectJ(更强大的AOP实现,可以编译时或加载时织入,但配置更复杂);③ 从AopContext获取当前代理(不推荐,有侵入性)。 - final方法问题: CGLIB通过生成子类来代理,所以无法代理final类或final方法,因为它们不能被继承或重写。
- 切点表达式精度: 切点表达式写得太宽泛(如
execution(* *.*(..)))可能会匹配到不期望的类和方法,造成性能浪费或意外行为。要尽量精确。 - 通知执行顺序: 如果一个切点被多个通知匹配,执行顺序需要通过
@Order注解或实现Ordered接口来控制。
总结 Java动态代理是AOP思想的基石之一,它利用反射机制在运行时“无中生有”地创建出代理类,从而实现对目标方法的拦截和增强。Spring AOP将这一技术封装得无比优雅,让我们通过简单的注解就能实现强大的横切关注点管理。
理解动态代理的原理,不仅能让你更深入地掌握Spring AOP,在遇到相关问题时能快速定位(比如为什么我的注解没生效?),也能让你在设计高扩展性系统时多一件趁手的工具。它体现了“通过代理进行扩展,而非修改”这一重要的软件设计原则。下次当你在代码中轻松地使用@Transactional时,不妨想一想,背后正是动态代理这位“无形的高手”在默默支撑。
评论