Spring框架入门:代理模式详解

引言

在深入学习 Spring 框架的过程中,你一定会频繁接触到"代理"(Proxy)这个概念。无论是 AOP(面向切面编程)事务管理缓存 ,还是 作用域 Bean 的注入 (如 @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)),其底层实现都离不开代理模式。

然而,很多初学者对代理的理解停留在"Spring 会自动创建代理对象"的模糊层面,不清楚:

  • 什么是代理模式?
  • Spring 为何需要代理?
  • JDK 动态代理和 CGLIB 代理有何区别?
  • 什么时候用哪种代理?
  • 代理会带来哪些性能开销或陷阱?

本文将从设计模式基础出发,系统性地讲解 代理模式在 Spring 中的应用,涵盖以下内容:

  1. 代理模式的基本概念与分类
  2. JDK 动态代理原理与实战
  3. CGLIB 代理原理与实战
  4. Spring 如何自动选择代理策略
  5. 代理在 AOP、事务、Scope 中的具体应用
  6. 常见问题与最佳实践
  7. Spring Boot 中的代理行为差异

无论你是刚接触 Spring 的新手,还是希望深入理解其底层机制的开发者,本文都将为你揭开 Spring 代理的神秘面纱。


第一章:代理模式的基本概念

1.1 什么是代理模式?

代理模式(Proxy Pattern) 是 GoF 23 种设计模式之一,属于结构型模式 。其核心思想是:为其他对象提供一种代理以控制对这个对象的访问

通俗理解:代理就像"中介"或"替身"。你不能直接接触目标对象,而是通过代理来间接操作它,并在过程中加入额外逻辑(如权限检查、日志记录、延迟加载等)。

1.2 代理模式的通用结构

复制代码
// 接口
public interface Subject {
    void request();
}

// 真实主题(被代理对象)
public class RealSubject implements Subject {
    public void request() {
        System.out.println("真实业务执行");
    }
}

// 代理类
public class Proxy implements Subject {
    private RealSubject realSubject;

    public void request() {
        // 前置增强
        System.out.println("权限检查...");
        
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        realSubject.request();
        
        // 后置增强
        System.out.println("记录日志...");
    }
}

调用方使用 Proxy 而非 RealSubject,从而在不修改原逻辑的前提下增强功能。

1.3 代理的分类(按创建时机)

类型 说明 示例
静态代理 编译时手动编写代理类 上述代码
动态代理 运行时自动生成代理类 JDK Proxy、CGLIB

Spring 主要使用动态代理,因为它更灵活、可复用。


第二章:JDK 动态代理

2.1 原理

JDK 动态代理基于 Java 反射机制 ,要求被代理类必须实现至少一个接口 。代理对象在运行时通过 Proxy.newProxyInstance() 动态生成,其实现了相同的接口,并将方法调用委托给 InvocationHandler

2.2 核心 API

  • java.lang.reflect.Proxy
  • java.lang.reflect.InvocationHandler

2.3 实战示例

复制代码
// 1. 定义接口
public interface UserService {
    void saveUser(String name);
}

// 2. 实现类
public class UserServiceImpl implements UserService {
    public void saveUser(String name) {
        System.out.println("保存用户: " + name);
    }
}

// 3. 实现代理处理器
public class LoggingHandler implements InvocationHandler {
    private Object target;

    public LoggingHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("【前置】调用方法: " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("【后置】方法执行完毕");
        return result;
    }
}

// 4. 创建代理
public class Main {
    public static void main(String[] args) {
        UserService target = new UserServiceImpl();
        UserService proxy = (UserService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new LoggingHandler(target)
        );
        proxy.saveUser("张三");
    }
}

输出

复制代码
【前置】调用方法: saveUser
保存用户: 张三
【后置】方法执行完毕

2.4 特点

优点

  • JDK 内置,无需额外依赖。
  • 生成的代理类是 $Proxy0 这样的匿名类,干净简洁。

缺点

  • 只能代理实现了接口的类
  • 如果目标类没有接口,JDK 代理无法使用。

第三章:CGLIB 动态代理

3.1 原理

CGLIB(Code Generation Library)是一个字节码生成库 ,它通过继承目标类并重写其方法来实现代理。因此,不要求目标类实现接口

CGLIB 底层使用 ASM 字节码操作框架,在运行时动态生成子类。

3.2 添加依赖

复制代码
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

注意:Spring Boot 项目中通常已间接引入(通过 spring-core)。

