第六篇:Spring用了哪些设计模式?——从单例到代理,拆解框架中的经典设计

前言

在前五篇文章中,我们拆解了IoC、AOP、SpringMVC、自动配置和事务管理。每个模块内部都用到了多种设计模式。现在换个视角,从设计模式的角度重新审视Spring,你会发现这些看似独立的模块,底层共享着同样的设计智慧。

面试中,这个问题是检验你是否"理解Spring"而非只是"会用Spring"的试金石:

"Spring中用到了哪些设计模式?各在什么地方用的?"

"BeanFactory和FactoryBean有什么区别?"

"Spring的AOP是用的哪种代理模式?"

本文串联前五篇的知识,拆解Spring中最核心的六种设计模式,让你从设计的视角重新理解Spring。

本文核心问题:

  1. 单例模式:Spring的Bean默认是单例的,底层怎么保证只有一个实例?
  2. 工厂模式:BeanFactory和FactoryBean有什么区别?为什么有两个"工厂"?
  3. 代理模式:AOP是如何运用动态代理的?JDK代理和CGLIB各在什么场景下使用?
  4. 模板方法模式:JdbcTemplate、RestTemplate的设计思想是什么?
  5. 观察者模式:ApplicationEvent和ApplicationListener如何实现事件驱动?
  6. 策略模式:Spring中是如何通过不同环境加载不同配置的?
  7. 这些设计模式是如何协同工作,共同构成Spring框架的?

读完本文,你将从设计模式的视角,对前五篇文章的知识做一次完整的串联和升华。


一、单例模式------Spring的Bean默认是单例的

疑问:Spring的Bean默认是单例的,这是怎么实现的?和手写单例模式有什么区别?

回答:Spring的单例是通过"容器内唯一"来保证的------同一个ApplicationContext中,相同id的Bean只创建一次。它不依赖static、不依赖私有构造器,而是通过容器的统一管理来约束实例的唯一性。

1.1 和传统单例的区别

java 复制代码
// 传统手写单例:靠语言特性强制唯一
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}  // 私有构造器禁止外部new
    public static Singleton getInstance() { return INSTANCE; }
}

// Spring单例:靠容器管理保证唯一
@Component  // 无私有构造器限制
public class OrderService {  // 可以被new,可以创建多个实例(通过多个容器)
    // ...
}

Spring的单例不依赖语言特性,而是依赖容器的生命周期管理。 容器启动时创建所有单例Bean的实例并存入一级缓存,后续获取时从缓存中直接返回。传统单例通过私有构造器在编码层面禁止外部创建新实例,而Spring的Bean可以被new、可以被反射调用------它的唯一性由容器维护,而非语言约束。

1.2 在哪用到?

Spring管理的所有默认Bean都是单例的------Controller、Service、Repository、以及框架内部的DispatcherServlet、HandlerMapping等都是单例。这就是为什么这些组件中不能持有可变状态------单例意味着所有请求共享同一个实例,状态字段会在并发访问下产生数据错乱。


二、工厂模式------BeanFactory和FactoryBean有什么区别?

疑问:Spring中有BeanFactory和FactoryBean,它们有什么不同?为什么需要两个"工厂"?

回答:BeanFactory是Spring的IoC容器本身------它管理所有Bean的创建。FactoryBean是Bean的"定制工厂"------当一个Bean的创建逻辑比较复杂,无法用简单的new或反射完成时,FactoryBean封装了这个复杂的创建过程。

2.1 BeanFactory------容器即工厂

java 复制代码
// BeanFactory是Spring容器的顶层接口
// 它的职责是:根据beanName返回创建好的Bean实例
public interface BeanFactory {
    Object getBean(String name);
    <T> T getBean(Class<T> requiredType);
}

BeanFactory就是Spring IoC容器本身 ------它是所有Bean的统一管理入口。你调用getBean(),它从缓存中取出已经创建好的实例返回。

2.2 FactoryBean------定制Bean的创建逻辑

java 复制代码
// FactoryBean用于封装复杂Bean的创建过程
public interface FactoryBean<T> {
    T getObject() throws Exception;    // 创建Bean实例
    Class<?> getObjectType();          // Bean的类型
    boolean isSingleton();             // 是否是单例
}

