你是否遇到过这样的场景:定义了UserService接口和实现类UserServiceImpl,用@Autowired UserServiceImpl userService注入时启动直接报错,提示找不到对应类型的Bean。但换成接口@Autowired UserService userService却能正常运行。
这背后藏着Spring的核心设计------动态代理机制。Spring的AOP功能依赖代理对象实现,而代理对象的创建方式直接决定了这个"接口能注入、实现类却不行"的现象。本文通过JDK动态代理与CGLib的原理对比,带你搞懂这个问题的底层逻辑。
一、JDK动态代理:基于接口的代理方式
JDK动态代理是Java原生支持的代理方式,核心特点是必须基于接口。它的原理是在运行时为目标接口生成一个代理类,这个代理类实现了目标接口,会拦截所有方法调用,在执行目标方法前后插入额外逻辑。
看一个最简单的例子:
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 定义接口
interface UserService {
void save();
}
// 实现类
class UserServiceImpl implements UserService {
@Override
public void save() {
System.out.println("保存用户数据");
}
}
// 代理逻辑处理器
class LogInvocationHandler implements InvocationHandler {
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("日志:方法开始执行");
Object result = method.invoke(target, args);
System.out.println("日志:方法执行结束");
return result;
}
}
public class JdkProxyDemo {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
// 生成代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogInvocationHandler(target)
);
proxy.save();
// 输出:
// 日志:方法开始执行
// 保存用户数据
// 日志:方法执行结束
// 代理对象的真实类型
System.out.println(proxy.getClass().getName());
// 输出:com.sun.proxy.$Proxy0
}
}
这里有个关键点:生成的代理对象类型是$Proxy0,它是Proxy类的子类,实现了UserService接口,但和UserServiceImpl没有任何继承关系。所以代理对象只能强转为UserService接口类型,不能转为UserServiceImpl。
这就解释了开篇的问题------当Spring使用JDK动态代理时,容器中实际存放的是代理对象,它的类型是接口而非实现类,用实现类接收自然会报错。
二、CGLib:基于继承的代理方式
CGLib(Code Generation Library)采用完全不同的思路:通过继承目标类生成代理对象。代理类是目标类的子类,通过重写父类方法实现增强逻辑。因此CGLib不需要目标类实现接口。
同样来看一个例子:
java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
// 无需接口,直接定义目标类
class OrderService {
public void pay() {
System.out.println("订单支付");
}
}
// 方法拦截器
class TransactionInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("事务:开始");
Object result = proxy.invokeSuper(obj, args); // 调用父类方法
System.out.println("事务:提交");
return result;
}
}
public class CglibProxyDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderService.class);
enhancer.setCallback(new TransactionInterceptor());
OrderService proxy = (OrderService) enhancer.create();
proxy.pay();
// 输出:
// 事务:开始
// 订单支付
// 事务:提交
// 代理对象是目标类的子类
System.out.println(proxy.getClass().getSuperclass().getName());
// 输出:OrderService
}
}
注意这里用的是proxy.invokeSuper(obj, args)而不是反射调用,这是CGLib推荐的写法,直接调用父类方法,性能更好,也不需要持有目标对象的引用。
CGLib代理对象是目标类的子类,所以可以直接用目标类类型接收,不存在JDK代理那种类型不匹配的问题。
三、核心差异对比
| 维度 | JDK动态代理 | CGLib |
|---|---|---|
| 实现原理 | 实现目标接口的代理类 | 继承目标类的子类 |
| 依赖条件 | 必须有接口 | 无需接口 |
| 代理对象类型 | 接口类型 | 目标类的子类 |
| 限制 | 无法代理没有接口的类 | 无法代理final类和final方法 |
| 方法调用 | 反射调用 | invokeSuper直接调用父类 |
性能方面,JDK 8之后两者差距已经很小,不再是选择的主要考量因素。
四、Spring的代理策略演变
Spring Framework默认的策略是:目标类实现了接口就用JDK动态代理,没有接口就用CGLib。这个策略延续了很长时间,体现了Spring"面向接口编程"的设计理念。
但从Spring Boot 2.0开始,默认配置改成了spring.aop.proxy-target-class=true,也就是默认使用CGLib代理。这个变化主要出于两个考虑:
第一是减少开发者踩坑。太多人因为"用实现类接收注入报错"而困惑,CGLib代理不存在这个问题,降低了使用门槛。
第二是避免强制定义接口。有些简单的Service类本身不需要接口抽象,但为了能被AOP增强,开发者不得不额外定义一个接口,这属于为了框架而妥协的设计。CGLib消除了这个限制。
如果你的项目有"所有Service必须定义接口"的规范,可以通过配置切换回JDK代理:
yaml
spring:
aop:
proxy-target-class: false
或者用注解方式:
java
@EnableAspectJAutoProxy(proxyTargetClass = false)
五、实战中的避坑指南
理解了代理原理,很多看似玄学的问题都能解释清楚。
1. final方法上的@Transactional不生效
CGLib通过继承实现代理,子类无法重写父类的final方法,所以final方法上的AOP注解(如@Transactional、@Cacheable)都不会生效。这是个容易忽略的坑,编译期不会报错,运行时事务直接失效。
2. 同类方法调用事务失效
这是个经典问题。假设UserServiceImpl中有两个方法:
java
public void methodA() {
this.methodB(); // 直接调用,绕过代理
}
@Transactional
public void methodB() {
// 事务不会生效
}
this.methodB()是直接调用当前对象的方法,没有经过代理对象,自然不会触发事务增强。解决方案是通过AopContext获取代理对象:
java
public void methodA() {
((UserService) AopContext.currentProxy()).methodB();
}
使用前需要开启exposeProxy:
java
@EnableAspectJAutoProxy(exposeProxy = true)
3. private方法无法被代理
无论是JDK动态代理还是CGLib,都无法增强private方法。JDK代理基于接口,接口中不存在private方法;CGLib基于继承,子类无法重写父类的private方法。所以在private方法上加@Transactional是无效的。
六、总结
回到开篇的问题:为什么注入实现类会报错?
如果你的项目用的是Spring Boot 2.0之前的版本,或者显式配置了JDK动态代理,那么容器中存放的是实现了接口的代理对象,它和实现类没有继承关系,用实现类类型接收就会报类型不匹配。
升级到Spring Boot 2.x之后,CGLib成为默认选项,代理对象是实现类的子类,这个问题就不存在了。
理解代理机制的价值不仅仅是解决注入报错,更重要的是能看懂Spring AOP的底层逻辑。下次遇到事务失效、注解不生效这类问题,先问自己两个问题:当前用的是哪种代理?代理对象到底是什么类型?答案往往就藏在这里。