动态代理允许我们在不修改源代码的情况下,为对象增加额外的行为。在SpringBoot应用中,动态代理被广泛用于实现事务管理、缓存、安全控制、日志记录等横切关注点。
1. JDK动态代理:Java原生的代理方案
实现原理
JDK动态代理是Java标准库提供的代理机制,基于java.lang.reflect.Proxy
类和InvocationHandler
接口实现。它通过反射在运行时动态创建接口的代理实例。
核心代码示例
java
public class JdkDynamicProxyDemo {
interface UserService {
void save(User user);
User find(Long id);
}
// 实现类
static class UserServiceImpl implements UserService {
@Override
public void save(User user) {
System.out.println("保存用户: " + user.getName());
}
@Override
public User find(Long id) {
System.out.println("查找用户ID: " + id);
return new User(id, "用户" + id);
}
}
// 调用处理器
static class LoggingInvocationHandler implements InvocationHandler {
private final Object target;
public LoggingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before: " + method.getName());
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
System.out.println("After: " + method.getName() + ", 耗时: " + (endTime - startTime) + "ms");
return result;
}
}
public static void main(String[] args) {
// 创建目标对象
UserService userService = new UserServiceImpl();
// 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
new LoggingInvocationHandler(userService)
);
// 调用代理方法
proxy.save(new User(1L, "张三"));
User user = proxy.find(2L);
}
}
优点
- JDK标准库自带:无需引入额外依赖,减少了项目体积
- 生成代码简单:代理逻辑集中在InvocationHandler中,易于理解和维护
- 性能相对稳定:在JDK 8后的版本中,性能有明显提升
局限性
- 只能代理接口:被代理类必须实现接口,无法代理类
- 反射调用开销:每次方法调用都需通过反射机制,有一定性能损耗
- 无法拦截final方法:无法代理被final修饰的方法
Spring中的应用
在Spring中,当Bean实现了接口时,默认使用JDK动态代理。这可以通过配置修改:
ini
@EnableAspectJAutoProxy(proxyTargetClass = false) // 默认值为false,表示优先使用JDK动态代理
2. CGLIB代理:基于字节码的强大代理
实现原理
CGLIB(Code Generation Library)是一个强大的高性能字节码生成库,它通过继承被代理类生成子类的方式实现代理。Spring从3.2版本开始将CGLIB直接集成到框架中。
核心代码示例
java
public class CglibProxyDemo {
// 不需要实现接口的类
static class UserService {
public void save(User user) {
System.out.println("保存用户: " + user.getName());
}
public User find(Long id) {
System.out.println("查找用户ID: " + id);
return new User(id, "用户" + id);
}
}
// CGLIB方法拦截器
static class LoggingMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before: " + method.getName());
long startTime = System.currentTimeMillis();
// 调用原始方法
Object result = proxy.invokeSuper(obj, args);
long endTime = System.currentTimeMillis();
System.out.println("After: " + method.getName() + ", 耗时: " + (endTime - startTime) + "ms");
return result;
}
}
public static void main(String[] args) {
// 创建CGLIB代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new LoggingMethodInterceptor());
// 创建代理对象
UserService proxy = (UserService) enhancer.create();
// 调用代理方法
proxy.save(new User(1L, "张三"));
User user = proxy.find(2L);
}
}
优点
- 可以代理类:不要求目标类实现接口,应用场景更广泛
- 性能较高:通过生成字节码而非反射调用,方法调用性能优于JDK代理
- 功能丰富:支持多种回调类型,如LazyLoader、Dispatcher等
- 集成到Spring:Spring框架已内置CGLIB,无需额外依赖
局限性
- 无法代理final类和方法:由于使用继承机制,无法代理final修饰的类或方法
- 构造函数调用:在生成代理对象时会调用目标类的构造函数,可能导致意外行为
- 复杂性增加:生成的字节码复杂度高,调试困难
- 对Java版本敏感:在不同Java版本间可能存在兼容性问题
Spring中的应用
在Spring中,当Bean没有实现接口或配置了proxyTargetClass=true
时,使用CGLIB代理:
ini
@EnableAspectJAutoProxy(proxyTargetClass = true) // 强制使用CGLIB代理
3. ByteBuddy:现代化的字节码操作库
实现原理
ByteBuddy是一个相对较新的字节码生成和操作库,设计更加现代化,API更加友好。它可以创建和修改Java类,而无需理解底层的JVM指令集。
核心代码示例
java
public class ByteBuddyProxyDemo {
interface UserService {
void save(User user);
User find(Long id);
}
static class UserServiceImpl implements UserService {
@Override
public void save(User user) {
System.out.println("保存用户: " + user.getName());
}
@Override
public User find(Long id) {
System.out.println("查找用户ID: " + id);
return new User(id, "用户" + id);
}
}
static class LoggingInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable,
@AllArguments Object[] args) throws Exception {
System.out.println("Before: " + method.getName());
long startTime = System.currentTimeMillis();
Object result = callable.call();
long endTime = System.currentTimeMillis();
System.out.println("After: " + method.getName() + ", 耗时: " + (endTime - startTime) + "ms");
return result;
}
}
public static void main(String[] args) throws Exception {
UserService userService = new UserServiceImpl();
// 创建ByteBuddy代理
UserService proxy = new ByteBuddy()
.subclass(UserServiceImpl.class)
.method(ElementMatchers.any())
.intercept(MethodDelegation.to(new LoggingInterceptor()))
.make()
.load(UserServiceImpl.class.getClassLoader())
.getLoaded()
.getDeclaredConstructor()
.newInstance();
// 调用代理方法
proxy.save(new User(1L, "张三"));
User user = proxy.find(2L);
}
}
优点
- 流畅的API:提供链式编程风格,代码更加可读
- 性能卓越:在多项基准测试中,性能优于CGLIB和JDK代理
- 类型安全:API设计更注重类型安全,减少运行时错误
- 支持Java新特性:对Java 9+模块系统等新特性有更好的支持
- 功能丰富:支持方法重定向、字段访问、构造函数拦截等多种场景
局限性
- 额外依赖:需要引入额外的依赖库
- 学习曲线:API虽然流畅,但概念较多,有一定学习成本
- 在Spring中集成度不高:需要自定义配置才能在Spring中替代默认代理机制
Spring中的应用
ByteBuddy虽然不是Spring默认的代理实现,但可以通过自定义ProxyFactory
来集成:
typescript
@Configuration
public class ByteBuddyProxyConfig {
@Bean
public AopConfigurer byteBuddyAopConfigurer() {
return new AopConfigurer() {
@Override
public void configureProxyCreator(Object bean, String beanName) {
// 配置ByteBuddy作为代理创建器
// 实现细节略
}
};
}
}
4. Javassist:更易用的字节码编辑库
实现原理
Javassist是一个开源的Java字节码操作库,提供了两个层次的API:源代码级别和字节码级别。它允许开发者用简单的Java语法直接编辑字节码,无需深入了解JVM规范。
核心代码示例
java
public class JavassistProxyDemo {
interface UserService {
void save(User user);
User find(Long id);
}
static class UserServiceImpl implements UserService {
@Override
public void save(User user) {
System.out.println("保存用户: " + user.getName());
}
@Override
public User find(Long id) {
System.out.println("查找用户ID: " + id);
return new User(id, "用户" + id);
}
}
public static void main(String[] args) throws Exception {
ProxyFactory factory = new ProxyFactory();
// 设置代理接口
factory.setInterfaces(new Class[] { UserService.class });
// 创建方法过滤器
MethodHandler handler = new MethodHandler() {
private UserService target = new UserServiceImpl();
@Override
public Object invoke(Object self, Method method, Method proceed, Object[] args) throws Throwable {
System.out.println("Before: " + method.getName());
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
System.out.println("After: " + method.getName() + ", 耗时: " + (endTime - startTime) + "ms");
return result;
}
};
// 创建代理对象
UserService proxy = (UserService) factory.create(new Class<?>[0], new Object[0], handler);
// 调用代理方法
proxy.save(new User(1L, "张三"));
User user = proxy.find(2L);
}
}
优点
- 源代码级API:可以不懂字节码指令集,以Java代码形式操作字节码
- 轻量级库:相比其他字节码库体积较小
- 功能全面:支持创建类、修改类、动态编译Java源代码等
- 性能良好:生成的代理代码性能接近CGLIB
- 长期维护:库历史悠久,稳定可靠
局限性
- API不够直观:部分API设计较为古老,使用不如ByteBuddy流畅
- 文档不足:相比其他库,文档和示例较少
- 内存消耗:在操作大量类时可能消耗较多内存
Spring中的应用
Javassist不是Spring默认的代理实现,但一些基于Spring的框架使用它实现动态代理,如Hibernate:
typescript
// 自定义Javassist代理工厂示例
public class JavassistProxyFactory implements ProxyFactory {
@Override
public Object createProxy(Object target, Interceptor interceptor) {
// Javassist代理实现
// ...
}
}
5. AspectJ:完整的AOP解决方案
实现原理
AspectJ是一个完整的AOP框架,与前面的动态代理方案不同,它提供两种方式:
- 编译时织入:在编译源代码时直接修改字节码
- 加载时织入:在类加载到JVM时修改字节码
AspectJ拥有专门的切面语言,功能比Spring AOP更强大。
核心代码示例
AspectJ切面定义:
java
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.UserService.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before: " + joinPoint.getSignature().getName());
}
@Around("execution(* com.example.service.UserService.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before: " + joinPoint.getSignature().getName());
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("After: " + joinPoint.getSignature().getName() +
", 耗时: " + (endTime - startTime) + "ms");
return result;
}
}
Spring配置AspectJ:
less
@Configuration
@EnableAspectJAutoProxy
public class AspectJConfig {
@Bean
public LoggingAspect loggingAspect() {
return new LoggingAspect();
}
}
编译时织入配置(maven):
xml
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<complianceLevel>11</complianceLevel>
<source>11</source>
<target>11</target>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
优点
- 功能最全面:支持几乎所有AOP场景,包括构造函数、字段访问、异常等
- 性能最优:编译时织入无运行时开销,性能接近原生代码
- 完整语言支持:有专门的切面语言和语法,表达能力强
- 更灵活的切入点:可以对接口、实现类、构造函数、字段等定义切面
局限性
- 复杂度高:学习曲线陡峭,需要掌握AspectJ语法
- 构建过程改变:需要特殊的编译器或类加载器
- 调试难度增加:修改后的字节码可能难以调试
Spring中的应用
Spring可以通过以下方式使用AspectJ:
- proxy-target-class模式:使用Spring AOP但CGLIB生成的代理
ini
@EnableAspectJAutoProxy(proxyTargetClass = true)
- LTW(Load-Time Weaving)模式:在类加载时织入
css
@EnableLoadTimeWeaving
- 编译时织入:需要配置AspectJ编译器
使用场景对比与选择建议
代理实现 | 最适用场景 | 不适用场景 |
---|---|---|
JDK动态代理 | 基于接口的简单代理,轻量级应用 | 没有实现接口的类,性能敏感场景 |
CGLIB | 没有实现接口的类,需要兼顾性能和便捷性 | final类/方法,高安全性环境 |
ByteBuddy | 现代化项目,关注性能优化,复杂代理逻辑 | 追求最小依赖的简单项目 |
Javassist | 需要动态生成/修改类的复杂场景 | API设计敏感项目,初学者 |
AspectJ | 企业级应用,性能关键型场景,复杂AOP需求 | 简单项目,快速原型,学习成本敏感 |