// 例如:MyBatis的SqlSessionFactoryBean
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory> {
    
    @Override
    public SqlSessionFactory getObject() throws Exception {
        // 复杂的创建逻辑:解析XML、构建Configuration、创建SqlSessionFactory
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        return builder.build(this.configuration);
    }
}

占位汇总 :FactoryBean封装的是"怎么创建"的逻辑,BeanFactory管理的是"什么时候给、给哪个实例"的调度。注入的是SqlSessionFactory而不是SqlSessionFactoryBean本身。

2.3 两者本质区别

BeanFactory是容器层------管理所有Bean的生命周期,调用者是Spring框架本身。FactoryBean是Bean层------封装单个复杂Bean的创建逻辑,调用者是需要定制化创建Bean的开发者。BeanFactory用工厂方法模式统一了Bean的获取入口,FactoryBean用工厂方法模式封装了单个Bean的创建复杂度。两者的分工清晰------BeanFactory管全局,FactoryBean管局部细节。


三、代理模式------AOP的核心基石

疑问:AOP是怎么通过代理模式实现的?JDK动态代理和CGLIB各在什么场景下使用?

回答:Spring AOP通过生成代理对象,在代理中插入切面逻辑,再把调用转发给原始对象。JDK动态代理要求目标对象实现接口,CGLIB通过继承目标类生成代理子类------两者都是代理模式在运行时的不同实现。

3.1 和静态代理的区别

java 复制代码
// 静态代理:手写代理类(每代理一个类就要写一个代理)
public class OrderServiceStaticProxy implements OrderService {
    private OrderService target;
    
    public void createOrder(Order order) {
        log.info("开始");
        target.createOrder(order);  // 代理中插入日志
        log.info("结束");
    }
}

// 动态代理:运行时动态生成代理(一套逻辑代理所有类)
// JDK动态代理:
OrderService proxy = (OrderService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    (proxy, method, args) -> {
        log.info("开始");
        Object result = method.invoke(target, args);
        log.info("结束");
        return result;
    }
);

3.2 Spring AOP = 动态代理 + IoC

Spring AOP将代理的生成和Bean的生命周期融合在一起------在Bean初始化的最后阶段,通过BeanPostProcessor判断是否需要为Bean生成代理。如果需要,返回代理对象替代原始Bean。此后容器中持有的就是代理对象,所有对该Bean的调用都经过代理------切面逻辑在其中生效。

这就是为什么自调用事务失效:this调用的是原始对象,而不是容器返回的代理对象------代理的拦截链从未被触发。


四、模板方法模式------JdbcTemplate的设计思想

疑问:JdbcTemplate为什么能省掉大量重复代码?它用的是什么设计模式?

回答:JdbcTemplate用的是模板方法模式------把"获取连接→执行SQL→处理结果→关闭资源"这个固定流程定义在父类中,只留出SQL执行和结果处理两个可变部分给子类或回调实现。

4.1 没有模板方法时

java 复制代码
// 手写JDBC:每次都要重复获取连接、关闭资源
public List<User> getUsers() {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;
    try {
        conn = dataSource.getConnection();                    // ← 重复
        stmt = conn.prepareStatement("SELECT * FROM user");   // ← 可变
        rs = stmt.executeQuery();                             // ← 重复
        List<User> users = new ArrayList<>();
        while (rs.next()) {
            users.add(new User(rs.getLong("id"), rs.getString("name"))); // ← 可变
        }
        return users;
    } catch (SQLException e) {
        throw new RuntimeException(e);
    } finally {
        if (rs != null) try { rs.close(); } catch (SQLException e) {}     // ← 重复
        if (stmt != null) try { stmt.close(); } catch (SQLException e) {} // ← 重复
        if (conn != null) try { conn.close(); } catch (SQLException e) {} // ← 重复
    }
}

4.2 JdbcTemplate的模板方法

java 复制代码
// JdbcTemplate帮你管理所有重复的部分
List<User> users = jdbcTemplate.query(
    "SELECT * FROM user",              // SQL(可变部分)
    (rs, rowNum) -> new User(          // 结果映射(可变部分)
        rs.getLong("id"),
        rs.getString("name")
    )
);

固定部分被封装 :获取连接、创建PreparedStatement、执行查询、遍历ResultSet、关闭资源------全由JdbcTemplate内部管理。可变部分由开发者传入:SQL语句和结果映射逻辑。开发者只需要关心"查什么"和"结果怎么映射",不需要写任何连接和资源管理代码。

