Java代理模式深度解析:静态代理 vs 动态代理 vs CGLIB

摘要

代理模式是Java开发中最核心的设计模式之一,也是Spring AOP的底层基石。但很多同学写了三年Java,对静态代理、JDK动态代理、CGLIB代理的区别还是一知半解。这篇文章从源码级别拆解三种代理的实现原理,对比它们的性能差异和适用场景,最后结合Spring AOP的源码,看看Spring到底是怎么选代理的。看完这篇,面试问代理模式,你可以直接和他聊源码。

一、为什么会写这篇

最近在review项目代码时,发现组里一位同学在写一个日志切面,用的是Spring AOP的@Around注解。一切看起来很正常,直到他问了我一个问题:

"门主,为什么我加了@Transactional的Service方法,事务有时候生效有时候不生效?"

我让他把代码发过来看了一眼,发现他Service实现了一个接口,但是在@Transactional注解加在了实现类的方法上,而不是接口方法上。深入了解后才发现,他对Spring AOP底层用的哪种代理方式完全不清楚。

这其实不是个例。很多Java开发用了几年Spring,每天在用@Transactional@Cacheable,但对底层的代理机制一知半解。一旦遇到代理失效、AOP不生效的问题,就开始玄学排错。

所以今天干脆把代理模式这件事彻底讲清楚。

二、什么是代理模式

2.1 定义

代理模式(Proxy Pattern)属于结构型设计模式。它的核心思想是:不直接访问目标对象,而是通过一个代理对象来间接访问

听起来很简单对吧?但就是这个简单的思想,撑起了Spring AOP、MyBatis Mapper接口、RPC远程调用等一大片技术体系。

2.2 核心角色

代理模式中有三个核心角色:

角色 说明 类比
Subject(抽象主题) 定义真实对象和代理对象的公共接口 合同
RealSubject(真实主题) 真正执行业务逻辑的对象 房东
Proxy(代理) 持有RealSubject的引用,在调用前后做增强处理 中介

2.3 UML类图

2.4 为什么需要代理?

举个例子。假设你有一个UserService,里面有个saveUser()方法。现在上线要求:所有Service方法调用都要记录日志

最简单的做法是什么?在每个方法里手动加日志:

c 复制代码
public void saveUser(User user) {
    log.info("开始保存用户: {}", user.getId());
    // 业务逻辑...
    log.info("保存用户完成");
}

一个方法还行,但如果有100个方法呢?这就是典型的横切关注点问题。日志、事务、权限这些逻辑和业务逻辑混在一起,代码越来越臃肿。

代理模式就是来解决这个问题的:把增强逻辑抽离出来,通过代理对象统一织入

三、静态代理

3.1 实现方式

静态代理是最原始的代理方式。代理类和目标类实现同一个接口,在代理类中持有目标类的引用,在调用目标方法前后做增强处理。

3.2 代码示例

先定义一个接口:

arduino 复制代码
/**
 * 用户服务接口
 * 不管是真实对象还是代理对象,都实现这个接口
 */
public interface UserService {
    
    /**
     * 保存用户
     */
    void saveUser(String userId, String userName);
    
    /**
     * 查询用户
     */
    String getUser(String userId);
}

真实业务类:

typescript 复制代码
/**
 * 真实的用户服务实现类
 * 这里只关心业务逻辑,不关心日志、事务等横切关注点
 */
public class UserServiceImpl implements UserService {
    
    @Override
    public void saveUser(String userId, String userName) {
        // 模拟业务处理
        System.out.println("【业务逻辑】保存用户: id=" + userId + ", name=" + userName);
    }
    
    @Override
    public String getUser(String userId) {
        // 模拟业务处理
        System.out.println("【业务逻辑】查询用户: id=" + userId);
        return "User{id='" + userId + "', name='张三'}";
    }
}

静态代理类:

typescript 复制代码
import java.time.LocalDateTime;

/**
 * 静态代理类
 * 
 * 核心思路:
 * 1. 实现和目标类相同的接口
 * 2. 持有目标类的引用
 * 3. 在调用目标方法前后做增强处理
 */
public class UserServiceStaticProxy implements UserService {
    
    // 持有目标对象的引用
    private final UserService target;
    
    /**
     * 通过构造方法传入目标对象
     */
    public UserServiceStaticProxy(UserService target) {
        this.target = target;
    }
    
