学完这篇,你能把 Spring AOP 拆成两半看清:反射负责“在运行时调到你想要的方法”,动态代理负责“在调用的前后插一段逻辑”,并且能自己手写一个可运行的代理例子,知道 @Transactional 为什么在同类自调用时会失效。
第一步:先用反射把方法“抠”出来
反射的核心只有一句话:类信息在运行时是可读的。拿到 Class 对象,就能问出它有什么方法、什么字段,并且能直接调用。
public class UserService {
public String save(String name) {
return "saved: " + name;
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.example.UserService");
Object target = clazz.getDeclaredConstructor().newInstance();
Method m = clazz.getMethod("save", String.class);
Object ret = m.invoke(target, "张三");
System.out.println(ret); // saved: 张三
}
}
三个常用入口:
Class.forName("全类名") —— 从字符串拿类,框架配置里写类名走的就是这条
clazz.getMethod(名, 参数类型...) —— 拿 public 方法,含继承来的
clazz.getDeclaredMethod(...) —— 拿本类声明的,包括 private
注意:反射调用私有方法或字段前要 setAccessible(true),Java 9 之后模块化有额外限制,JDK 17+ 对 java.* 包内反射会直接抛 InaccessibleObjectException,别拿它去改 JDK 内部类。
第二步:给反射套一层“方法拦截”
反射能调方法,但只是“能调”。要在调用前后加日志、加事务,就得把方法调用包装成一个可替换的过程。动态代理做的就是这件事:生成一个替身对象,所有方法调用都先进你的拦截器,你再决定要不要反射调到真对象上。
JDK 动态代理要求目标实现接口:
public interface UserService {
void save(String name);
}
UserService target = new UserServiceImpl();
InvocationHandler handler = (proxy, method, args) -> {
System.out.println("前置:" + method.getName());
Object ret = method.invoke(target, args); // 反射调真对象
System.out.println("后置");
return ret;
};
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[]{UserService.class},
handler);
proxy.save("张三");
proxy 不是 UserServiceImpl 的实例,是 JVM 运行时生成的 $Proxy0,它实现了 UserService,每个方法体里都是 handler.invoke(...)。
注意:method.invoke(target, args) 里 target 不能写成 proxy,否则自己调自己,栈溢出。
第三步:没有接口怎么办,用 CGLIB
JDK 代理的硬限制是只能代理接口方法。Spring 的补法是 CGLIB:用字节码生成目标类的子类,覆写所有非 final 方法,在重写的方法里插拦截。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) ->
proxy.invokeSuper(obj, args)); // 调用父类原方法
UserServiceImpl p = (UserServiceImpl) enhancer.create();
注意:子类方案下,final 类和 final 方法无法被覆写,private 方法也无法被代理——这些方法上的 AOP 注解会静默失效。这也是为什么 Spring 文档一直建议切面方法声明成 public。
第四步:Spring AOP 怎么二选一
Spring 的判断逻辑在 DefaultAopProxyFactory:
- 目标实现了至少一个接口 → 默认用 JDK 代理
- 目标没有接口 → 用 CGLIB
- 显式设置
proxyTargetClass = true → 一律 CGLIB
Spring Boot 2.x 起默认值已经改成 true,也就是默认全用 CGLIB。改回来:
spring.aop.proxy-target-class=false
或者在配置类上:
@EnableAspectJAutoProxy(proxyTargetClass = true)
注意:用 JDK 代理时,注入必须按接口类型注入(@Autowired UserService),按实现类注入会报找不到 Bean;CGLIB 则没这个限制。混用两种模式的项目最容易在这里出问题。
第五步:绕开三个必踩的失效场景
自调用失效。 类里 this.methodB() 是原始对象上的调用,没经过代理,切面不生效。
@Service
public class OrderService {
public void a() { this.b(); } // 事务不生效
@Transactional public void b() { }
}
三种解法:把 b() 拆到另一个 Bean;注入自身 @Lazy @Autowired OrderService self;或 AopContext.currentProxy()(需开 @EnableAspectJAutoProxy(exposeProxy = true))。
private / final 方法不生效。 见第三步。静态方法同理。
代理对象不等于目标对象。 proxy.getClass() != target.getClass(),用 AopUtils.isAopProxy(bean) 判断,取原始对象用 AopProxyUtils.getSingletonTarget(bean)。
小结
- 反射是运行时读取并调用成员的能力,入口是
Class → Method → invoke。
- 动态代理在反射外面套一层拦截:JDK 靠接口 +
InvocationHandler,CGLIB 靠生成子类 + MethodInterceptor。
- Spring AOP 只是把这两件事工程化:有接口走 JDK,没接口走 CGLIB,Boot 2.x 起默认全 CGLIB。
- 失效基本逃不出三条:同类自调用、private/final/static 方法、注入类型和代理类型不匹配。
- 排查先问自己:这次调用到底有没有经过代理对象。