4.3 还有哪些用了模板方法?

组件 固定流程 可变部分(回调)
JdbcTemplate 获取连接→执行→处理结果→关闭 SQL语句、结果集映射
RestTemplate 建立HTTP连接→发送请求→接收响应→关闭 URL、请求方法、响应体映射
TransactionTemplate 开启事务→执行业务→提交/回滚→清理 业务逻辑
RedisTemplate 获取连接→序列化→执行命令→反序列化→关闭 Key、Value、Redis命令

五、观察者模式------ApplicationEvent和ApplicationListener

疑问:Spring的事件机制是干什么的?用的是哪种设计模式?

回答:Spring的事件机制用的是观察者模式------Bean可以发布事件,监听该事件的Listener在事件发布时自动被调用。这是Bean之间解耦通信的重要手段。

5.1 典型使用场景

java 复制代码
// 1. 定义事件
public class OrderCreatedEvent extends ApplicationEvent {
    private Order order;
    public OrderCreatedEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }
}

// 2. 发布事件(在创建订单的Service中)
@Component
public class OrderService {
    @Autowired
    private ApplicationEventPublisher publisher;
    
    public void createOrder(Order order) {
        orderMapper.insert(order);
        publisher.publishEvent(new OrderCreatedEvent(this, order)); // 发布事件
        // 订单创建Service不关心谁来监听,它只知道"有订单创建了"
    }
}

// 3. 监听事件(在积分Service中,完全解耦)
@Component
public class PointService {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 增加用户积分------和订单创建逻辑完全解耦!
        pointMapper.addPoint(event.getOrder().getUserId(), 10);
    }
}

// 4. 再加一个监听(短信通知),完全不修改订单创建逻辑
@Component
public class SmsService {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        smsUtil.send(event.getOrder().getUserId(), "订单已创建");
    }
}

事件驱动的好处:订单创建逻辑不需要知道积分逻辑的存在,也不需要知道短信逻辑的存在。增加新的监听者(如记录操作日志)时,对订单创建代码零修改------符合开闭原则。

5.2 Spring用在哪?

  • 容器生命周期事件ContextRefreshedEvent(容器刷新完成)、ContextClosedEvent(容器关闭)。Spring内部的扩展点大多通过事件机制触发
  • Spring Security:认证成功/失败事件、授权事件
  • 业务层面的事件解耦:订单创建、用户注册等,将主流程和后续通知、积分、日志等副作用分离

六、策略模式------不同环境加载不同配置

疑问:Spring怎么做到开发环境一套配置,生产环境另一套配置?

回答:用的是策略模式------定义一套配置加载的策略接口,不同环境使用不同的策略实现。

6.1 Spring中的策略模式

yaml 复制代码
# application-dev.yml(开发环境)
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev_db

# application-prod.yml(生产环境)
spring:
  datasource:
    url: jdbc:mysql://prod-cluster:3306/prod_db
java 复制代码
// 激活不同的配置策略
// 通过 spring.profiles.active=dev 或 prod 切换

Spring的Profile机制本质就是策略模式:定义配置项的统一Profile策略接口,不同Profile(dev/prod/test)提供不同的属性值作为该策略的实现。运行时根据激活的Profile选择对应的策略实现进行组合。

6.2 还有哪些策略模式应用?

策略 场景 Spring中的体现
配置文件 不同环境 Profile + application-{profile}.yml
代理方式 目标类是否实现接口 JDK动态代理 vs CGLIB
事务管理 不同数据源 JpaTransactionManager vs DataSourceTransactionManager
视图解析 不同模板引擎 ThymeleafViewResolver vs InternalResourceViewResolver

七、六种模式如何协同工作?

复制代码
Spring框架全景图(设计模式视角):

单例模式
  └── 容器中所有Bean默认单例,由IoC容器统一管理生命周期

工厂模式
  ├── BeanFactory:全局Bean注册表(容器层)
  └── FactoryBean:封装复杂Bean的创建过程(Bean层)

代理模式
  ├── AOP:为Bean生成代理,插入切面逻辑
  ├── @Transactional:代理中包裹事务管理
  └── @Cacheable:代理中包裹缓存管理

模板方法模式
  ├── JdbcTemplate:管理JDBC的固定流程
  ├── RestTemplate:管理HTTP的固定流程
  └── TransactionTemplate:管理事务的固定流程

