设计模式实战:从 Spring/MyBatis/Netty 源码看 8 种经典模式
学设计模式,最怕的是"为了模式而模式"。看十本 GoF 教材、配二十张 UML 图、背二十三个经典模式——结果一上手业务代码,if-else 还是 if-else,造轮子照样造轮子,问题在哪?
问题在于脱离了真实场景 。当你翻开 Spring 的 BeanFactory、MyBatis 的 Mapper 代理、Netty 的 ChannelPipeline 时,会发现这些"大牛框架"其实就是在不停地"翻花样"地复用二十三种模式——而且每个模式都有自己的边界条件、踩坑点、变种。心里的"概念股"一旦和框架源码对上号,模式就活了。
这篇文章挑了八种出场率最高的模式(涵盖单例、工厂、模板、策略、装饰器、代理、观察者、责任链 ),用 Spring、MyBatis、Netty 的真实源码逐个拆给你看。每种模式配 1-2 张原创图解 + 真实代码片段,看完你应该能回答两个问题:① 这个模式在哪个源码入口?② 在我的业务代码里要不要用、怎么用?

下面进入正文。每一节我都按"概念 → 真实源码 → 实战启示"三段式讲,方便你按需跳读。
一、单例模式:Spring Bean 默认就是它
模式释义 :保证一个类只有一个实例,并提供一个全局访问点。源码入口 :org.springframework.beans.factory.support.DefaultSingletonBeanRegistry,三个 Map(三级缓存)决定了 Bean 在容器内的"单例形态"。
Spring 的 Bean 默认作用域就是 singleton,整个 IoC 容器对同一个 Bean ID 只保留一份实例。但这"一份"远比简单 private static final 复杂——因为 Bean 是有依赖的、可能存在循环依赖、可能被 AOP 代理 。Spring 把"单例"拆成了三级缓存 :
- 一级缓存
singletonObjects:完整可用的 Bean 实例。 - 二级缓存
earlySingletonObjects:早期未填充属性的"半成品" Bean,用于解决循环依赖。 - 三级缓存
singletonFactories:存 ObjectFactory Lambda,能生成"可能已被 AOP 代理"的早期引用。

对应的核心源码(Spring Framework 6.x 抽象版):
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
public class DefaultSingletonBeanRegistry {
// 一级缓存:完整 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);
public Object getSingleton(String beanName) {
// 第一重检查(无锁,性能关键)
Object singleton = this.singletonObjects.get(beanName);
if (singleton == null && isSingletonCurrentlyInCreation(beanName)) {
// 加锁后进入二级缓存查找
synchronized (this.singletonObjects) {
// 第二重检查(加锁后)
singleton = this.earlySingletonObjects.get(beanName);
if (singleton == null) {
// 从三级缓存拿 ObjectFactory → 生成早期引用
ObjectFactory<?> factory = this.singletonFactories.get(beanName);
if (factory != null) {
singleton = factory.getObject(); // 这里会触发 AOP 代理
this.earlySingletonObjects.put(beanName, singleton);
this.singletonFactories.remove(beanName);
}
}
}
}
return singleton;
}
}
⚠️ 实战警示 1 :不要在 @Component Bean 里定义可变更的成员字段 。Singleton 意味着"整个应用共享一个对象",字段一旦被某个请求改了,下一个请求会读到被污染的数据。如果必须留字段,请标
@Scope("prototype")或显式用 ThreadLocal。⚠️ 实战警示 2 :循环依赖只能解决setter 注入 ,构造器注入的循环依赖 Spring 解决不了 ,会直接抛
BeanCurrentlyInCreationException。三级缓存的"提前曝光"发生在 setter 阶段,构造器阶段还没早期引用可用。
二、工厂模式三件套:BeanFactory / FactoryBean / 反射
模式释义 :把"创建对象"这件事从调用方抽出去,集中到工厂里管理。调用方只知道要什么 ,不知道怎么造 。GoF 原本分三种:简单工厂(静态方法)、工厂方法(一个工厂接口 + 多个工厂子类)、抽象工厂(一组关联对象的家族)。Spring 三件套正好对应这三种:

