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操作数据库,流程是不是固定的?

  1. 获取连接
  2. 创建Statement
  3. 执行SQL
  4. 处理ResultSet
  5. 关闭连接

这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的ResourceLoaderBeanFactory都用到了策略模式

拿资源加载来说,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中的设计模式脉络:

Spring框架中的设计模式全景图 IoC容器 工厂模式 + 单例模式 AOP 代理模式 Template系列 模板方法模式 事件机制 观察者模式 资源加载/DI 策略模式 MVC/事务 适配器模式 这些模式相互配合,构成了Spring框架的坚实底座 装饰器模式(BeanPostProcessor) 原型模式(prototype作用域) 责任链模式(拦截器链) 设计模式不是目的,解耦和可扩展才是

29.7 总结与思考

Spring的设计模式远不止今天讲的这些。还有装饰器模式(BeanPostProcessor)、原型模式(prototype作用域的Bean)、责任链模式(拦截器链)等等。但核心思想是一致的:

  • 解耦:让组件之间的依赖降到最低
  • 可扩展:在不修改核心代码的前提下增加新功能
  • 复用:把通用逻辑抽象出来,避免重复造轮子

我个人觉得,学习设计模式最好的方式就是读Spring源码。你不用全读,挑几个核心类看看就够。比如DefaultListableBeanFactoryAopProxyJdbcTemplate,每个类都是一种设计模式的教科书式实现。

最后送你一句话:设计模式不是万能药,但不懂设计模式,你连Spring的源码都看不懂。


公众号:蓝海资料掘金营,微信deep3321