设计模式在框架源码中的体现: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又分多种实现:DefaultSqlSession、SqlSessionManager等。
// 工厂接口
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会依次通知它们。
核心流程:
- Channel注册到EventLoop
- 数据到达时,EventLoop触发Channel的读事件
- Pipeline从head开始,依次调用每个Handler的channelRead方法
- Handler处理完,调用ctx.fireChannelRead()传给下一个
你想想看,如果没有这个机制,你得自己写一个循环来调用所有Handler。那代码得多难维护?
四、知识体系总览
下面这张图,是我梳理的三个框架中三种设计模式的对应关系。你看一眼,心里就有谱了。
五、避坑指南与个人经验
最后,我总结几个实战中容易踩的坑,都是我曾经的血泪史:
- 单例的线程安全问题:我曾经在Spring单例Bean里放了一个HashMap做缓存,结果没加锁,线上出现数据错乱。记住:单例Bean里如果有可变状态,必须考虑并发。
- 工厂模式不要滥用:我见过有人给每个DAO都写一个工厂类,结果工厂比业务代码还多。其实Spring的@Bean注解已经够用了,别自己造轮子。
- 观察者模式注意内存泄漏:在Netty里,如果你注册了Listener但忘记移除,会导致对象无法被GC。我早期写的一个网关项目,就是因为这个原因导致内存持续增长。
我的建议:学设计模式,别死记硬背。打开你用的框架源码,找到这些模式的实现,一行一行看。看懂了,你才算真正掌握了。
好了,这一章就到这里。记住:设计模式不是目的,让代码更清晰、更可维护才是。下次你写代码时,想想Spring是怎么做的,你也能写出框架级的代码。