    @Override
    public void saveUser(String userId, String userName) {
        // 前置增强:记录日志
        System.out.println("【代理】前置日志 - 开始执行 saveUser,时间: " + LocalDateTime.now());
        
        // 调用目标对象的实际方法
        target.saveUser(userId, userName);
        
        // 后置增强:记录日志
        System.out.println("【代理】后置日志 - saveUser 执行完成,时间: " + LocalDateTime.now());
    }
    
    @Override
    public String getUser(String userId) {
        // 前置增强
        System.out.println("【代理】前置日志 - 开始执行 getUser,时间: " + LocalDateTime.now());
        
        // 调用目标方法
        String result = target.getUser(userId);
        
        // 后置增强
        System.out.println("【代理】后置日志 - getUser 执行完成,时间: " + LocalDateTime.now());
        
        return result;
    }
}

客户端调用:

arduino 复制代码
/**
 * 客户端调用
 * 对调用方来说,完全感知不到代理的存在
 * 它只知道自己拿到的是一个 UserService 接口
 */
public class Client {
    public static void main(String[] args) {
        // 1. 创建真实对象
        UserService realService = new UserServiceImpl();
        
        // 2. 创建代理对象,把真实对象传进去
        UserService proxy = new UserServiceStaticProxy(realService);
        
        // 3. 调用代理对象的方法(对调用方透明)
        proxy.saveUser("U001", "张三");
        
        System.out.println("--- 分割线 ---");
        
        String user = proxy.getUser("U001");
        System.out.println("查询结果: " + user);
    }
}

运行结果:

ini 复制代码
【代理】前置日志 - 开始执行 saveUser,时间: 2025-06-15T10:30:15.123
【业务逻辑】保存用户: id=U001, name=张三
【代理】后置日志 - saveUser 执行完成,时间: 2025-06-15T10:30:15.125
--- 分割线 ---
【代理】前置日志 - 开始执行 getUser,时间: 2025-06-15T10:30:15.125
【业务逻辑】查询用户: id=U001
【代理】后置日志 - getUser 执行完成,时间: 2025-06-15T10:30:15.125
查询结果: User{id='U001', name='张三'}

3.3 静态代理的优缺点

优点:

  • 实现简单,容易理解
  • 代码直观,编译期就能发现类型错误

缺点:

  • 代码冗余严重:每个方法都要写一遍增强逻辑,如果有100个方法,代理类就有100份重复代码
  • 类爆炸:每个目标类都需要一个对应的代理类。如果有10个Service,就需要10个代理类
  • 维护成本高:新增方法时,接口、目标类、代理类都要改
  • 只能代理特定接口:代理类是为特定接口服务的,不具备通用性

总结一句话:静态代理只适合接口方法极少、代理类数量极少的场景。企业项目中基本不会用。

四、JDK动态代理

4.1 为什么需要动态代理?

静态代理的问题在于:代理类是写死的,每个目标类都要手写一个代理类。能不能在运行时动态生成代理类?

答案是:JDK动态代理

4.2 核心原理

JDK动态代理的核心是两个类:

  • java.lang.reflect.Proxy:负责生成代理对象
  • java.lang.reflect.InvocationHandler :负责处理代理逻辑

核心流程:

  1. 调用Proxy.newProxyInstance()方法
  2. JDK在运行时动态生成一个代理类(字节码),这个类实现了你指定的接口
  3. 所有接口方法的调用,都会被转发到InvocationHandler.invoke()方法
  4. invoke()方法中,你可以自由决定是否调用目标方法,以及在调用前后做什么

4.3 代码示例

java 复制代码
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.LocalDateTime;

/**
 * JDK动态代理的调用处理器
 * 
 * 核心思想:
 * 不再为每个接口手写代理类,而是通过一个通用的 InvocationHandler
 * 来处理所有接口方法的调用
 */
public class LogInvocationHandler implements InvocationHandler {
    
    // 持有目标对象
    private final Object target;
    
    public LogInvocationHandler(Object target) {
        this.target = target;
    }
    
