设计模式在框架源码中的体现:Spring、MyBatis、Netty中的单例、工厂、观察者

说实话,很多同学学了设计模式,但一到看源码就懵了。为什么?因为框架里的模式不是「教科书式」的,它们往往混着用、变着花样用。今天我就拿三个最常用的框架——Spring、MyBatis、Netty,带你看看单例、工厂、观察者到底是怎么落地到代码里的。

一、单例模式:从饿汉到容器化

单例模式,说白了就是保证一个类只有一个实例。但框架里的单例,远不止「new一次」这么简单。

1. Spring中的单例:IoC容器默认行为

Spring的Bean默认就是单例的。你想想看,一个Service类,如果每次请求都new一个,那内存早就爆了。Spring怎么做的?它用了一个三级缓存来管理单例Bean。

核心代码:DefaultSingletonBeanRegistry

// 第一级缓存:成品Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 第二级缓存:早期Bean(还没完成属性注入)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 第三级缓存:Bean工厂(用于解决循环依赖)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

我在项目中遇到过一个问题:两个Service互相依赖,结果启动报错。当时我查了半天,才发现是忘了加@Lazy注解。嗯,Spring的三级缓存虽然能解决大部分循环依赖,但构造器注入还是不行。这个坑我踩过,你注意一下。

我的习惯:业务Service用单例就够了,但如果是状态类(比如计数器),千万别用单例,否则并发问题够你喝一壶的。

2. MyBatis中的单例:SqlSessionFactory

MyBatis里最典型的单例就是SqlSessionFactory。每个数据库对应一个工厂实例,全局共享。为什么?因为创建它需要解析XML、加载映射文件,成本很高。

// 官方推荐写法
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 这个factory通常用静态变量保存,全局唯一

我曾经见过一个项目,每次请求都new一个SqlSessionFactory,结果数据库连接池直接爆了。说白了,这就是没理解单例的用意——重量级对象必须复用

3. Netty中的单例:ChannelHandler

Netty里的单例有点特殊。它的Handler可以是单例,也可以是多例。但有个关键点:带状态的Handler不能是单例

注意:如果你用@Sharable注解标记一个Handler为单例,那它里面不能有成员变量。否则并发请求会把数据搞乱。我早期写Netty时就犯过这个错,一个计数器变量被多个线程同时改,结果数字对不上。

二、工厂模式:从简单工厂到抽象工厂

工厂模式在框架里无处不在。说白了,就是把「创建对象」这件事封装起来,让调用方不用关心具体实现。

1. Spring的BeanFactory:最经典的工厂

Spring的BeanFactory就是一个大工厂。你给它一个名字,它返回一个对象。但它的厉害之处在于:延迟加载多种创建方式

工厂方法 说明 使用场景
getBean(String name) 按名称获取 最常用
getBean(Class<T> requiredType) 按类型获取 避免类型转换
getBean(String name, Object... args) 带参数创建 原型Bean

你想想看,如果没有这个工厂,你得自己管理所有对象的创建和销毁。那代码得多乱?

2. MyBatis的SqlSessionFactory:抽象工厂的变体

MyBatis的SqlSessionFactory其实是个抽象工厂。它生产SqlSession,而SqlSession又分多种实现:DefaultSqlSessionSqlSessionManager等。

// 工厂接口
public interface SqlSessionFactory {
    SqlSession openSession();
    SqlSession openSession(boolean autoCommit);
    SqlSession openSession(Connection connection);
    SqlSession openSession(TransactionIsolationLevel level);
    SqlSession openSession(ExecutorType execType);
}

我个人习惯用openSession(ExecutorType.BATCH)来做批量插入,性能能提升好几倍。这个技巧在数据迁移时特别有用。

3. Netty的ChannelFactory:反射工厂

Netty的工厂模式体现在ChannelFactory上。它通过反射来创建Channel实例,这样你只需要指定一个Class,它就能帮你new出来。

// 实际使用
bootstrap.channel(NioSocketChannel.class);
// 内部就是通过ReflectiveChannelFactory反射创建

为什么要用反射?因为Netty支持多种IO模型:NIO、Epoll、KQueue。用工厂模式,切换IO模型只需要改一行代码。这就是工厂模式的威力——解耦