三者的实战场景差异:
// ① 反射工厂方法(最常用,配合 @Configuration)
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() { // 这个方法就是 "工厂方法"
HikariConfig cfg = new HikariConfig();
cfg.setJdbcUrl("jdbc:mysql://...");
cfg.setUsername("root");
return new HikariDataSource(cfg); // 容器拿到返回值注册成 Bean
}
}
// ② FactoryBean:你自己造一个工厂Bean,容器调 getObject() 拿到真正的 Bean
public class TaxServiceFactoryBean implements FactoryBean<TaxService> {
@Override public TaxService getObject() {
return new TaxServiceImpl(loadConfigFromNacos());
}
@Override public Class<?> getObjectType() { return TaxService.class; }
}
// 取值:getBean("taxService") → 拿到 TaxService;getBean("&taxService") → 拿到工厂本身
// ③ BeanFactory:基本不会直接用,但所有 ApplicationContext 都"是一个" BeanFactory
BeanFactory factory = new AnnotationConfigApplicationContext(AppConfig.class);
UserService svc = factory.getBean(UserService.class); // 实际拿到的是 CGLIB 代理类
⚠️ 实战警示 :@Configuration 类里的 @Bean 方法会被 Spring 用 CGLIB 代理,多次调用返回的是同一个 Bean (走 Spring 单例缓存)。如果想要"每次新建",把类改成 @Component(非 @Configuration),或者用 @Scope("prototype")。
三、模板模式:父类定骨架,子类填细节
模式释义 :抽象父类定义算法骨架(哪几步、固定顺序),把可变步骤延迟到子类实现。源码入口 :Spring JdbcTemplate、MyBatis BaseExecutor、Netty AbstractBootstrap。
JdbcTemplate 是模板模式的典范。它把"打开连接 → 预编译 SQL → 执行 → 处理结果 → 关闭连接"这一整条样板代码做了,唯一开放给调用者的是"如何把结果转成对象" 这一个钩子方法。其余十几行样板全藏在父类里。这就是经典的好莱坞法则 :Don't call us, we'll call you。

对比一下自己写原生 JDBC 的"丑陋版本",就懂 JdbcTemplate 模板为什么爽:
// 反例:原生 JDBC 每次都要写样板
public User findById(Long id) throws SQLException {
Connection conn = dataSource.getConnection(); // 1
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?"); // 2
ps.setLong(1, id);
rs = ps.executeQuery();
if (rs.next()) return mapRow(rs); // 唯一变化点
return null;
} finally {
if (rs != null) rs.close();
if (ps != null) ps.close();
conn.close(); // 3 还得小心 conn 归还连接池!
}
}
// 正例:JdbcTemplate 模板
public User findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM user WHERE id = ?",
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")),
id
);
}
MyBatis 的 BaseExecutor、Netty 的 AbstractBootstrap 用的是同一套路:一个抽象父类定义"做哪些事",子类必须实现"怎么做某一步"。
⚠️ 实战警示 :模板模式 vs 策略模式经常被混淆——模板的骨架是固定的(顺序不能改) ,策略是整体可替换的(顺序换不叫事) 。举例:HttpServlet 的 service() 是模板(doGet → doPost 顺序固定),Comparator 是策略(升序降序随便换)。
四、策略模式:算法可替换,调用方无感
模式释义 :定义一系列算法,封装起来使它们可以相互替换。调用方只面向"策略接口"编程,不关心具体实现。源码入口 :JDK Comparator<T>、Spring TaskExecutor、Netty EventExecutorChooser。
策略模式的核心是把"变化的行为"抽成接口 ,调用方持有一个接口引用,运行期切换实现。JDK 的 Comparator 就是教科书级的例子:

Spring 的 @Async 注解背后就是策略:你写 @Async 不知道它用哪个线程池,要切换时只要换 Bean:
// 切换 TaskExecutor 策略
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
// 策略 1:固定大小线程池
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(8);
exec.setMaxPoolSize(16);
exec.setQueueCapacity(200);
exec.setThreadNamePrefix("biz-async-");
exec.initialize();
return exec;
// 策略 2:换成 ForkJoinPool(计算密集型)
// return new ForkJoinPool(Runtime.getRuntime().availableProcessors());
// 策略 3:换成 WorkStealingPool(I/O + 计算混合)
// return Executors.newWorkStealingPool();
}
}
// 业务代码完全无感
@Service
public class OrderService {
@Async // 用的就是上面 getAsyncExecutor 返回的那个策略
public CompletableFuture<Boolean> settleOrder(Long orderId) {
orderRepository.update(orderId, SETTLED);
return CompletableFuture.completedFuture(true);
}
}
⚠️ 实战警示 :if-else 链超过 3 个就该考虑策略模式。但别过度设计 ——只有"真的会换实现"才有策略价值。把只走一次的逻辑硬抽成接口,反而增加维护成本。判断标准:实现类数量 ≥ 2 且未来会扩展。
五、装饰器模式:动态叠加职责
模式释义 :在不改变原类、不使用继承的情况下,动态地给对象增加额外职责。源码入口 :Java IO 流(BufferedInputStream(FileInputStream))、Netty 的 ChannelHandler 链、Servlet 的 HttpServletRequestWrapper。
装饰器有两条铁律:① 装饰器实现与被装饰对象相同的接口 (多态),② 装饰器持有被装饰对象的引用 (组合)。最经典的就是 JDK IO 流——每一层都在叠加职责,直到 4-5 层嵌套都司空见惯。

装饰器还有一个常见的实战用法:HttpServletRequestWrapper。因为 Servlet API 的 HttpServletRequest 是接口不能直接继承实现类,要"增强"它就只能装饰一下——常用于读取 body 多次(因为 getInputStream() 只能读一次):
// 场景:网关要读取 body 用于日志,同时下游 Filter 还要读 body
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private final byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
// 第一次读 body,存到内存里
this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(this.cachedBody); // 返回装饰后的流
}
}
⚠️ 实战警示 :装饰器 ≠ 继承。装饰器的核心是组合(持有引用) ,继承是强耦合 。典型反例:用继承做"增强"会导致类爆炸——装饰 100 个职责要写 100 个子类,装饰器只要 100 个装饰类按需组合。
但装饰器层级别太多 ,阅读性会崩溃。Java IO 那种 5-6 层嵌套已经是极限了。
六、代理模式三件套:JDK Proxy / CGLIB / 静态代理
模式释义 :给目标对象提供一个"替身",以控制对这个对象的访问(增强 + 拦截)。源码入口 :JDK java.lang.reflect.Proxy、CGLIB net.sf.cglib.proxy.Enhancer、Spring AOP 的 ProxyFactory。
代理三件套的差异:

三种代理在 Spring AOP / MyBatis / RPC 框架里反复出现。JDK 动态代理是面试最高频问的:
// 写一个通用 JDK 动态代理:把方法调用时间统计出来
public class TimingInvocationHandler implements InvocationHandler {
private final Object target;
public TimingInvocationHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
try {
Object result = method.invoke(target, args); // 真的执行
return result;
} finally {
long cost = System.nanoTime() - start;
System.out.println(method.getName() + " 耗时 " + cost / 1_000_000 + "ms");
}
}
}
// 用法:所有方法自动加耗时打印
UserService svc = (UserService) Proxy.newProxyInstance(
classLoader,
new Class[]{UserService.class},
new TimingInvocationHandler(new UserServiceImpl())
);
svc.save(user); // 输出:save 耗时 12ms
⚠️ 实战警示 :CGLIB 用继承实现代理,所以 final 类和 final 方法代理不了——这是很多"@Transactional 无效"坑的根因(例如把 Service 标了 final class)。同时 JDK 动态代理
equals/hashCode也踩坑:代理对象的 equals 会触发 invoke → target.equals,可能造成递归调用导致栈溢出。
七、观察者模式:状态变化自动通知
模式释义 :定义对象间一对多依赖,当一个对象状态改变时,所有依赖者自动收到通知 。源码入口 :Spring ApplicationEvent / ApplicationListener、JDK java.util.Observable(已废弃,被 EventObject/Listener 体系替代)、Netty ChannelFuture。
Spring 的事件机制是观察者模式的最好例子。一个事件发布出来后,可以有 N 个监听器异步(或同步)处理;监听器之间互不影响,发布方也不用关心谁来处理。

