Skip to content

Spring 中的设计模式(工厂/单例/模板/观察者/代理等)

提出问题

设计模式在 Spring 框架中无处不在。面试官问"Spring 用了哪些设计模式"时,多数人只能答出"BeanFactory 是工厂模式,AOP 是代理模式",但深入一问——FactoryBean 和 BeanFactory 有什么区别?Spring 的模板方法模式怎么体现?单例模式如何保证线程安全?——就卡住了。这道题不是在让你背 23 种设计模式列表,而是考察你是否真的读过 Spring 源码,理解框架设计者在面对"对象创建、代理增强、流程控制、事件通知"这些通用问题时,为什么选这种模式,而不选另一种

分析问题

1. 工厂模式:BeanFactory 与 FactoryBean

Spring 最核心的设计模式就是工厂模式。BeanFactory 是顶级工厂接口,负责根据 bean 定义创建对象实例:

java
// 工厂模式的核心:getBean 是工厂方法
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
MyService service = factory.getBean("myService", MyService.class);

但真正体现设计模式精妙的是 FactoryBean——它不是容器,而是"工厂 bean",用来创建复杂对象。比如 MyBatis 的 SqlSessionFactoryBean:

java
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory> {

    @Override
    public SqlSessionFactory getObject() throws Exception {
        // 创建 SqlSessionFactory 需要加载配置、解析 XML、注册 mapper
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(configLocation.getInputStream());
        return factory;
    }

    @Override
    public Class<?> getObjectType() {
        return SqlSessionFactory.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

FactoryBean vs BeanFactory 的区别

概念角色典型场景
BeanFactoryIoC 容器接口,管理所有 bean 的创建和生命周期是 Spring 容器本身
FactoryBean工厂 bean,用来创建特定类型的复杂对象集成第三方框架、创建代理对象、创建复杂配置对象

BeanFactory 通过 & 前缀来区分获取的是 FactoryBean 本身还是它生产的对象:getBean("&sqlSessionFactoryBean") 返回 FactoryBean 实例,getBean("sqlSessionFactoryBean") 返回 getObject() 的结果。

踩坑案例:某团队自定义了一个 JacksonObjectMapperFactoryBean,结果在配置类里注入 ObjectMapper 时忘了加 @Qualifier,导致容器里有两个 ObjectMapper 实例(一个 FactoryBean 生产的,一个自动装配的),序列化行为不一致,排查了 3 天。解决方案:FactoryBean 生产对象要设置 @Primary,或者确保框架只初始化一个 ObjectMapper 来源。

Agent 工程场景:在 Agent 框架中,FactoryBean 常用于创建 LLM Client 实例。比如构建一个 LLMClientFactoryBean,调用方只需配置 modelNameapiKeybaseUrl 等参数,FactoryBean 自动完成客户端初始化、连接池管理、重试策略注入:

java
public class LLMClientFactoryBean implements FactoryBean<LLMClient> {
    private String modelName;
    private String apiKey;
    private String baseUrl;
    private int retryCount = 3;

    @Override
    public LLMClient getObject() {
        return LLMClient.builder()
            .model(modelName)
            .apiKey(apiKey)
            .baseUrl(baseUrl)
            .retryPolicy(RetryPolicy.fixedDelay(retryCount, Duration.ofSeconds(2)))
            .build();
    }
    // setters...
}

2. 单例模式:三级缓存与线程安全

Spring 默认所有 bean 都是单例的。单例 bean 存储在 DefaultSingletonBeanRegistrysingletonObjects 中:

java
/** 一级缓存:完全初始化好的单例 bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 二级缓存:提前暴露的早期 bean(半成品,未完成属性注入) */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

/** 三级缓存:ObjectFactory 工厂,用于延迟创建代理对象 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

为什么需要三级缓存?因为 Spring 用 ConcurrentHashMap 存储单例 bean,但 bean 的创建过程并非原子操作——实例化、属性注入、初始化、BeanPostProcessor 处理是分步完成的。三级缓存的设计本质是双重检查锁定(DCL)的变体:在 getSingleton() 方法中,先从一级缓存拿,拿不到再从二级缓存拿,还拿不到就从三级缓存拿 ObjectFactory 生成早期引用(放入二级缓存后再删除三级缓存)。这样既保证了线程安全,也避免了不必要的代理对象创建。

三级缓存执行时序

1. 线程 A 创建 BeanA → 实例化完成 → 存入三级缓存 singletonFactories
2. 线程 A 注入 BeanB → 发现 BeanB 尚在创建 → 尝试从三级缓存取 BeanB 的早期引用
3. 三级缓存中的 ObjectFactory 执行 → 生成早期 BeanB(仅实例化,未注入)→ 移入二级缓存
4. 线程 A 拿到的 BeanB 早期引用注入到 BeanA → BeanA 完成初始化
5. 线程 A 将 BeanA 从三级缓存移除 → 放入一级缓存 singletonObjects
6. 线程 B 同时 getBean(BeanB) → 一级缓存无 → 二级缓存有 → 直接返回早期引用

踩坑案例:生产环境遇到一个诡异问题——某个 @Service 的 @PostConstruct 方法执行了两次。排查发现是因为该 bean 实现了 ApplicationContextAware,在 setApplicationContext 中又手动调了一次 getBean,导致 bean 提前从三级缓存进入二级缓存,后续 BeanPostProcessor 处理时又创建了一次。结论:不要在 setApplicationContext 或构造器中调用 getBean

Agent 工程场景:Agent 框架中的 ToolRegistry、MemoryStore 等组件通常设计为单例。但要注意,如果 Agent 实例也设计为单例,可能会引发状态污染——多个请求共享同一个 Agent 上下文。解决方案:Agent 对象用 prototype 作用域,而底层的 LLM 连接池、向量数据库连接保持单例,这才是单例模式的正确用法:共享无状态资源,隔离有状态对象

3. 模板方法模式:JdbcTemplate 与 RestTemplate

Spring 的 xxxTemplate 家族是模板方法模式的经典实现。以 JdbcTemplate 为例,它封装了"获取连接 → 创建 Statement → 执行 SQL → 处理 ResultSet → 关闭连接"这个固定流程,只把"处理 ResultSet 的代码"暴露给调用者:

java
// 模板方法模式:固定流程封装在模板中,Callback 暴露给使用者
jdbcTemplate.query(
    "SELECT id, name, age FROM users WHERE age > ?",
    new Object[]{18},
    (rs, rowNum) -> {
        User user = new User();
        user.setId(rs.getLong("id"));
        user.setName(rs.getString("name"));
        user.setAge(rs.getInt("age"));
        return user;
    }
);

JdbcTemplate.query() 的内部实现(简化版):

java
public <T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper) {
    // 固定流程:获取连接
    Connection conn = DataSourceUtils.getConnection(dataSource);
    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
        // 固定流程:创建 Statement
        ps = conn.prepareStatement(sql);
        // 固定流程:设置参数
        setValues(ps, args);
        // 固定流程:执行查询
        rs = ps.executeQuery();
        // 可变部分:解析结果集 → 交给 RowMapper 回调
        List<T> results = new ArrayList<>();
        int rowNum = 0;
        while (rs.next()) {
            results.add(rowMapper.mapRow(rs, rowNum++));
        }
        return results;
    } catch (SQLException e) {
        throw new DataAccessException("查询失败", e);
    } finally {
        // 固定流程:释放资源
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(ps);
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}

模板方法模式的核心优势:消除重复代码,统一资源管理,防止资源泄露。开发者只需要关注 RowMapper 回调,不用管连接池获取、异常处理、资源释放这些脏活。

踩坑案例:生产环境中 JdbcTemplate.query() 默认每次调用都从连接池获取连接,如果方法被频繁调用(比如每秒几千次),且 SQL 执行时间较长(>100ms),连接池很快耗尽。排查发现是 @Transactional 注解没加——加了事务后,同一个事务内的所有 SQL 复用同一个连接,连接池压力骤降 80%。

Agent 工程场景:LLM API 调用非常吻合模板方法模式。构建一个 LLMTemplate,封装「构建请求 → 设置参数 → 发送 API → 处理响应 → 解析工具调用 → 重试降级」这个固定流程,只把 prompt 组装和响应解析暴露给回调:

java
public class LLMTemplate {
    public <T> T chat(String systemPrompt, String userInput, 
                       Function<LLMResponse, T> resultParser) {
        // 固定流程:构建请求
        ChatRequest request = ChatRequest.builder()
            .model(modelName)
            .messages(List.of(
                Message.system(systemPrompt),
                Message.user(userInput)
            ))
            .temperature(0.7)
            .maxTokens(2048)
            .build();
        // 固定流程:发送请求 + 重试
        for (int i = 0; i < retryCount; i++) {
            try {
                LLMResponse response = client.chat(request);
                // 可变部分:结果解析
                return resultParser.apply(response);
            } catch (RateLimitException e) {
                Thread.sleep(1000 * (i + 1));
            }
        }
        throw new LLMException("API 调用失败,已达最大重试次数");
    }
}

4. 观察者模式:ApplicationEvent/Listener

Spring 事件机制是观察者模式的完整实现。三个核心角色:

  • Subject(主题)ApplicationEventPublisher(ApplicationContext 默认实现)
  • Event(事件)ApplicationEvent 的子类
  • Observer(观察者)ApplicationListener 实现类
java
// 定义事件
public class OrderCreatedEvent extends ApplicationEvent {
    private final Long orderId;
    private final String userId;

    public OrderCreatedEvent(Object source, Long orderId, String userId) {
        super(source);
        this.orderId = orderId;
        this.userId = userId;
    }
    // getters...
}

// 发布事件
@Service
public class OrderService {

    @Autowired
    private ApplicationEventPublisher publisher;

    public Order createOrder(OrderDTO dto) {
        Order order = doCreate(dto);
        // 同步发布事件,监听器与发布者在同一事务中
        publisher.publishEvent(new OrderCreatedEvent(this, order.getId(), dto.getUserId()));
        return order;
    }
}

// 监听事件
@Component
public class OrderEventListener {

    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 发送短信通知
        smsService.send(event.getUserId(), "您的订单已创建");
    }
}

@TransactionalEventListener 则可以控制监听器在事务的哪个阶段执行:

java
@Component
public class OrderEventListener {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 事务提交后才发送 MQ 消息,避免事务回滚导致消息脏数据
        mqService.send("order.topic", event.getOrderId());
    }
}

事件传播时序

publishEvent() 调用
  → SimpleApplicationEventMulticaster.multicastEvent()
    → 获取 TaskExecutor(如果有配置则异步执行)
    → 遍历所有匹配的 ApplicationListener
    → 逐个调用 onApplicationEvent()
  → 默认同步执行,监听器抛出异常会影响发布者
  → 如需异步,需配置 SimpleApplicationEventMulticaster.setTaskExecutor()

踩坑案例:某团队在 @EventListener 中发了短信,结果订单创建接口响应时间从 50ms 飙升到 800ms。原因是短信通道超时(供应商服务抖动),导致主线程卡住。解决方案:监听器要么用 @Async 异步执行,要么抛到消息队列异步处理。另一个坑:某个监听器抛出异常,导致 @TransactionalEventListener 所在的事务被标记为 rollback-only,虽然设置了 AFTER_COMMIT,但主事务回滚了监听器就没执行,而用户以为订单创建成功了——实际上订单已回滚。

Agent 工程场景:Agent 的事件驱动架构非常适合观察者模式。Agent 执行过程中产生的各种事件(LLM 调用开始/结束、工具调用、错误、状态变更)都可以通过事件机制解耦:

java
// Agent 事件
public class AgentEvent extends ApplicationEvent {
    private final String agentId;
    private final EventType type;
    // LLM 调用耗时、Token 消耗、错误信息等
}

// 日志监听器:记录所有 Agent 事件到 ELK
@Component
public class AgentLoggingListener {
    @EventListener
    public void onAgentEvent(AgentEvent event) {
        log.info("Agent {}: {} - {}", event.getAgentId(), event.getType(), event.getDetails());
    }
}

// 指标监听器:统计 Token 消耗和延迟
@Component
public class AgentMetricsListener {
    @EventListener
    public void onAgentEvent(AgentEvent event) {
        if (event.getType() == LLM_CALL_COMPLETED) {
            metrics.counter("llm.token.total", event.getTokenCount());
            metrics.timer("llm.latency", event.getDuration());
        }
    }
}

5. 代理模式:AOP 的 JDK 动态代理与 CGLIB

Spring AOP 的底层是代理模式。当目标类实现了接口,Spring 用 JDK 动态代理;否则用 CGLIB(Spring Boot 2.x 后默认全用 CGLIB):

java
// ProxyFactory 内部会根据目标类是否实现接口选择代理方式
public class DefaultAopProxyFactory implements AopProxyFactory {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) {
        if (config.isOptimize() || config.isProxyTargetClass()
                || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                // 目标类是接口 → JDK 动态代理
                return new JdkDynamicAopProxy(config);
            }
            // 否则 → CGLIB 代理
            return new ObjenesisCglibAopProxy(config);
        } else {
            // 有接口且未强制 proxyTargetClass → JDK 动态代理
            return new JdkDynamicAopProxy(config);
        }
    }
}

JDK 动态代理的核心是 InvocationHandler

java
public class LoggingProxy implements InvocationHandler {

    private final Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("[代理] 调用方法: " + method.getName());
        long start = System.nanoTime();
        try {
            Object result = method.invoke(target, args);
            return result;
        } finally {
            long elapsed = System.nanoTime() - start;
            System.out.println("[代理] 方法执行耗时: " + elapsed / 1_000_000 + "ms");
        }
    }
}

JDK 动态代理 vs CGLIB 对比

维度JDK 动态代理CGLIB
原理运行时生成实现目标接口的代理类运行时生成目标类的子类
要求目标类必须实现至少一个接口目标类不能是 final 类,方法不能是 final
性能(创建)较快较慢(需生成字节码)
性能(调用)反射调用,JDK8+ 优化后接近原生直接调用,速度略快
Spring Boot 默认否(Spring Boot 2.x 起默认 CGLIB)
典型陷阱强转目标类而非接口会抛 ClassCastExceptionfinal 方法不会被代理

踩坑案例:经典问题——@Transactional 在同一个类中方法 A 调用方法 B 时,事务不生效。原因是方法 A 内部调用的 this.methodB() 走的是原始对象而非代理对象,事务切面根本没执行。解决方案:要么把方法 B 拆分到另一个 @Service 中,要么注入自己的代理(@Autowired self 自注入),要么用 AopContext.currentProxy() 强行获取当前代理。

Agent 工程场景:代理模式在 Agent 中用于实现横切关注点——LLM 调用的鉴权、限流、日志、缓存、重试都可以通过代理实现,而不需要侵入业务代码:

java
// 代理:为 LLM 调用添加缓存和限流
public class LLMClientProxy implements InvocationHandler {
    private final LLMClient target;
    private final Cache<String, String> cache;
    private final RateLimiter rateLimiter;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 限流检查
        if (!rateLimiter.tryAcquire()) {
            throw new RateLimitException("请求过于频繁,请稍后重试");
        }
        // 缓存检查(仅对 chat 方法)
        if ("chat".equals(method.getName())) {
            String cacheKey = buildCacheKey(args);
            String cached = cache.get(cacheKey);
            if (cached != null) return cached;
        }
        // 执行目标方法
        Object result = method.invoke(target, args);
        // 写入缓存
        if ("chat".equals(method.getName())) {
            cache.put(buildCacheKey(args), result.toString());
        }
        return result;
    }
}

6. 策略模式:Resource 访问与实例化策略

Spring 中有多处策略模式的应用。最典型的是 Resource 接口——同一种操作("获取资源")有多种实现策略:

java
// 策略接口
public interface Resource extends InputStreamSource {
    boolean exists();
    URL getURL() throws IOException;
    File getFile() throws IOException;
    // ...
}

// 策略实现:classpath 资源
Resource r1 = new ClassPathResource("application.yml");
// 策略实现:文件系统资源
Resource r2 = new FileSystemResource("/etc/config/app.properties");
// 策略实现:URL 资源
Resource r3 = new UrlResource("https://example.com/config.json");
// 策略实现:Servlet 上下文资源
Resource r4 = new ServletContextResource(servletContext, "/WEB-INF/config.xml");

另一处策略模式是 InstantiationStrategy——Bean 实例化有多种策略:

java
// 策略接口
public interface InstantiationStrategy {
    Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner);
    // ...
}

// 具体策略:CGLIB 实例化(用于类代理)
public class CglibSubclassingInstantiationStrategy extends SimpleInstantiationStrategy {
    // 使用 CGLIB Enhancer 创建子类实例
}

// 具体策略:简单反射实例化
public class SimpleInstantiationStrategy implements InstantiationStrategy {
    // 使用 Constructor.newInstance() 创建实例
}

踩坑案例ResourcePatternResolver 匹配 classpath*: 路径时,如果项目被打包成 jar,ClassPathResource 无法直接读取目录(只能读文件),导致 getResource("classpath*:mybatis/mapper/*.xml") 在 jar 包中找不到文件。解决方案:改为 PathMatchingResourcePatternResolver 或者确保 mapper 路径明确指定文件名。

Agent 工程场景:策略模式在 Agent 框架中用于实现可替换的算法组件。例如,RAG 检索策略:

java
// 策略接口
public interface RetrievalStrategy {
    List<Document> retrieve(String query, int topK);
}

// 策略:基础向量检索
@Component("vectorRetrieval")
public class VectorRetrievalStrategy implements RetrievalStrategy {
    public List<Document> retrieve(String query, int topK) {
        float[] embedding = embeddingService.embed(query);
        return vectorStore.similaritySearch(embedding, topK);
    }
}

// 策略:混合检索(向量 + BM25)
@Component("hybridRetrieval")
public class HybridRetrievalStrategy implements RetrievalStrategy {
    public List<Document> retrieve(String query, int topK) {
        List<Document> vectorResults = vectorStore.similaritySearch(embed(query), topK);
        List<Document> bm25Results = bm25Search(query, topK);
        return mergeAndRerank(vectorResults, bm25Results);
    }
}

// 动态选择策略
@Service
public class RAGPipeline {
    @Autowired @Qualifier("hybridRetrieval")
    private RetrievalStrategy retrievalStrategy;

    public String answer(String question) {
        List<Document> docs = retrievalStrategy.retrieve(question, 5);
        return llmClient.generate(question, docs);
    }
}

这些设计模式在 Agent 框架中的统一运用

从 8 年 Java 后端转 Agent 工程,最应该理解的一点是:Agent 框架(LangChain、LlamaIndex、Spring AI)本质上就是把 Spring 的设计模式复制到了 LLM 领域

设计模式Spring 用法Agent 框架对应
工厂模式BeanFactory 创建 beanAgentFactory 创建 Agent 实例,ToolFactory 创建工具
单例模式单例 bean 池LLM 连接池、Embedding 模型池(无状态,全局共享)
模板方法JdbcTemplate 封装 SQL 流程LLMTemplate 封装 LLM 调用流程
观察者模式ApplicationEvent/ListenerAgent 事件总线(生命周期事件、工具调用事件)
代理模式AOP 切面LLM 调用代理(缓存、限流、日志、重试)
策略模式Resource 策略检索策略、Prompt 策略、模型选择策略

核心变化:从"管理对象"变为"管理 LLM 调用"。对象创建变成了提示词组合,SQL 执行变成了 LLM 调用,数据库连接池变成了 Token 配额池。设计模式的思想不变,只是应用场景变了。

总结

六种设计模式一览

设计模式Spring 中的体现核心类解决了什么问题
工厂模式BeanFactory / FactoryBeanDefaultListableBeanFactory, SqlSessionFactoryBean对象创建与使用的解耦,复杂对象的构造封装
单例模式默认 bean 作用域DefaultSingletonBeanRegistry共享对象、减少内存浪费、避免重复创建
模板方法模式JdbcTemplate 等 xxxTemplateJdbcTemplate, RestTemplate, JmsTemplate固定流程封装,可变部分暴露给回调
观察者模式ApplicationEvent/ListenerApplicationEventPublisher, SimpleApplicationEventMulticaster解耦事件发布与处理,支持同步/异步/事务边界
代理模式AOP 实现JdkDynamicAopProxy, CglibAopProxy, ProxyFactory不修改源码的情况下增加横切逻辑
策略模式Resource 访问 / InstantiationStrategyResource 接口及其实现类同一操作的不同算法实现,可替换

关键点

  • FactoryBean 和 BeanFactory 是两回事,前者是"工厂 bean",后者是"bean 工厂"。面试时区分清楚,能体现源码阅读深度
  • Spring 单例 bean 用三级缓存实现了线程安全的单例注册,同时延迟创建代理对象,避免性能浪费
  • 模板方法模式在 Spring 中不是用抽象类实现的,而是用"模板类 + 回调接口"的方式,更灵活
  • 观察者模式中,@TransactionalEventListener 的事务阶段控制是生产级必备技能
  • 代理模式的核心是 JDK 动态代理只能代理接口,CGLIB 不能代理 final 类和方法
  • 策略模式在 Spring 中无处不在,从 Resource 访问到 Bean 实例化策略,设计上遵循"开闭原则"
  • Agent 工程面试加分项:能说出这些设计模式在 Agent 框架中的对应关系,证明你理解了从传统后端到 AI 工程的思维迁移

参考:Spring 源码 — AbstractBeanFactory、DefaultSingletonBeanRegistry、JdbcTemplate、AbstractApplicationContext.publishEvent()、DefaultAopProxyFactory、Resource 接口体系;《Spring 设计模式》;LangChain / Spring AI 源码

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。