    /**
     * 代理对象的所有方法调用都会走到这里
     * 
     * @param proxy  代理对象本身(注意:不要在这个方法里调用proxy的方法,会死循环)
     * @param method 当前被调用的方法
     * @param args   方法参数
     * @return 方法执行结果
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 方法名,方便日志输出
        String methodName = method.getName();
        
        // 前置增强
        System.out.println("【JDK动态代理】前置日志 - 开始执行 " + methodName 
                + ",时间: " + LocalDateTime.now());
        
        Object result = null;
        try {
            // 调用目标对象的实际方法
            // 这里用了反射,method.invoke(target, args)
            result = method.invoke(target, args);
            
            // 后置增强
            System.out.println("【JDK动态代理】后置日志 - " + methodName 
                    + " 执行成功,时间: " + LocalDateTime.now());
        } catch (Exception e) {
            // 异常增强
            System.out.println("【JDK动态代理】异常日志 - " + methodName 
                    + " 执行失败,异常: " + e.getCause().getMessage());
            throw e;
        }
        
        return result;
    }
    
    /**
     * 创建代理对象的工厂方法
     * 
     * @param target 目标对象
     * @return 代理对象
     */
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target) {
        // 三个参数:
        // 1. 类加载器:用来加载动态生成的代理类
        // 2. 接口数组:代理类需要实现的接口
        // 3. InvocationHandler:方法调用处理器
        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogInvocationHandler(target)
        );
    }
}

客户端调用:

csharp 复制代码
/**
 * 客户端调用JDK动态代理
 */
public class JdkProxyClient {
    public static void main(String[] args) {
        // 1. 创建真实对象
        UserService realService = new UserServiceImpl();
        
        // 2. 通过工厂方法创建代理对象
        UserService proxy = LogInvocationHandler.createProxy(realService);
        
        // 3. 调用代理对象的方法
        proxy.saveUser("U002", "李四");
        
        System.out.println("--- 分割线 ---");
        
        String user = proxy.getUser("U002");
        System.out.println("查询结果: " + user);
        
        // 验证代理对象的类型
        System.out.println("--- 类型信息 ---");
        System.out.println("代理对象类型: " + proxy.getClass().getName());
        System.out.println("是否是代理对象: " + Proxy.isProxyClass(proxy.getClass()));
    }
}

运行结果:

ini 复制代码
【JDK动态代理】前置日志 - 开始执行 saveUser,时间: 2025-06-15T10:35:22.456
【业务逻辑】保存用户: id=U002, name=李四
【JDK动态代理】后置日志 - saveUser 执行成功,时间: 2025-06-15T10:35:22.458
--- 分割线 ---
【JDK动态代理】前置日志 - 开始执行 getUser,时间: 2025-06-15T10:35:22.458
【业务逻辑】查询用户: id=U002
【JDK动态代理】后置日志 - getUser 执行成功,时间: 2025-06-15T10:35:22.458
查询结果: User{id='U002', name='李四'}
--- 类型信息 ---
代理对象类型: com.sun.proxy.$Proxy0
是否是代理对象: true

4.4 源码级分析:JDK动态代理到底做了什么?

关键问题来了:Proxy.newProxyInstance() 到底生成了什么?

我们可以通过设置系统属性来保存生成的代理类字节码,然后反编译看看:

erlang 复制代码
// 在main方法开头设置,保存JDK动态代理生成的类到磁盘
System
.
setProperty
(
"sun.misc.ProxyGenerator.saveGeneratedFiles"
,
"true"
)
;
// JDK 8+ 使用:
// System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

反编译后看到的代理类大致长这样(简化版):

typescript 复制代码
/**
 * 这是JDK在运行时动态生成的代理类
 * 注意:这个类在编译期不存在,是在运行时由ProxyGenerator生成的字节码
 */
public final class $Proxy0 extends Proxy implements UserService {
    
    // 缓存Method对象,避免每次调用都通过反射获取
    private static Method m1;  // equals
    private static Method m2;  // toString
    private static Method m3;  // saveUser
    private static Method m4;  // getUser
    private static Method m0;  // hashCode
    
    // 构造方法:接收InvocationHandler
    public $Proxy0(InvocationHandler h) {
        super(h);  // 调用Proxy的构造方法,保存h
    }
    