3.3 实战示例

复制代码
// 1. 目标类(无接口!)
public class OrderService {
    public void createOrder(String orderId) {
        System.out.println("创建订单: " + orderId);
    }
}

// 2. 实现代理回调
public class OrderMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("【CGLIB 前置】" + method.getName());
        Object result = proxy.invokeSuper(obj, args); // 调用父类方法
        System.out.println("【CGLIB 后置】");
        return result;
    }
}

// 3. 创建代理
public class Main {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(OrderService.class);
        enhancer.setCallback(new OrderMethodInterceptor());
        
        OrderService proxy = (OrderService) enhancer.create();
        proxy.createOrder("1001");
    }
}

输出

复制代码
【CGLIB 前置】createOrder
创建订单: 1001
【CGLIB 后置】

3.4 特点

优点

  • 可代理任意类 (只要不是 final)。
  • 性能略优于 JDK 代理(尤其在大量方法调用时)。

缺点

  • 无法代理 final 类或 final 方法(因为不能被继承/重写)。
  • 生成的代理类是目标类的子类,可能引发类型判断问题(如 instanceof)。

第四章:Spring 如何选择代理策略?

Spring 在启用 AOP 或需要代理时,会自动选择最合适的代理方式。其决策逻辑如下:

4.1 默认策略(Spring 5+)

条件 使用的代理
目标类实现了接口 优先使用 JDK 动态代理
目标类未实现接口 强制使用 CGLIB 代理

这是 Spring 的默认行为,符合"面向接口编程"的最佳实践。

4.2 强制使用 CGLIB

可通过配置强制所有 Bean 使用 CGLIB:

XML 方式:
复制代码
<aop:config proxy-target-class="true"/>
注解方式(Spring Boot):
复制代码
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Configuration
public class AopConfig {}

建议:除非有特殊需求(如代理无接口的类),否则保持默认即可。

4.3 查看实际使用的代理类型

复制代码
@Autowired
private UserService userService;

@Test
public void testProxyType() {
    System.out.println(userService.getClass()); 
    // 输出:class com.sun.proxy.$Proxy42 (JDK 代理)
    // 或:class com.example.UserServiceImpl$$EnhancerBySpringCGLIB$$1a2b3c (CGLIB 代理)
}

第五章:代理在 Spring 核心功能中的应用

5.1 AOP(面向切面编程)

AOP 是代理最典型的应用。例如:

复制代码
@Aspect
@Component
public class LogAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("方法调用前: " + joinPoint.getSignature());
    }
}

Spring 会为目标 Service 类创建代理,所有方法调用都会经过切面逻辑。

注意只有通过代理对象调用的方法才会被拦截

自调用(this.method())不会触发 AOP!

复制代码
@Service
public class OrderService {
    public void placeOrder() {
        this.validate(); // ❌ 不会触发 AOP!
        validate();      // ❌ 同上
    }

    @Transactional
    public void validate() {
        // ...
    }
}

解决方案

  • 注入自身(不推荐,易循环依赖);
  • 使用 AopContext.currentProxy()(需开启 exposeProxy = true);
  • 重构代码,避免自调用。

5.2 声明式事务(@Transactional)

@Transactional 本质上是一个 AOP 切面。Spring 会为标注该注解的类创建事务代理:

复制代码
@Service
public class AccountService {
    @Transactional
    public void transfer(String from, String to, BigDecimal amount) {
        // 数据库操作
    }
}
  • AccountService 实现了接口 → JDK 代理;
  • 否则 → CGLIB 代理。

⚠️ 常见错误 :在非 public 方法上使用 @Transactional(JDK 代理无法拦截 protected/private 方法)。


5.3 作用域代理(Scoped Proxy)

当 singleton Bean 需要注入 request/session 作用域的 Bean 时,Spring 会创建代理:

复制代码
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String userId;
    // ...
}

@Service
public class UserService {
    @Autowired
    private RequestContext requestContext; // 实际是代理对象
}
  • proxyMode = TARGET_CLASS → 使用 CGLIB 代理;
  • proxyMode = INTERFACES → 使用 JDK 代理(要求 RequestContext 实现接口)。

每次调用 requestContext.getUserId() 时,代理会从当前请求上下文中获取真实的实例。


第六章:常见问题与最佳实践

6.1 问题一:代理导致的类型转换异常

复制代码
UserService userService = context.getBean(UserService.class);
UserServiceImpl impl = (UserServiceImpl) userService; // ❌ ClassCastException!