实战里最常用的是自定义事件 + @EventListener:
// ① 定义事件
public class OrderPaidEvent extends ApplicationEvent {
private final Long orderId;
private final BigDecimal amount;
public OrderPaidEvent(Object source, Long orderId, BigDecimal amount) {
super(source);
this.orderId = orderId;
this.amount = amount;
}
// getters...
}
// ② 业务代码发布事件(不再关心谁来处理)
@Service
public class OrderService {
@Autowired private ApplicationEventPublisher publisher;
@Transactional
public void pay(Long orderId) {
orderRepository.updateStatus(orderId, PAID);
publisher.publishEvent(new OrderPaidEvent(this, orderId, orderIdMoney));
}
}
// ③ 监听器:审计
@Component
public class AuditListener {
@EventListener
public void onPaid(OrderPaidEvent e) {
auditLog.save("订单 " + e.getOrderId() + " 已支付 " + e.getAmount());
}
}
// ④ 监听器:发短信(异步)
@Component
public class SmsListener {
@Async
@EventListener
public void onPaid(OrderPaidEvent e) {
smsClient.send(e.getOrderId(), "您已支付成功");
}
}
// ⑤ 监听器:事务提交后再发 MQ(关键技巧!)
@Component
public class MqListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onPaid(OrderPaidEvent e) {
mqProducer.send("order.paid", e); // 只有事务提交才发,避免回滚后幽灵消息
}
}
⚠️ 实战警示 :默认的
SimpleApplicationEventMulticaster是同步 的——监听器抛异常会冒泡到发布方,可能回滚你的事务。要异步请显式setTaskExecutor(...)。还要注意 @EventListener 在同步模式下会拖慢业务主链路,非关键通知建议都加 @Async 或走 MQ 。Spring 4.2+ 起事件不必继承 ApplicationEvent,普通 POJO 也能被发布出去(只要监听器签名匹配)。
八、责任链模式:管道里的接力赛
模式释义 :把多个处理器串成一条链,请求沿链传递直到被处理(或全部放行)。源码入口 :Netty ChannelPipeline、Spring HandlerInterceptor、Servlet Filter、Spring Security FilterChain。
Netty 的 ChannelPipeline 是责任链的集大成者。一个网络请求在 Netty 里"过五关斩六将":业务解码 → 协议解析 → 业务鉴权 → 业务处理 → 异常处理 → 编码回写。每一步都可能拦截或修改请求。这个模型比 servlet 的 Filter 强大得多——它能动态增删 handler、还能同时处理 inbound 和 outbound 两条流向。

实战场景:搭一个命令模式的"网关"。每一个 Handler 都按"鉴权 → 限流 → 解密 → 业务"的顺序排列:
// Netty Handler 链实战:每个 channelRead 都"接力传递"
public class AuthHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (((Request) msg).getToken() == null || !verify(((Request) msg).getToken())) {
ctx.writeAndFlush(new Response(401, "未授权"));
return; // 不调用 ctx.fireChannelRead → 链中断
}
ctx.fireChannelRead(msg); // 放行,下一个 handler 处理
}
}
public class RateLimitHandler extends ChannelInboundHandlerAdapter {
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (!rateLimiter.tryAcquire()) {
ctx.writeAndFlush(new Response(429, "请求过多"));
return;
}
ctx.fireChannelRead(msg);
}
}
// 启动时装链
ServerBootstrap b = new ServerBootstrap();
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new AuthHandler()) // 1. 鉴权
.addLast(new RateLimitHandler()) // 2. 限流
.addLast(new ProtobufDecoder(...)) // 3. 解码
.addLast(new BusinessHandler()); // 4. 业务
}
});
⚠️ 实战警示 :责任链的每个节点必须显式决定"放行/中断" 。常见 bug 是写了 handler 但忘记
ctx.fireChannelRead(msg),导致消息悄悄"消失",下游收不到。Servlet Filter 同理,chain.doFilter()没调用就等过滤器链断了。线上出问题往往查两小时发现是这个低级错误。
九、8 种模式速查表 + 学习的三层境界
讲完 8 种模式,最后给一张速查表 + 一点个人学习建议。

写到这里,这篇文章也接近收尾了。模式本身不是目的,读得懂框架源码、写得稳业务代码、设计得了抽象层 ,这三条加起来才是它的真正价值。
下一篇预告:Spring Boot 启动流程与自动配置原理 ——把"启动一个 Spring Boot 应用"这件事拆解到 BeanFactory 创建、ApplicationContext 准备、Bean 生命周期、AutoConfiguration 装配、内嵌 Tomcat 启动 5 大阶段,配 7+ 张原创图讲透。看到这行字就懂:下一篇会用本文讲的"工厂模式 + 模板模式"再来一次实战。
本文首发于掘金。配图均为原创 SVG,转载请保留作者信息。