    @Override
    public final void saveUser(String userId, String userName) {
        try {
            // 核心逻辑:调用InvocationHandler的invoke方法
            // 把当前方法(m3)和参数传进去
            super.h.invoke(this, m3, new Object[]{userId, userName});
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
    
    @Override
    public final String getUser(String userId) {
        try {
            return (String) super.h.invoke(this, m4, new Object[]{userId});
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
}

关键点就一个: 代理类$Proxy0继承自java.lang.reflect.Proxy,实现了UserService接口。所有接口方法的实现,都是调用super.h.invoke(),也就是InvocationHandler.invoke()

4.5 JDK动态代理的优缺点

优点:

  • 无需手写代理类 :运行时动态生成,一个InvocationHandler可以代理任意接口
  • 代码复用性 :增强逻辑集中在invoke()方法中
  • 接口驱动:符合面向接口编程的思想

缺点:

  • 必须实现接口:JDK动态代理要求目标类必须实现至少一个接口。如果目标类没有实现任何接口,就无法使用
  • 性能开销:反射调用有一定的性能损耗(虽然JDK 8+已经做了大量优化)

五、CGLIB代理

5.1 为什么需要CGLIB?

JDK动态代理有一个硬伤:目标类必须实现接口

但在实际项目中,并不是所有类都实现了接口。比如:

typescript 复制代码
// 没有实现任何接口的类
public class UserService {
    public void saveUser(String userId, String userName) {
        System.out.println("保存用户: " + userId);
    }
}

这种类怎么办?用不了JDK动态代理。

这时候CGLIB就派上用场了。

5.2 核心原理

CGLIB(Code Generation Library)的代理原理和JDK动态代理完全不同:

  • JDK动态代理:基于接口,生成一个实现了该接口的代理类
  • CGLIB代理:基于继承,生成目标类的子类作为代理类

CGLIB的核心机制:

  1. 通过ASM字节码框架,动态生成目标类的子类
  2. 子类重写所有非final的方法
  3. 在方法中通过MethodInterceptor来拦截调用
  4. 调用父类(目标类)的方法,并在前后做增强

5.3 代码示例

首先添加Maven依赖:

xml 复制代码
<!-- CGLIB 依赖 -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

目标类(没有实现接口):

typescript 复制代码
/**
 * 目标类:没有实现任何接口
 * 这就是JDK动态代理无法处理的情况
 */
public class UserServiceNoInterface {
    
    public void saveUser(String userId, String userName) {
        System.out.println("【业务逻辑】保存用户: id=" + userId + ", name=" + userName);
    }
    
    public String getUser(String userId) {
        System.out.println("【业务逻辑】查询用户: id=" + userId);
        return "User{id='" + userId + "', name='王五'}";
    }
    
    // final方法:CGLIB无法重写,也就无法代理
    public final void cannotProxy() {
        System.out.println("这个方法是final的,CGLIB无法代理");
    }
}

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;
import java.time.LocalDateTime;

/**
 * CGLIB代理实现
 * 
 * 核心思路:
 * 1. 通过Enhancer创建目标类的子类
 * 2. 子类重写目标类的所有非final方法
 * 3. 通过MethodInterceptor拦截方法调用
 * 4. 调用父类方法(目标类方法)前后做增强
 */
public class LogMethodInterceptor implements MethodInterceptor {
    
    /**
     * 拦截目标类的方法调用
     * 
     * @param obj    代理对象本身(目标类的子类)
     * @param method 被拦截的方法(父类的方法)
     * @param args   方法参数
     * @param proxy  方法代理对象,用于调用父类的方法
     * @return 方法执行结果
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) 
            throws Throwable {
        
        String methodName = method.getName();
        
        // 前置增强
        System.out.println("【CGLIB代理】前置日志 - 开始执行 " + methodName 
                + ",时间: " + LocalDateTime.now());
        
        Object result = null;
        try {
            // 注意:这里调用的是 proxy.invokeSuper(),而不是 method.invoke()
            // invokeSuper 会调用父类(目标类)的方法,而不是代理类的方法
            // 如果这里用 method.invoke(obj, args),obj是代理对象,会死循环
            result = proxy.invokeSuper(obj, args);
            
            // 后置增强
            System.out.println("【CGLIB代理】后置日志 - " + methodName 
                    + " 执行成功,时间: " + LocalDateTime.now());
        } catch (Exception e) {
            System.out.println("【CGLIB代理】异常日志 - " + methodName 
                    + " 执行失败,异常: " + e.getCause().getMessage());
            throw e;
        }
        
        return result;
    }
    
    /**
     * 创建CGLIB代理对象
     */
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(Class<T> targetClass) {
        // 1. 创建Enhancer对象(CGLIB的核心类)
        Enhancer enhancer = new Enhancer();
        
        // 2. 设置父类(目标类)
        enhancer.setSuperclass(targetClass);
        
        // 3. 设置方法拦截器
        enhancer.setCallback(new LogMethodInterceptor());
        
        // 4. 创建代理对象(目标类的子类)
        return (T) enhancer.create();
    }
}

客户端调用:

csharp 复制代码
/**
 * 客户端调用CGLIB代理
 */
public class CglibProxyClient {
    public static void main(String[] args) {
        // 1. 创建CGLIB代理对象
        UserServiceNoInterface proxy = LogMethodInterceptor.createProxy(
                UserServiceNoInterface.class);
        
        // 2. 调用代理对象的方法
        proxy.saveUser("U003", "王五");
        
        System.out.println("--- 分割线 ---");
        
        String user = proxy.getUser("U003");
        System.out.println("查询结果: " + user);
        
        System.out.println("--- 类型信息 ---");
        System.out.println("代理对象类型: " + proxy.getClass().getName());
        System.out.println("代理对象的父类: " + proxy.getClass().getSuperclass().getName());
        
        // 验证:代理对象是目标类的子类
        System.out.println("是否是目标类的实例: " + (proxy instanceof UserServiceNoInterface));
    }
}

运行结果:

ini 复制代码
【CGLIB代理】前置日志 - 开始执行 saveUser,时间: 2025-06-15T10:40:33.789
【业务逻辑】保存用户: id=U003, name=王五
【CGLIB代理】后置日志 - saveUser 执行成功,时间: 2025-06-15T10:40:33.791
--- 分割线 ---
【CGLIB代理】前置日志 - 开始执行 getUser,时间: 2025-06-15T10:40:33.791
【业务逻辑】查询用户: id=U003
【CGLIB代理】后置日志 - getUser 执行成功,时间: 2025-06-15T10:40:33.791
查询结果: User{id='U003', name='王五'}
--- 类型信息 ---
代理对象类型: com.example.proxy.UserServiceNoInterface$$EnhancerByCGLIB$$a1b2c3d4
代理对象的父类: com.example.proxy.UserServiceNoInterface
是否是目标类的实例: true

5.4 CGLIB的注意事项

第一:final方法无法被代理

CGLIB基于继承,所以凡是不能被继承、不能被重写的方法,都无法被代理。包括:

  • final 方法
  • static 方法
  • private 方法

第二:invokeSuperinvoke 的区别

这是最容易踩坑的地方:

scss 复制代码
// 正确用法:调用父类(目标类)的方法
proxy.invokeSuper(obj, args);

// 错误用法:在代理对象上调用方法,会导致死循环
// method.invoke(obj, args);  // obj是代理对象,会再次进入intercept()方法

第三:CGLIB生成的是子类

从输出可以看到,代理类名是 UserServiceNoInterface$$EnhancerByCGLIB$$a1b2c3d4,继承了 UserServiceNoInterface。这意味着:

  • proxy instanceof UserServiceNoInterfacetrue
  • 目标类如果有 public 字段,子类可以直接访问

5.5 JDK 8+ 对CGLIB的替代

从JDK 16开始,Java提供了 java.lang.reflect.InvocationHandler 的一个变体方案,但真正值得注意的是 Spring已经可以不依赖CGLIB了

Spring 4.0+ 自带了 org.springframework.cglib 包,把CGLIB的核心代码直接内嵌了,所以使用Spring的项目不需要额外引入CGLIB依赖。

六、三种代理方式对比

6.1 核心差异对比

对比维度 静态代理 JDK动态代理 CGLIB代理
实现原理 手写代理类,实现相同接口 运行时生成代理类,实现目标接口 运行时生成目标类的子类
是否需要接口
代理类生成时机 编译期 运行时 运行时
代理类与目标类关系 平级(实现相同接口) 平级(实现相同接口) 父子关系(继承)
能否代理final方法 能(代理类本身有该方法) 能(接口方法没有final限制) 不能(final方法不可重写)
调用方式 直接调用目标对象方法 通过反射调用 通过FastClass机制调用
代码维护 困难(类爆炸) 容易(一个Handler代理所有) 容易(一个Interceptor代理所有)
性能(首次) 最快(无额外开销) 较慢(需要生成代理类) 较慢(需要生成字节码)
性能(调用) 最快 较慢(反射调用) 快(FastClass,接近原生)

6.2 性能说明

很多人以为CGLIB比JDK动态代理慢,其实恰恰相反:

  • JDK动态代理 :方法调用走的是反射,Method.invoke() 有额外的开销
  • CGLIB代理:使用了FastClass机制,通过索引直接调用方法,性能接近原生调用

JDK 8+ 对反射做了大量优化,但即便如此,CGLIB的调用性能仍然优于JDK动态代理。

不过在实际项目中,这个性能差异通常可以忽略不计。真正影响性能的是代理对象的创建过程,而不是方法调用过程。

6.3 深入理解:CGLIB的FastClass机制

CGLIB之所以调用性能好,是因为它内部使用了 FastClass 机制。

简单来说,FastClass为一个类生成两个索引表:

  • 一个索引表:根据方法签名,给方法编号
  • 另一个索引表:根据方法编号,直接跳转到方法实现

调用时,先通过方法签名找到编号,然后通过编号直接调用。本质上是一个空间换时间的策略,避免了反射调用。

七、Spring AOP中的代理机制

7.1 Spring AOP的代理选择策略

这是面试高频题,也是很多人在实际开发中踩坑的地方。

Spring AOP的代理选择逻辑在 DefaultAopProxyFactory 中,源码如下:

arduino 复制代码
// 来自 Spring 源码:org.springframework.aop.framework.DefaultAopProxyFactory
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        // 核心判断逻辑
        if (!NativeDetector.inNativeImage()
                && (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("...");
            }
            // 如果目标类本身是接口,或者目标类是JDK代理类,则使用JDK动态代理
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            // 否则使用CGLIB代理
            return new ObjenesisCglibAopProxy(config);
        } else {
            // 默认使用JDK动态代理
            return new JdkDynamicAopProxy(config);
        }
    }
    
    /**
     * 判断是否没有用户自定义的代理接口
     */
    private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class<?>[] interfaces = config.getProxiedInterfaces();
        return interfaces.length == 0 || (interfaces.length == 1 
                && SpringProxy.class.isAssignableFrom(interfaces[0]));
    }
}

7.2 选择逻辑总结

Spring选择JDK代理还是CGLIB代理的逻辑如下:

arduino 复制代码
是否满足以下任一条件?
  1. config.isOptimize() == true       (优化标志)
  2. config.isProxyTargetClass() == true (proxyTargetClass=true)
  3. 没有用户自定义的代理接口
  ┌───────── 是 ─────────┐         ┌──── 否 ────┐
  ↓                       ↓         ↓              
目标类是否是接口或         ↓   使用JDK动态代理
JDK代理类?               ↓
┌─ 是 → JDK动态代理        ↓
└─ 否 → CGLIB代理          ↓

7.3 如何在Spring中配置

方式一:全局配置(XML方式)

xml 复制代码
<!-- 强制使用CGLIB代理 -->
<aop:config proxy-target-class="true">
<aop:aspect ref="logAspect">
<aop:pointcut id="servicePointcut" 
            expression="execution(* com.example.service.*.*(..))"/>
<aop:around pointcut-ref="servicePointcut" method="logAround"/>
</aop:aspect>
</aop:config>

方式二:全局配置(注解方式)

less 复制代码
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)  // 强制使用CGLIB
public class AppConfig {
    // ...
}