原因:userService$Proxy42,不是 UserServiceImpl

解决方案

  • 编程时始终面向接口
  • 避免强转具体实现类。

6.2 问题二:final 类/方法无法被代理

复制代码
@Service
public final class FinalService { // ❌ 无法被 CGLIB 代理
    public void doSomething() {}
}

或:

复制代码
@Service
public class MyService {
    public final void criticalMethod() {} // ❌ 无法被重写
}

解决方案

  • 移除 final 关键字;
  • 或改用编译时织入(AspectJ,非运行时代理)。

6.3 问题三:自调用不生效

如前所述,内部方法调用绕过代理。

最佳实践

  • 将需代理的方法拆分到另一个 Service;

  • 使用 ApplicationContext 获取代理自身(谨慎使用):

    @Service
    public class OrderService implements ApplicationContextAware {
    private ApplicationContext context;

    复制代码
      public void placeOrder() {
          OrderService self = context.getBean(OrderService.class);
          self.validate(); // ✅ 通过代理调用
      }
    
      @Transactional
      public void validate() { ... }

    }


6.4 性能考量

  • 代理本身有轻微性能开销(反射或方法重写);
  • 但在现代 JVM 下,影响微乎其微;
  • 不要因性能拒绝使用代理,其带来的解耦和可维护性远大于开销。

第七章:Spring Boot 中的代理行为

Spring Boot 默认启用了 AOP 自动配置(@EnableAspectJAutoProxy),但有几点需要注意:

7.1 默认代理策略

  • 与 Spring 相同:有接口用 JDK,无接口用 CGLIB。

7.2 开启 exposeProxy

若需在自调用中使用代理,需显式开启:

复制代码
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {}

然后通过 AopContext.currentProxy() 获取:

复制代码
public void methodA() {
    ((MyService) AopContext.currentProxy()).methodB();
}

⚠️ 此方式破坏了代码的纯洁性,仅作为最后手段。

7.3 测试中的代理

@SpringBootTest 中,注入的 Bean 已是代理对象,可直接测试 AOP 行为:

复制代码
@SpringBootTest
class TransactionalTest {

    @Autowired
    private AccountService accountService;

    @Test
    void testTransactionRollback() {
        assertThrows(Exception.class, () -> 
            accountService.transfer("A", "B", new BigDecimal("999999"))
        );
        // 验证数据库未变更(事务回滚)
    }
}

结语

代理模式是 Spring 实现非侵入式增强的核心机制。理解 JDK 动态代理与 CGLIB 的区别,掌握 Spring 的代理选择策略,不仅能帮助你正确使用 AOP、事务等功能,还能避免常见的"坑"。

记住以下关键点:

  • 优先面向接口编程,让 Spring 自然选择 JDK 代理;
  • 避免字段注入和自调用,确保代理生效;
  • 不要代理 final 类/方法
  • 代理是工具,不是魔法------理解其原理才能驾驭它。

希望本文能为你构建坚实的 Spring 代理知识体系,助你在企业级开发中游刃有余。


参考资料

  1. Spring Framework 官方文档:AOP Proxying
  2. 《Spring 实战(第6版)》
  3. CGLIB GitHub: https://github.com/cglib/cglib
  4. Java Reflection and Dynamic Proxies (Oracle Docs)
相关推荐
Rock_yzh26 分钟前
LeetCode算法刷题——53. 最大子数组和
java·数据结构·c++·算法·leetcode·职场和发展·动态规划
简单的话*26 分钟前
Logback 日志按月归档并保留 180 天,超期自动清理的配置实践
java·前端·python
m***567227 分钟前
在Nginx上配置并开启WebDAV服务的完整指南
java·运维·nginx
Mr.朱鹏34 分钟前
RocketMQ可视化监控与管理
java·spring boot·spring·spring cloud·maven·intellij-idea·rocketmq
带刺的坐椅37 分钟前
Solon AI 开发学习9 - chat - 聊天会话(对话)的记忆与持久化
java·ai·llm·openai·solon·mcp
曹牧38 分钟前
Oracle中ROW_NUMBER() OVER()
java·数据库·sql
客梦39 分钟前
数据结构-哈希表
java·数据结构·笔记
草原印象42 分钟前
Spring SpringMVC Mybatis框架整合实战
java·spring·mybatis·spring mvc
四谎真好看1 小时前
Java 黑马程序员学习笔记(进阶篇30)
java·笔记·学习·学习笔记