观察者模式
  ├── ApplicationEvent:容器生命周期事件
  └── @EventListener:解耦Bean之间的通信

策略模式
  ├── Profile:多环境配置切换
  └── 代理选择:JDK vs CGLIB

单例模式是容器管理的基础------所有Bean默认单例,统一生命周期。工厂模式提供了Bean的创建能力------BeanFactory管全局,FactoryBean管局部复杂创建。代理模式在工厂模式之上为Bean生成代理------AOP将切面逻辑透明插入。模板方法模式封装了事务、JDBC的固定流程------让开发者只关心可变部分。观察者模式解耦Bean之间的通信------发布事件不依赖监听者的存在。策略模式让不同环境的配置和行为切换透明------对外暴露统一接口,对内封装不同的实现。


八、面试中这样回答

面试官:"Spring中用到了哪些设计模式?"

回答框架

"最核心的有六种。单例模式------所有Bean默认单例,由容器统一管理。工厂模式------BeanFactory是容器的顶层接口,FactoryBean封装复杂Bean的创建过程。代理模式------AOP通过动态代理实现,JDK代理要求接口,CGLIB通过继承生成子类,也是@Transactional和@Cacheable的底层机制。模板方法模式------JdbcTemplate、RestTemplate把固定流程封装在内部,可变部分留为回调。观察者模式------ApplicationEvent和@EventListener实现Bean之间的解耦通信。策略模式------Profile机制支持多环境配置切换。这些模式不是各自独立的------单例和工厂是容器的基础,代理在工厂之上为Bean生成增强对象,模板方法封装了框架内部的重试和事务流程,观察者和策略让业务和配置更灵活。"


总结

  • 单例模式:Spring容器通过一级缓存保证每个Bean定义只创建一个实例,是所有Bean管理的基石
  • 工厂模式:BeanFactory是容器层工厂,FactoryBean封装单个复杂Bean的创建。前者管全局,后者管局部细节
  • 代理模式:Spring AOP的灵魂------JDK代理要求接口,CGLIB通过继承生成子类。@Transactional和@Cacheable都是代理模式的延伸
  • 模板方法模式:JdbcTemplate、RestTemplate、TransactionTemplate将固定流程抽离为框架代码,可变部分留给回调实现
  • 观察者模式:ApplicationEvent和@EventListener实现Bean间的解耦通信------发布者完全不依赖监听者的存在
  • 策略模式:Profile机制切换不同环境的配置和实现------开发/测试/生产环境对外暴露统一接口,对内封装各自的策略
  • 模式协同:这六种模式相辅相成------单例和工厂是容器的根基,代理在工厂之上生成增强对象,模板方法封装框架内的固定流程,观察者和策略让通信和配置更灵活

下一篇预告:Spring原理(七)------Spring扩展点:如何优雅地介入Bean的创建流程。拆解BeanPostProcessor、BeanFactoryPostProcessor、InitializingBean、Aware等Spring内置扩展点的执行时机和使用场景,配合权限系统中自定义注解的实际应用。

相关推荐
番石榴AI1 小时前
纯 CPU 推理!0.1B 超轻量级端到端OCR模型,使用 Java 进行文档解析
java·开发语言·ocr
多加点辣也没关系1 小时前
设计模式-工厂方法模式
设计模式·工厂方法模式
likerhood1 小时前
ConcurrentHashMap详细讲解(java)
java·开发语言·性能优化
源码集结号2 小时前
基于 Spring Boot + JPA + MySQL的上门家政系统代码示例
java·前端·后端
程序员老邢3 小时前
【技术底稿 32】Nginx 经典大坑复盘:本机公网域名自环代理,导致接口返回首页 / 404 实战排障
java·运维·nginx·前后端分离·技术底稿·后端部署
该昵称用户已存在3 小时前
从成本中心到价值引擎:MyEMS 开源系统激活企业能源数据资产
java·后端·struts
隐退山林3 小时前
JavaEE进阶:SpringBoot配置文件
java·spring boot·java-ee
阿维的博客日记4 小时前
求解深分页问题,last pk适合什么情况
java·mysql·深分页
DolphinScheduler社区4 小时前
Apache DolphinScheduler 与 Spring Cloud Data Flow:差异与优势解析
spring·spring cloud·apache·海豚调度·大数据工作流调度