方式三:Spring Boot中的配置

ini 复制代码
# application.properties
spring.aop.proxy-target-class=true

7.4 经典踩坑:事务注解失效

这是我在项目中被问得最多的问题。来看一个典型的场景:

java 复制代码
// 接口
public interface UserService {
    void saveUser(User user);
}

// 实现类
@Service
public class UserServiceImpl implements UserService {

    @Override
    @Transactional  // 事务注解加在实现类方法上
    public void saveUser(User user) {
        // 保存用户...
        saveLog(user);  // 调用本类的另一个方法
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(User user) {
        // 保存日志...
    }
}

问题: saveLog() 的事务不会生效。

原因分析:

  1. Spring默认配置下,如果 UserServiceImpl 实现了接口,优先使用JDK动态代理
  2. JDK动态代理生成的代理对象,只代理接口中声明的方法
  3. saveLog() 不是接口方法,所以不会被代理
  4. 更关键的是,saveUser() 内部调用 saveLog() 是直接调用 this.saveLog(),这里的 this 是目标对象本身,不是代理对象

解决方案:

typescript 复制代码
// 方案一:将事务方法提取到接口中
public interface UserService {
    void saveUser(User user);
    void saveLog(User user);  // 在接口中声明
}

// 方案二:强制使用CGLIB代理
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// 方案三:通过AopContext获取当前代理对象
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserService self;  // 注入自己(代理对象)
    
