Spring 中的设计模式(工厂/单例/模板/观察者/代理等)
提出问题
设计模式在 Spring 框架中无处不在。面试官问"Spring 用了哪些设计模式"时,多数人只能答出"BeanFactory 是工厂模式,AOP 是代理模式",但深入一问——FactoryBean 和 BeanFactory 有什么区别?Spring 的模板方法模式怎么体现?单例模式如何保证线程安全?——就卡住了。这道题不是在让你背 23 种设计模式列表,而是考察你是否真的读过 Spring 源码,理解框架设计者在面对"对象创建、代理增强、流程控制、事件通知"这些通用问题时,为什么选这种模式,而不选另一种。
分析问题
1. 工厂模式:BeanFactory 与 FactoryBean
Spring 最核心的设计模式就是工厂模式。BeanFactory 是顶级工厂接口,负责根据 bean 定义创建对象实例:
// 工厂模式的核心:getBean 是工厂方法
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
MyService service = factory.getBean("myService", MyService.class);但真正体现设计模式精妙的是 FactoryBean——它不是容器,而是"工厂 bean",用来创建复杂对象。比如 MyBatis 的 SqlSessionFactoryBean:
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 的区别:
| 概念 | 角色 | 典型场景 |
|---|---|---|
| BeanFactory | IoC 容器接口,管理所有 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,调用方只需配置 modelName、apiKey、baseUrl 等参数,FactoryBean 自动完成客户端初始化、连接池管理、重试策略注入:
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 存储在 DefaultSingletonBeanRegistry 的 singletonObjects 中:
/** 一级缓存:完全初始化好的单例 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 的代码"暴露给调用者:
// 模板方法模式:固定流程封装在模板中,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() 的内部实现(简化版):
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 组装和响应解析暴露给回调:
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实现类
// 定义事件
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 则可以控制监听器在事务的哪个阶段执行:
@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 调用开始/结束、工具调用、错误、状态变更)都可以通过事件机制解耦:
// 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):
// 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:
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) | 是 |
| 典型陷阱 | 强转目标类而非接口会抛 ClassCastException | final 方法不会被代理 |
踩坑案例:经典问题——@Transactional 在同一个类中方法 A 调用方法 B 时,事务不生效。原因是方法 A 内部调用的 this.methodB() 走的是原始对象而非代理对象,事务切面根本没执行。解决方案:要么把方法 B 拆分到另一个 @Service 中,要么注入自己的代理(@Autowired self 自注入),要么用 AopContext.currentProxy() 强行获取当前代理。
Agent 工程场景:代理模式在 Agent 中用于实现横切关注点——LLM 调用的鉴权、限流、日志、缓存、重试都可以通过代理实现,而不需要侵入业务代码:
// 代理:为 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 接口——同一种操作("获取资源")有多种实现策略:
// 策略接口
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 实例化有多种策略:
// 策略接口
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 检索策略:
// 策略接口
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 创建 bean | AgentFactory 创建 Agent 实例,ToolFactory 创建工具 |
| 单例模式 | 单例 bean 池 | LLM 连接池、Embedding 模型池(无状态,全局共享) |
| 模板方法 | JdbcTemplate 封装 SQL 流程 | LLMTemplate 封装 LLM 调用流程 |
| 观察者模式 | ApplicationEvent/Listener | Agent 事件总线(生命周期事件、工具调用事件) |
| 代理模式 | AOP 切面 | LLM 调用代理(缓存、限流、日志、重试) |
| 策略模式 | Resource 策略 | 检索策略、Prompt 策略、模型选择策略 |
核心变化:从"管理对象"变为"管理 LLM 调用"。对象创建变成了提示词组合,SQL 执行变成了 LLM 调用,数据库连接池变成了 Token 配额池。设计模式的思想不变,只是应用场景变了。
总结
六种设计模式一览
| 设计模式 | Spring 中的体现 | 核心类 | 解决了什么问题 |
|---|---|---|---|
| 工厂模式 | BeanFactory / FactoryBean | DefaultListableBeanFactory, SqlSessionFactoryBean | 对象创建与使用的解耦,复杂对象的构造封装 |
| 单例模式 | 默认 bean 作用域 | DefaultSingletonBeanRegistry | 共享对象、减少内存浪费、避免重复创建 |
| 模板方法模式 | JdbcTemplate 等 xxxTemplate | JdbcTemplate, RestTemplate, JmsTemplate | 固定流程封装,可变部分暴露给回调 |
| 观察者模式 | ApplicationEvent/Listener | ApplicationEventPublisher, SimpleApplicationEventMulticaster | 解耦事件发布与处理,支持同步/异步/事务边界 |
| 代理模式 | AOP 实现 | JdkDynamicAopProxy, CglibAopProxy, ProxyFactory | 不修改源码的情况下增加横切逻辑 |
| 策略模式 | Resource 访问 / InstantiationStrategy | Resource 接口及其实现类 | 同一操作的不同算法实现,可替换 |
关键点
- 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 源码