设计模式实战:从 Spring/MyBatis/Netty 源码看 8 种经典模式

7 阅读11分钟

设计模式实战:从 Spring/MyBatis/Netty 源码看 8 种经典模式

学设计模式,最怕的是"为了模式而模式"。看十本 GoF 教材、配二十张 UML 图、背二十三个经典模式——结果一上手业务代码,if-else 还是 if-else,造轮子照样造轮子,问题在哪?

问题在于脱离了真实场景 。当你翻开 Spring 的 BeanFactory、MyBatis 的 Mapper 代理、Netty 的 ChannelPipeline 时,会发现这些"大牛框架"其实就是在不停地"翻花样"地复用二十三种模式——而且每个模式都有自己的边界条件、踩坑点、变种。心里的"概念股"一旦和框架源码对上号,模式就活了。

这篇文章挑了八种出场率最高的模式(涵盖单例、工厂、模板、策略、装饰器、代理、观察者、责任链 ),用 Spring、MyBatis、Netty 的真实源码逐个拆给你看。每种模式配 1-2 张原创图解 + 真实代码片段,看完你应该能回答两个问题:① 这个模式在哪个源码入口?② 在我的业务代码里要不要用、怎么用?

图 1

下面进入正文。每一节我都按"概念 → 真实源码 → 实战启示"三段式讲,方便你按需跳读。

一、单例模式: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 代理"的早期引用。

图 2

对应的核心源码(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 三件套正好对应这三种:

图 3

三者的实战场景差异:

    // ① 反射工厂方法(最常用,配合 @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。

图 4

对比一下自己写原生 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 就是教科书级的例子:

图 5

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 层嵌套都司空见惯。

图 6

装饰器还有一个常见的实战用法: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。

代理三件套的差异:

图 7

三种代理在 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 个监听器异步(或同步)处理;监听器之间互不影响,发布方也不用关心谁来处理。

图 8

实战里最常用的是自定义事件 + @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 两条流向。

图 9

实战场景:搭一个命令模式的"网关"。每一个 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 种模式,最后给一张速查表 + 一点个人学习建议。

图 10

写到这里,这篇文章也接近收尾了。模式本身不是目的,读得懂框架源码、写得稳业务代码、设计得了抽象层 ,这三条加起来才是它的真正价值。

下一篇预告:Spring Boot 启动流程与自动配置原理 ——把"启动一个 Spring Boot 应用"这件事拆解到 BeanFactory 创建、ApplicationContext 准备、Bean 生命周期、AutoConfiguration 装配、内嵌 Tomcat 启动 5 大阶段,配 7+ 张原创图讲透。看到这行字就懂:下一篇会用本文讲的"工厂模式 + 模板模式"再来一次实战。


本文首发于掘金。配图均为原创 SVG,转载请保留作者信息。