    @Override
    @Transactional
    public void saveUser(User user) {
        // ...
        self.saveLog(user);  // 通过代理对象调用
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(User user) {
        // ...
    }
}

八、实际应用场景

8.1 声明式事务管理

这是代理模式在企业应用中最经典的应用。Spring的 @Transactional 注解,底层就是通过AOP代理来实现的。

scss 复制代码
@Service
public class OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO order) {
        // 1. 保存订单(代理对象会在方法执行前开启事务)
        orderDao.insert(order);
        
        // 2. 扣减库存
        stockService.deduct(order.getProductId(), order.getQuantity());
        
        // 3. 发送消息(代理对象会在方法执行后提交事务)
        // 如果上面任何一步报错,代理对象会自动回滚事务
        mqService.sendOrderMessage(order);
    }
}

8.2 日志记录

统一的日志记录,不用在每个方法里手动写 log.info()

java 复制代码
@Aspect
@Component
public class LogAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        // 前置日志
        log.info("调用方法: {},参数: {}", methodName, args);
        
        long start = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            
            // 后置日志
            long elapsed = System.currentTimeMillis() - start;
            log.info("方法执行成功: {},耗时: {}ms,结果: {}", methodName, elapsed, result);
            
            return result;
        } catch (Exception e) {
            log.error("方法执行异常: {},异常: {}", methodName, e.getMessage());
            throw e;
        }
    }
}