三、观察者模式:从事件驱动到异步通知

观察者模式,说白了就是「发布-订阅」。一个对象状态变了,通知所有关注它的对象。框架里到处都是这个模式。

1. Spring的事件机制:ApplicationListener

Spring的事件机制是观察者模式的经典实现。你可以自定义事件,然后监听它。

// 1. 定义事件
public class OrderEvent extends ApplicationEvent {
    private Long orderId;
    public OrderEvent(Object source, Long orderId) {
        super(source);
        this.orderId = orderId;
    }
}

// 2. 发布事件
applicationContext.publishEvent(new OrderEvent(this, 123L));

// 3. 监听事件
@Component
public class OrderEventListener implements ApplicationListener<OrderEvent> {
    @Override
    public void onApplicationEvent(OrderEvent event) {
        // 处理订单逻辑
        System.out.println("收到订单:" + event.getOrderId());
    }
}

我在项目中用这个做过异步日志。订单创建后,发布一个事件,监听器里写日志。这样主流程不会被日志拖慢。但要注意:默认是同步的,如果监听器抛异常,主流程也会挂。我建议用@Async注解改成异步。

2. MyBatis的插件机制:Interceptor

MyBatis的插件其实也是观察者模式。你写一个Interceptor,它就能拦截SQL执行。说白了,就是在执行前后「观察」并「干预」。

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class SqlInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 执行前
        System.out.println("SQL执行前...");
        Object result = invocation.proceed();
        // 执行后
        System.out.println("SQL执行后...");
        return result;
    }
}

我曾经用这个做过SQL审计。所有更新操作都记录到日志表里。但要注意:插件会影响性能,别在核心链路上加太多拦截器。

3. Netty的ChannelPipeline:责任链+观察者

Netty的ChannelPipeline是个混合体。它既是责任链模式,也用了观察者模式。每个Handler就像观察者,当有数据到达时,Pipeline会依次通知它们。

核心流程:

  1. Channel注册到EventLoop
  2. 数据到达时,EventLoop触发Channel的读事件
  3. Pipeline从head开始,依次调用每个Handler的channelRead方法
  4. Handler处理完,调用ctx.fireChannelRead()传给下一个

你想想看,如果没有这个机制,你得自己写一个循环来调用所有Handler。那代码得多难维护?

四、知识体系总览

下面这张图,是我梳理的三个框架中三种设计模式的对应关系。你看一眼,心里就有谱了。

设计模式在框架源码中的体现 单例模式 Spring: 三级缓存单例Bean MyBatis: SqlSessionFactory Netty: @Sharable Handler 工厂模式 Spring: BeanFactory MyBatis: SqlSessionFactory Netty: ReflectiveChannelFactory 观察者模式 Spring: ApplicationListener MyBatis: Interceptor插件 Netty: ChannelPipeline 核心要点 单例:控制实例数量 | 工厂:解耦创建过程 | 观察者:实现事件驱动 三个模式在框架中往往组合使用,比如Spring的BeanFactory既是工厂也是单例容器

五、避坑指南与个人经验

最后,我总结几个实战中容易踩的坑,都是我曾经的血泪史:

  • 单例的线程安全问题:我曾经在Spring单例Bean里放了一个HashMap做缓存,结果没加锁,线上出现数据错乱。记住:单例Bean里如果有可变状态,必须考虑并发
  • 工厂模式不要滥用:我见过有人给每个DAO都写一个工厂类,结果工厂比业务代码还多。其实Spring的@Bean注解已经够用了,别自己造轮子。
  • 观察者模式注意内存泄漏:在Netty里,如果你注册了Listener但忘记移除,会导致对象无法被GC。我早期写的一个网关项目,就是因为这个原因导致内存持续增长。

我的建议:学设计模式,别死记硬背。打开你用的框架源码,找到这些模式的实现,一行一行看。看懂了,你才算真正掌握了。

好了,这一章就到这里。记住:设计模式不是目的,让代码更清晰、更可维护才是。下次你写代码时,想想Spring是怎么做的,你也能写出框架级的代码。