问题引入
首先我们看一个问题,假设项目中有一个用户服务接口,定义了一些核心操作:
public interface UserService {
void register(String username, String password);
void login(String username, String password);
}
以及他的实现类:
public class UserServiceImpl implements UserService {
@Override
public void register(String username, String password) {
// 注册逻辑
}
@Override
public void login(String username, String password) {
// 登录逻辑
}
}
有一天,产品经理找到你,提出一个新需求:给 UserService 的每个方法都加上日志记录,每次调用时输出方法名和参数。你一听,这还不简单?打开 UserServiceImpl,在每个方法体开头加一行日志不就完了。三分钟搞定,收工。
但问题是真的这么简单吗?你重新审视了一下刚才的做法,发现几个问题:
1.代码侵入:日志代码和业务逻辑耦合在一起。
2.冗余:每个方法都要手动加,如果接口有几十个方法,都要重复加。
3.不易扩展:如果需求继续变化,需要加权限、加监控,只能继续往实现类里塞。
那么有没有一种方式,能在不修改原有实现类的前提下,给这个类的方法"织入"额外的逻辑?答案就是------代理。
静态代理
代理可以理解为:你原来直接调用 UserServiceImpl 的方法,现在我们找一个"中间人"挡在前面。这个中间人长得跟 UserService 一模一样,但它可以在把请求转给真正的 UserServiceImpl 之前或之后,做一些额外的事情,比如记录日志。
java的代理有静态代理和动态代理两种,唯一的区别就是代理类的生成时机。
静态代理的代理类在编译期就已经确定了。也就是说,你需要手动为每一个需要被增强的类,手写一个对应的代理类。回到我们的场景,给 UserService 加日志,用静态代理的做法是这样的。
首先,创建一个代理类,它和 UserServiceImpl 一样实现了 UserService 接口:
public class UserServiceProxy implements UserService {
// 持有真正的实现类对象
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void register(String username, String password) {
System.out.println("日志:调用register方法,参数:" + username);
target.register(username, password);
System.out.println("日志:register方法执行完毕");
}
@Override
public void login(String username, String password) {
System.out.println("日志:调用login方法,参数:" + username);
target.login(username, password);
System.out.println("日志:login方法执行完毕");
}
}
使用时,只要把原来的实现类"包"进代理里:
UserService userService = new UserServiceProxy(new UserServiceImpl());
userService.register("张三", "123456");
有没有发现什么问题,我们需要手动生成代理类,每对一个类做增强,都需要手动去写一个代理类,这就是静态代理最大的痛点------代理类数量膨胀、重复代码多、维护成本高。
既然问题出在"每次都要手写代理类"上,那能不能让程序自动帮我们生成代理类?说到这,我们大概能理解动态代理的出场逻辑了------它的代理类是在运行时通过反射动态生成的,不需要我们手动编写。下一节,我们来继续看看 JDK 动态代理是怎么优雅解决这个问题的。
jdk动态代理
这里只关注jdk实现的动态代理,他有个硬性要求就是被代理的对象必须实现接口。
为什么?因为代理本质上就是"模板 + 增强逻辑"。接口就是模板------它定义了有哪些方法可以调用,代理类照着这个模板生成,才能跟原来的实现类长得一模一样。增强逻辑则统一写在 InvocationHandler 中,一个 Handler 就代表一个独立的增强功能。
首先我们写一个增强的逻辑:
public class LogInvocationHandler implements InvocationHandler {
// 持有真正的业务对象
private Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 记录日志(前)
System.out.println("日志:调用" + method.getName() + "方法,参数:" + Arrays.toString(args));
// 调用真正的业务方法
Object result = method.invoke(target, args);
// 记录日志(后)
System.out.println("日志:" + method.getName() + "方法执行完毕");
return result;
}
}
使用的时候需要把模板和增强逻辑组装起来:
// 被代理的对象
UserService target = new UserServiceImpl();
// 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
new Class[]{UserService.class}, // 接口
new LogInvocationHandler(target) // 增强逻辑
);
// 通过代理调用
proxy.register("张三", "123456");
proxy.login("张三", "123456");
类之间的关系:
客户端
│
│ 调用
▼
┌──────────────────────────┐
│ $Proxy0 (动态生成的代理) │ ← 实现 UserService 接口(模板)
│ 实现了 UserService 接口 │
└──────────┬───────────────┘
│ 持有
▼
┌──────────────────────────┐
│ LogInvocationHandler │ ← 增强逻辑(集中在一处)
│ - invoke() │
└──────────┬───────────────┘
│ 持有
▼
┌──────────────────────────┐
│ UserServiceImpl │ ← 真正的业务对象
└──────────────────────────┘
可以发现,JDK 动态代理抽象出了一层 InvocationHandler,所有增强逻辑都集中在这里,不像静态代理那样散落在每个代理类的每个方法中,减少了逻辑冗余。
那么jdk代理有没有什么缺点呢?
- 代理类需要手动生成,每次都要手动调用 Proxy.newProxyInstance(),写一堆样板代码。项目里有几个需要代理的类,就得写几遍,没有自动化的创建和管理机制。
- 以及多个增强逻辑的编排需要手动完成,一个 InvocationHandler 代表一个增强功能。日志一个 Handler,事务一个 Handler,权限又一个 Handler。当多个功能需要组合时,只能手动层层嵌套。
- 方法路由靠硬编码,匹配规则太简单,一个 Handler 代理了整个接口,所有方法调用都进同一个 invoke。想对不同方法做不同处理,只能在 invoke 里手写判断:
spring aop代理
回顾一下核心结论------代理就做了两件事:拿模板 + 增强逻辑。这里只讲 JDK 实现的代理方式。
在 Spring AOP 中,增强逻辑进一步被拆分为切面(Aspect),一个切面就是 "匹配规则 + 增强逻辑" 的组合:
切面(Aspect)= Pointcut(在哪切)+ Advice(切了干啥)
代理对象的生成过程是Spring 通过 Bean 后置处理器(BeanPostProcessor,简称 BPP)在 Bean 初始化之后介入,判断是否需要为这个 Bean 生成代理对象。不再需要我们手动创建。
容器启动
│
├── 扫描所有 @Aspect 类
│ └── 解析成一个个 Advisor(匹配规则 + 增强逻辑)
│
└── 初始化 Bean(比如 UserService)
│
└── BPP 拦截
│
├── 遍历所有 Advisor,用 Pointcut 匹配这个 Bean 的方法
│
├── 有匹配 → 创建代理对象(JDK 方式:拿接口做模板)
│
└── 无匹配 → 返回原对象
spring BPP会在bean初始化之后,判断是否需要生成代理对象。核心的BPP:找到所有切面,判断匹配规则pointcut是否匹配,创建代理对象。
代理对象:spring 容器创建
路由的逻辑:invocationhandler自动匹配+组织切面
代理对象的核心是 JdkDynamicAopProxy,它实现了 InvocationHandler------这直接证明了 Spring AOP 底层和 JDK 动态代理是同一套逻辑:模板 + 增强。
当调用代理对象的方法时,入口就是它的 invoke 方法,路由的逻辑全部由 JdkDynamicAopProxy.invoke() 自动完成------自动匹配 + 自动编排切面,不再需要我们手写 if-else 判断方法名。
接口和实现类保持不变,定义切面:
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.UserService.*(..))")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("日志:调用" + methodName + "方法,参数:" + Arrays.toString(args));
Object result = joinPoint.proceed();
System.out.println("日志:" + methodName + "方法执行完毕");
return result;
}
}
使用:
@RestController
public class UserController {
@Autowired
private UserService userService; // 注入的已经是代理对象
@PostMapping("/register")
public String register(@RequestParam String username, @RequestParam String password) {
userService.register(username, password);
return "注册成功";
}
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
userService.login(username, password);
return "登录成功";
}
}
spring aop实现代理的调用过程:
容器启动
│
├── 扫描 @Aspect → 解析成 Advisor(匹配规则 + 增强逻辑)
│
└── 初始化 Bean(UserService)
│
└── BPP 拦截
│
├── 拿切面的匹配规则去匹配这个Bean的方法
├── 匹配上 → 生成代理对象(模板)
└── 没匹配上 → 返回原对象
行时调用代理方法
│
└── JdkDynamicAopProxy.invoke()
│
├── 用 Pointcut 匹配当前方法,找到所有匹配的 Advisor
├── 将这些 Advisor 的 Advice 包装成拦截器链
├── 构建 ReflectiveMethodInvocation 责任链
└── 调用 invocation.proceed(),拦截器链依次执行
至此,JDK 动态代理的三大痛点都有了答案:
- 代理需手动创建?→ Spring 容器通过 BPP 自动生成
- 多 Handler 需手动嵌套?→ JdkDynamicAopProxy 内部用拦截器链串联
- 方法路由靠硬编码?→ Pointcut 表达式自动匹配
总结
直接改实现类 → 业务侵入
静态代理 → 解决侵入,但类爆炸 + 逻辑重复
动态代理 → 解决类爆炸,但代理需手动创建 + 多逻辑手动套娃
Spring AOP → 全自动:代理自动生成、容器自动注入、切面自由编排
本博客内容仅为个人技术学习与分享,文中观点仅代表个人。由于个人水平有限,文章难免有疏漏或错误之处,欢迎在评论区指正交流。