8.3 权限控制

在方法执行前检查当前用户是否有权限。

java 复制代码
@Aspect
@Component
public class PermissionAspect {
    
    @Around("@annotation(permission)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, 
                                   Permission permission) throws Throwable {
        
        String requiredRole = permission.value();
        String currentUserRole = SecurityContextHolder.getContext()
                .getAuthentication().getAuthorities().iterator().next().getAuthority();
        
        // 检查权限
        if (!currentUserRole.equals(requiredRole)) {
            throw new AccessDeniedException("权限不足,需要: " + requiredRole);
        }
        
        return joinPoint.proceed();
    }
}

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
    String value();  // 需要的角色
}

// 使用
@RestController
public class UserController {
    
    @Permission("ADMIN")
    @GetMapping("/admin/users")
    public List<User> listUsers() {
        return userService.listAll();
    }
}

8.4 缓存

通过代理在方法执行前检查缓存,如果缓存命中直接返回,避免执行方法。

java 复制代码
@Aspect
@Component
public class CacheAspect {
    
    private final CacheManager cacheManager;
    
    @Around("@annotation(cacheable)")
    public Object aroundCache(ProceedingJoinPoint joinPoint, 
                               Cacheable cacheable) throws Throwable {
        
        String cacheName = cacheable.value();
        String key = generateKey(joinPoint);
        
        // 先查缓存
        Cache cache = cacheManager.getCache(cacheName);
        Cache.ValueWrapper wrapper = cache.get(key);
        if (wrapper != null) {
            log.info("缓存命中: {}:{}", cacheName, key);
            return wrapper.get();
        }
        
        // 缓存未命中,执行方法
        Object result = joinPoint.proceed();
        
        // 放入缓存
        cache.put(key, result);
        log.info("缓存写入: {}:{}", cacheName, key);
        
        return result;
    }
}

8.5 性能监控

统计每个方法的调用次数和耗时,用于监控告警。

java 复制代码
@Aspect
@Component
public class MonitorAspect {
    
    private final MeterRegistry meterRegistry;
    
    @Around("@annotation(monitored)")
    public Object monitor(ProceedingJoinPoint joinPoint, 
                           Monitored monitored) throws Throwable {
        
        String methodName = joinPoint.getSignature().toShortString();
        
        // 调用次数计数
        Counter.builder("method.calls")
                .tag("method", methodName)
                .register(meterRegistry)
                .increment();
        
        long start = System.nanoTime();
        try {
            return joinPoint.proceed();
        } finally {
            long duration = System.nanoTime() - start;
            
            // 记录耗时
            Timer.builder("method.duration")
                    .tag("method", methodName)
                    .register(meterRegistry)
                    .record(duration, TimeUnit.NANOSECONDS);
            
            // 如果耗时超过阈值,告警
            if (duration > monitored.threshold().toNanos()) {
                log.warn("方法执行超时: {},耗时: {}ms", 
                        methodName, duration / 1_000_000);
            }
        }
    }
}

九、总结

