29、设计模式综合案例(下):Spring框架中的设计模式分析
上一讲我们聊了设计模式在JDK和Tomcat里的应用。今天咱们把目光转向Spring——这个Java生态里绕不开的框架。说实话,Spring是我见过设计模式用得最密集、最优雅的项目之一。你想想看,一个框架能火十几年,底层的设计功不可没。
我个人习惯把Spring的设计模式分析分成两块:容器核心和扩展机制。今天咱们就按这个思路来拆解。
核心观点:Spring不是“用了”设计模式,而是“长在”设计模式之上。理解这些模式,你才能真正看懂Spring的源码。
29.1 IoC容器:工厂模式 + 单例模式
先说最基础的。Spring的IoC容器,本质上就是一个超级工厂。
你平时写ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"),这行代码背后发生了什么?说白了,Spring帮你把对象的创建、组装、生命周期管理全包了。这就是工厂模式的典型应用——把“创建对象”这件事从客户端代码里抽离出来。
我在项目中遇到过一个问题:有个同事在Service里直接new Dao(),结果后来Dao层换了实现,改代码改到崩溃。嗯,这就是不用工厂模式的代价。
再看单例模式。Spring Bean默认的作用域就是singleton。为什么?因为大多数Service、Dao是无状态的,没必要每次请求都new一个。Spring容器内部维护了一个singletonObjects的ConcurrentHashMap,所有单例Bean都缓存在这里。
// Spring源码中的单例缓存(简化版)
public class DefaultSingletonBeanRegistry {
// 一级缓存:存放完整的单例Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 获取单例Bean
public Object getSingleton(String beanName) {
return this.singletonObjects.get(beanName);
}
}
这里有个坑要注意:Spring的单例和设计模式里的单例不完全一样。Spring的单例是“容器级别”的——同一个Spring容器里,一个Bean ID只对应一个实例。但如果你启动了多个Spring容器,那就有多个实例了。
避坑指南:我曾经在集群环境下踩过这个坑——每个节点启动一个Spring容器,结果每个节点都持有一个单例Bean的副本。如果你的Bean里存了本地状态,那就会出现数据不一致。解决方案:要么把状态存到Redis,要么把Bean改成prototype作用域。
29.2 AOP:代理模式
AOP是Spring的另一大杀器。它的底层实现,说白了就是代理模式。
Spring AOP支持两种代理:
- JDK动态代理:目标类实现了接口时使用
- CGLIB代理:目标类没有实现接口时使用
我举个例子你就明白了。假设你有一个UserService接口,里面有个saveUser()方法。你想在调用这个方法前后打印日志。传统做法是每个方法里手动加日志代码——这太蠢了。Spring AOP的做法是:
// 定义一个切面
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("调用方法: " + joinPoint.getSignature().getName());
}
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("方法执行完毕: " + joinPoint.getSignature().getName());
}
}
Spring在运行时动态生成一个代理对象,这个代理对象包裹了目标对象。当你调用userService.saveUser()时,实际调用的是代理对象的方法,代理对象先执行切面逻辑,再调用真实的目标方法。
为什么会这样?因为代理模式的核心就是“控制对目标对象的访问”。Spring AOP利用这个模式,把横切关注点(日志、事务、安全)和业务逻辑完美解耦。
个人经验:我建议你在使用AOP时,尽量让目标类实现接口。这样Spring会使用JDK动态代理,性能比CGLIB好一些。而且接口本身就是一种契约,有利于代码的维护和测试。
29.3 模板方法模式:JdbcTemplate、RestTemplate
Spring里有一堆以“Template”结尾的类:JdbcTemplate、RestTemplate、RedisTemplate……它们都用的是模板方法模式。
模板方法模式的思想是:把固定的流程写在父类里,把可变的部分留给子类去实现。你想想看,用JDBC操作数据库,流程是不是固定的?
- 获取连接
- 创建Statement
- 执行SQL
- 处理ResultSet
- 关闭连接
这5步里,第1、2、5步是固定的,第3步的SQL和执行逻辑、第4步的结果处理是变化的。JdbcTemplate把固定流程封装好,你只需要提供SQL和结果处理器就行了:
// 使用JdbcTemplate查询用户
String sql = "SELECT * FROM users WHERE id = ?";
User user = jdbcTemplate.queryForObject(sql, new Object[]{1}, (rs, rowNum) -> {
User u = new User();
u.setId(rs.getLong("id"));
u.setName(rs.getString("name"));
return u;
});
你看,连接管理、异常处理、资源释放这些脏活累活,JdbcTemplate全帮你干了。你只需要关心业务逻辑——这就是模板方法模式的魅力。
我记得有一次接手一个老项目,里面全是手写JDBC的代码,每个方法里都有try-catch-finally,看着就头疼。后来我花了一天时间全部改成JdbcTemplate,代码量直接砍掉60%。
29.4 观察者模式:事件驱动机制
Spring的事件机制是观察者模式的经典实现。它包括三个角色:
- 事件(Event):继承自
ApplicationEvent - 发布者(Publisher):调用
ApplicationEventPublisher.publishEvent() - 监听器(Listener):实现
ApplicationListener接口或使用@EventListener注解
举个实际场景:用户注册成功后,需要发送欢迎邮件、赠送积分、记录日志。如果用同步调用的方式,注册方法里会塞满各种业务代码,耦合度极高。用事件机制就清爽多了:
// 1. 定义事件
public class UserRegisteredEvent extends ApplicationEvent {
private Long userId;
private String email;
// 构造方法、getter...
}
// 2. 发布事件
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher publisher;
public void register(User user) {
// 保存用户到数据库
saveUser(user);
// 发布注册事件
publisher.publishEvent(new UserRegisteredEvent(this, user.getId(), user.getEmail()));
}
}
// 3. 监听事件
@Component
public class EmailListener {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
// 发送欢迎邮件
sendWelcomeEmail(event.getEmail());
}
}
这样做的好处是:发布者和监听者完全解耦。以后想加个“注册后送优惠券”的功能,只需要再写一个监听器就行了,不用改UserService一行代码。
注意:Spring事件默认是同步的。也就是说,发布事件的线程会阻塞,直到所有监听器执行完毕。如果你需要异步处理,记得在监听器方法上加@Async注解,并在配置类上开启@EnableAsync。
29.5 策略模式:资源加载与依赖注入
Spring的ResourceLoader和BeanFactory都用到了策略模式。
拿资源加载来说,Spring支持从classpath、文件系统、URL等多种位置加载资源。每种加载方式对应一个策略实现:
| 资源类型 | 策略实现类 | 前缀示例 |
|---|---|---|
| classpath资源 | ClassPathResource | classpath:beans.xml |
| 文件系统资源 | FileSystemResource | file:/config/beans.xml |
| URL资源 | UrlResource | https://example.com/beans.xml |
你传一个"classpath:beans.xml"给ResourceLoader,它自动识别前缀,选择对应的策略去加载。这就是策略模式——把算法(加载方式)封装成独立的类,让它们可以互相替换。
依赖注入也是策略模式的应用。Spring支持构造器注入、Setter注入、字段注入(@Autowired)。每种注入方式都是一个策略,你可以根据场景选择。我个人习惯用构造器注入,因为能保证依赖不可变,而且方便单元测试。
29.6 知识体系总览
说了这么多,我画了一张图帮你梳理Spring中的设计模式脉络:
29.7 总结与思考
Spring的设计模式远不止今天讲的这些。还有装饰器模式(BeanPostProcessor)、原型模式(prototype作用域的Bean)、责任链模式(拦截器链)等等。但核心思想是一致的:
- 解耦:让组件之间的依赖降到最低
- 可扩展:在不修改核心代码的前提下增加新功能
- 复用:把通用逻辑抽象出来,避免重复造轮子
我个人觉得,学习设计模式最好的方式就是读Spring源码。你不用全读,挑几个核心类看看就够。比如DefaultListableBeanFactory、AopProxy、JdbcTemplate,每个类都是一种设计模式的教科书式实现。
最后送你一句话:设计模式不是万能药,但不懂设计模式,你连Spring的源码都看不懂。
公众号:蓝海资料掘金营,微信deep3321