摘要

代理模式是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方法调用都要记录日志。

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

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

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

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

三、静态代理

3.1 实现方式

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

3.2 代码示例

先定义一个接口:

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

真实业务类:

/**
 * 真实的用户服务实现类
 * 这里只关心业务逻辑,不关心日志、事务等横切关注点
 */
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='张三'}";
    }
}

静态代理类:

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;
    }
}

客户端调用:

/**
 * 客户端调用
 * 对调用方来说,完全感知不到代理的存在
 * 它只知道自己拿到的是一个 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);
    }
}

运行结果:

【代理】前置日志 - 开始执行 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 代码示例

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)
        );
    }
}

客户端调用:

/**
 * 客户端调用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()));
    }
}

运行结果:

【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() 到底生成了什么?

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

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

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

/**
 * 这是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动态代理有一个硬伤:目标类必须实现接口。

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

// 没有实现任何接口的类
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依赖:

<!-- CGLIB 依赖 -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

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

/**
 * 目标类:没有实现任何接口
 * 这就是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代理实现:

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();
    }
}

客户端调用:

/**
 * 客户端调用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));
    }
}

运行结果:

【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 方法

第二:invokeSuper 和 invoke 的区别

这是最容易踩坑的地方:

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

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

第三:CGLIB生成的是子类

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

  • proxy instanceof UserServiceNoInterface 为 true
  • 目标类如果有 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 中,源码如下:

// 来自 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代理的逻辑如下:

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

7.3 如何在Spring中配置

方式一:全局配置(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>

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

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

方式三:Spring Boot中的配置

# application.properties
spring.aop.proxy-target-class=true

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

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

// 接口
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 是目标对象本身,不是代理对象

解决方案:

// 方案一:将事务方法提取到接口中
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代理来实现的。

@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()。

@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 权限控制

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

@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 缓存

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

@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 性能监控

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

@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接入现有业务,不知道从哪下手
  • ✅ 需要靠谱的开发外包或长期技术顾问
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