代理模式从设计模式上来说不难,但它在Java生态中的地位极其重要。Spring AOP、事务管理、MyBatis Mapper接口、RPC框架,底层全部依赖代理机制。

三种代理方式的选择建议:

  1. 静态代理:基本不用。除非你的接口极其稳定且方法极少,否则别碰。
  2. JDK动态代理:优先选择。如果你的代码遵循"面向接口编程"的原则,JDK动态代理足够用,而且不需要额外依赖。
  3. CGLIB代理:当目标类没有实现接口,或者需要代理非接口方法时使用。Spring Boot 2.0+ 默认使用CGLIB,因为大部分场景下我们不需要额外的接口层。

门主踩坑总结:

  • 事务注解加在 private 方法上?不生效。因为代理子类无法重写 private 方法。
  • 同一个类中方法A调用方法B,方法B的 @Transactional 不生效?因为 this 指向的是目标对象,不是代理对象。
  • 接口方法上加了 @Transactional,但实现类实现了接口?如果Spring发现接口上有事务注解,会优先使用JDK动态代理。如果实现类方法上也有 @Transactional,事务注解会被覆盖。

三连支持,动力源泉

如果这篇文章帮你省下了踩坑的时间,欢迎:

点赞 ------ 让更多人看到这篇干货

在看 ------ 你的认可是我持续输出的动力

转发 ------ 分享给身边正在做AI Agent的朋友

你的每一个小动作,对我都很重要 ❤️


关于作者

你好,我是 空门技术栈 ,一个常年和Bug战斗、持续填坑的Java开发者

专注分享:

  • ✅ Java / Spring Boot / Spring AI Alibaba 企业级实战
  • ✅ RAG知识库、AI Agent、多智能体协作落地经验
  • ✅ Docker部署、微服务架构、线上问题排查
  • ✅ 偶尔聊聊「如何保住头发」这类程序员终极话题

不搞水文,不贩卖焦虑,只写能跑通、能落地、能帮你少加班的实战内容。

关注我,咱们一起少踩坑,多写优雅代码。


更多干货推荐


粉丝福利

本文涉及的 完整源码 + 可运行Demo + 配置示例,已打包整理好。

免费获取方式

私信空门技术栈,领取源码包。

有任何问题,也欢迎后台私信,门主看到都会回复~

对文章内容有疑问?想看下一篇写什么主题?

直接在评论区留言,你的每一条评论我都会看。说不定下一篇文章的主题,就来自你的问题!


项目合作 / 技术咨询

平时工作之余,也会接一些技术项目和咨询,主要方向:

⚔️ 企业级开发

AI 应用落地(这是我最近的主力方向)

  • Spring AI Alibaba / RAG / Agent 应用开发
  • 企业私有知识库搭建
  • AI能力接入现有业务系统
  • 大模型本地化部署与调优

技术顾问 / 疑难Bug排查

  • 项目架构评审与方案设计
  • 线上疑难问题定位解决
  • 技术选型与团队培训

如果你正遇到以下情况,欢迎找我聊聊:

  • ✅ 想做AI项目,但技术方案拿不准
  • ✅ 项目卡在某个Bug上很久,团队搞不定
  • ✅ 想把AI接入现有业务,不知道从哪下手
  • ✅ 需要靠谱的开发外包或长期技术顾问
相关推荐
爱勇宝16 小时前
AI不会淘汰所有人,但会淘汰这6种人
前端·后端·程序员
Conan在掘金17 小时前
鸿蒙 ArkUI 进阶:@Extend 和 @Styles,把样式也抽成「乐高块」的正确姿势
后端
orient17 小时前
为什么需要并发编程
后端
Cosolar17 小时前
AI Agent 架构原理详解:从一次提问到任务完成的完整闭环
人工智能·后端·架构
Conan在掘金17 小时前
鸿蒙 ArkUI 深水区:Stage 模型多维状态,从「组件内」跳到「应用级」的分水岭
后端
长栎18 小时前
用AI优化Redis缓存失效,我把热key问题想简单了
后端
面试鸭18 小时前
我说我们做 Agent 从来不怕死循环,面试官调出兜底日志:“第 47 次强制终止,是你半夜爬起来按停的?”
后端·面试·求职
小小猪的春天18 小时前
团队级 Code Review Skill 实践:4个人、4个AI、1套规则,CR从找bug变成做决策
后端·架构
长栎18 小时前
AI帮我写了个Spring Boot校验,线上漏掉了这组边界条件
后端