203|常用设计模式:工厂、策略、模板、责任链、观察者、代理的工程应用

架构设计与系统设计约 22 分钟阅读12,335 字符
知识库文档 204 / 251

一、引言:为什么架构师必须懂设计模式

设计模式不是面试八股文,而是工程实践中沉淀下来的可复用设计经验。在架构设计层面,GoF 23 种设计模式里,有六种模式的出场率远超其他——工厂、策略、模板方法、责任链、观察者和代理。这六种模式几乎出现在每一个中大型 Java 项目的代码中,Spring 框架本身就是它们的集大成者。

本文不按教科书讲所有模式,而是聚焦这六种模式在工程中的真实应用:Spring 源码中怎么用、业务代码中怎么选、设计时容易踩哪些坑。读完你会发现,设计模式不是背出来的,是你写代码时自然就用到的。

阅读时间:约 40-50 分钟


二、六种模式全景概览

在深入每个模式之前,先建立整体认知:

模式 核心思想 一句话总结 Spring 中的典型应用
工厂模式 将对象创建逻辑封装起来,客户端无需关心具体类 用工厂方法替代 new BeanFactory、ApplicationContext
策略模式 定义算法族,让它们可以互相替换 把 if-else 换成策略类 Resource 接口、InstantiationStrategy
模板方法 父类定义算法骨架,子类实现具体步骤 不变的框架,变化的细节 JdbcTemplate、RestTemplate、AbstractApplicationContext.refresh()
责任链 多个处理器串联,请求沿着链传递直到被处理 请求的接力赛 FilterChain、HandlerInterceptor、DispatcherServlet
观察者 主题状态变化时自动通知所有观察者 一对多的发布-订阅 ApplicationEvent、ApplicationListener
代理模式 为对象提供替身,控制对其的访问 在调用前后做增强 AOP 动态代理、@Transactional、@Cacheable

文章配图
文章配图

图1:六种设计模式在工程应用中的关系全景


三、工厂模式:解耦对象创建

3.1 核心概念

工厂模式解决的核心问题是:客户端不需要知道具体类的名称,也不需要知道创建细节,只需要告诉工厂"我要一个能做什么的"对象即可。

工厂模式有三个层次:

  • 简单工厂:一个工厂类根据参数创建不同产品
  • 工厂方法:定义一个创建对象的接口,让子类决定实例化哪一个类
  • 抽象工厂:创建一系列相关或相互依赖的对象,无需指定具体类

文章配图
文章配图

图2:工厂模式三层次进化——简单工厂→工厂方法→抽象工厂

3.2 工程实践

Spring 中的工厂模式是工厂模式的终极范例。BeanFactory 是工厂方法模式的体现:

java
// ApplicationContext 就是最大的工厂
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = ctx.getBean("userService", UserService.class);

Spring 的 BeanFactory 层次结构:

  • BeanFactory — 顶层接口,定义了获取 bean 的基本方法
  • HierarchicalBeanFactory — 增加父子容器支持
  • AutowireCapableBeanFactory — 增加自动装配能力
  • ConfigurableBeanFactory — 增加配置能力
  • DefaultListableBeanFactory — 最终落地实现

业务场景:当订单系统需要对接多种支付方式(微信、支付宝、银联),工厂模式可以将支付渠道的创建与业务逻辑解耦:

java
// 支付工厂
public class PaymentFactory {
    private static final Map<String, PaymentService> paymentMap = new HashMap<>();

    static {
        paymentMap.put("WECHAT", new WechatPaymentService());
        paymentMap.put("ALIPAY", new AlipayPaymentService());
        paymentMap.put("UNIONPAY", new UnionPayPaymentService());
    }

    public static PaymentService getPayment(String channel) {
        PaymentService service = paymentMap.get(channel);
        if (service == null) {
            throw new IllegalArgumentException("Unsupported payment channel: " + channel);
        }
        return service;
    }
}

常见误区:在工厂方法里直接写业务逻辑,把工厂类变成"万能类"。工厂只负责创建对象,不负责处理业务。


四、策略模式:消除 if-else 的利器

4.1 核心概念

策略模式定义了一系列算法,将每个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。

策略模式的经典结构:

  • Context(上下文):持有一个策略引用,负责调用策略
  • Strategy(抽象策略):定义算法的公共接口
  • ConcreteStrategy(具体策略):实现具体的算法

文章配图
文章配图

图3:策略模式完整类图——Context、Strategy接口与具体实现

4.2 工程实践

Spring 中的策略模式:Resource 接口是策略模式的典型应用。Spring 根据资源路径的前缀选择合适的 Resource 实现:

java
// Spring 根据路径前缀选择不同的 Resource 策略
Resource res1 = ctx.getResource("classpath:config.xml");    // ClassPathResource
Resource res2 = ctx.getResource("file:/path/to/file.txt");  // FileSystemResource
Resource res3 = ctx.getResource("https://example.com");     // UrlResource

业务场景:电商平台的优惠券计算,不同优惠规则(满减、折扣、立减)用策略模式实现:

java
// 策略接口
public interface DiscountStrategy {
    BigDecimal calculate(BigDecimal originalPrice);
    String getType();
}

// 满减策略
@Component
public class FullReductionStrategy implements DiscountStrategy {
    public BigDecimal calculate(BigDecimal price) {
        return price.compareTo(new BigDecimal("200")) >= 0
            ? price.subtract(new BigDecimal("30")) : price;
    }
    public String getType() { return "FULL_REDUCTION"; }
}

// 折扣策略
@Component
public class PercentDiscountStrategy implements DiscountStrategy {
    public BigDecimal calculate(BigDecimal price) {
        return price.multiply(new BigDecimal("0.85"));
    }
    public String getType() { return "PERCENT_DISCOUNT"; }
}

// 上下文(Spring 自动注入所有策略实现)
@Service
public class DiscountContext {
    private final Map<String, DiscountStrategy> strategyMap;

    public DiscountContext(List<DiscountStrategy> strategies) {
        this.strategyMap = strategies.stream()
            .collect(Collectors.toMap(DiscountStrategy::getType, s -> s));
    }

    public BigDecimal applyDiscount(String type, BigDecimal price) {
        DiscountStrategy strategy = strategyMap.get(type);
        if (strategy == null) {
            return price;
        }
        return strategy.calculate(price);
    }
}

策略 vs 工厂:工厂模式关注的是"创建哪个对象",策略模式关注的是"执行哪个算法"。两者经常配合使用:工厂负责创建策略对象,策略负责执行算法。这种组合在 Spring 中随处可见。


五、模板方法模式:不变的框架,变化的细节

5.1 核心概念

模板方法在一个方法中定义算法的骨架,将某些步骤延迟到子类中实现。模板方法让子类可以重新定义算法的某些步骤,而不改变算法的结构。

核心角色:

  • AbstractClass(抽象类):定义模板方法和抽象步骤
  • ConcreteClass(具体类):实现具体的步骤

钩子方法(Hook Method)是模板方法的关键扩展——父类提供默认实现(通常是空方法),子类可以选择性覆盖。

5.2 工程实践

Spring 中的模板方法是 Spring 框架最核心的设计理念之一:

  1. **AbstractApplicationContext.refresh()** — 这是 Spring 容器初始化的模板方法,定义了 12 个步骤的骨架:
java
public void refresh() throws BeansException, IllegalStateException {
    // 1. 准备刷新
    prepareRefresh();
    // 2. 获取 BeanFactory
    ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    // 3. 准备 BeanFactory
    prepareBeanFactory(beanFactory);
    // 4. BeanFactory 后置处理(钩子方法)
    postProcessBeanFactory(beanFactory);
    // 5. 调用 BeanFactory 后置处理器
    invokeBeanFactoryPostProcessors(beanFactory);
    // 6. 注册 Bean 后置处理器
    registerBeanPostProcessors(beanFactory);
    // 7. 初始化消息源
    initMessageSource();
    // 8. 初始化事件广播器
    initApplicationEventMulticaster();
    // 9. 模板方法——留给子类扩展
    onRefresh();
    // 10. 注册监听器
    registerListeners();
    // 11. 实例化所有非懒加载单例
    finishBeanFactoryInitialization(beanFactory);
    // 12. 完成刷新
    finishRefresh();
}

其中 postProcessBeanFactory() 和 onRefresh() 是两个钩子方法,Spring Boot 的 ServletWebServerApplicationContext 正是通过覆盖 onRefresh() 来启动内嵌 Web 服务器的。

文章配图
文章配图

图4:AbstractApplicationContext.refresh() 模板方法12步骤骨架 2. **JdbcTemplate** — 数据库操作的模板方法:

java
// JdbcTemplate 定义了查询的骨架:获取连接 → 创建 Statement → 执行 SQL → 处理 ResultSet → 释放资源
jdbcTemplate.query("SELECT * FROM users WHERE id = ?", ps -> ps.setLong(1, userId), rs -> {
    User user = new User();
    user.setId(rs.getLong("id"));
    user.setName(rs.getString("name"));
    return user;
});
  1. **RestTemplate** — HTTP 请求的模板方法

业务场景:数据导出功能,不同格式(Excel、PDF、CSV)的导出流程相同,但具体实现不同:

java
public abstract class DataExporter<T> {
    // 模板方法——定义导出骨架
    public final void export(List<T> data, OutputStream outputStream) {
        validate(data);                    // 1. 校验数据
        List<T> filtered = filter(data);   // 2. 过滤(钩子方法)

        byte[ ] content = convert(filtered); // 3. 转换(抽象方法)

        write(outputStream, content);      // 4. 写入
        afterExport(outputStream);         // 5. 后置处理(钩子方法)
    }

    private void validate(List<T> data) {
        if (data == null || data.isEmpty()) {
            throw new IllegalArgumentException("Export data cannot be empty");
        }
    }

    protected List<T> filter(List<T> data) { return data; }  // 钩子:默认不过滤

    protected abstract byte[ ] convert(List<T> data);         // 子类实现


    private void write(OutputStream os, byte[ ] content) { /* 写入 */ }

    protected void afterExport(OutputStream os) {}           // 钩子:默认空
}

模板方法 vs 策略模式:模板方法用继承(子类覆写父类方法),策略模式用组合(注入策略接口)。模板方法适合"骨架固定、步骤可变"的场景,策略模式适合"整体算法可切换"的场景。


六、责任链模式:请求的接力赛

6.1 核心概念

责任链模式让多个对象都有机会处理请求,从而避免请求发送者和接收者之间的耦合。将这些对象连成一条链,请求沿着链传递,直到有对象处理它为止。

两种变体:

  • 纯责任链:要么处理请求,要么传给下一个(不处理则不传)
  • 不纯责任链:每个处理器都可以对请求做部分处理,然后传给下一个

文章配图
文章配图

图5:Spring MVC 请求处理全链路——责任链模式

6.2 工程实践

Spring 中的责任链模式无处不在:

  1. Servlet Filter Chain — 最经典的责任链:
java
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    // 前置处理
    logRequest(request);
    // 传递给下一个过滤器
    chain.doFilter(request, response);
    // 后置处理
    logResponse(response);
}
  1. Spring Security Filter Chain — 多过滤器串联的安全链:
code
UsernamePasswordAuthenticationFilter → BasicAuthenticationFilter → ... → FilterSecurityInterceptor
  1. Spring MVC HandlerInterceptor — 拦截器责任链:
java
public class AuthInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");
        if (!isValidToken(token)) {
            throw new UnauthorizedException();
        }
        return true;  // 返回 true 继续传递,false 则中断
    }
}
  1. Spring AOP 拦截器链 — MethodInterceptor 链式调用

业务场景:订单审核流程,不同级别的审批人在链上按顺序处理:

java
// 审批处理器抽象
public abstract class ApprovalHandler {
    protected ApprovalHandler next;

    public ApprovalHandler setNext(ApprovalHandler next) {
        this.next = next;
        return next;  // 链式构建
    }

    public void handle(Order order) {
        if (canApprove(order)) {
            doApprove(order);
        } else if (next != null) {
            next.handle(order);
        } else {
            throw new RuntimeException("订单审批链已到尽头,无法处理");
        }
    }

    protected abstract boolean canApprove(Order order);
    protected abstract void doApprove(Order order);
}

// 构建链
ApprovalHandler chain = new ManagerHandler();
chain.setNext(new DirectorHandler())
     .setNext(new VPHandler())
     .setNext(new CEOHandler());
chain.handle(order);

关键设计要点:

  • 链的构建要灵活,支持动态增减处理器
  • 每个处理器职责要单一,不要一个处理器干多件事
  • 注意链的长度,过长会影响性能,也可能导致请求被遗漏
  • 不要在链中修改共享状态,除非你很清楚自己在做什么

七、观察者模式:一对多的自动通知

7.1 核心概念

观察者模式定义了对象之间的一对多依赖关系,当一个对象(主题/Subject)状态发生变化时,所有依赖它的对象(观察者/Observer)都会得到通知并自动更新。

核心角色:

  • Subject(主题):维护观察者列表,提供注册/移除/通知方法
  • Observer(观察者):定义更新接口
  • ConcreteSubject(具体主题):状态变化时通知所有观察者
  • ConcreteObserver(具体观察者):收到通知后执行具体逻辑

文章配图
文章配图

图6:Spring 事件机制——观察者模式完整流程

7.2 工程实践

Spring 事件机制是观察者模式的最佳实践:

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

// 2. 发布事件
@Service
public class OrderService {
    @Autowired
    private ApplicationEventPublisher publisher;

    @Transactional
    public void createOrder(Order order) {
        // 保存订单...
        // 发布事件
        publisher.publishEvent(new OrderCreatedEvent(this, order.getId()));
    }
}

// 3. 观察者1:发送通知
@Component
public class OrderNotificationListener {
    @EventListener
    @Async  // 异步处理,不阻塞主流程
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 发送短信/推送通知
    }
}

// 4. 观察者2:更新统计
@Component
public class OrderStatisticsListener {
    @EventListener
    @Order(2)  // 控制执行顺序
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 更新销量统计
    }
}

Spring 事件机制的核心实现:

  • ApplicationEventMulticaster 负责广播事件
  • 默认使用 SimpleApplicationEventMulticaster,同步调用所有监听器
  • 可通过 @Async 注解让监听器异步执行
  • 事件可以用 @TransactionalEventListener 绑定事务阶段(提交后、回滚后等)

观察者 vs 发布-订阅:发布-订阅模式是观察者模式的演进,增加了 Broker 中间层,实现发布者和订阅者的完全解耦。消息队列(Kafka、RocketMQ)就是发布-订阅的工程实现。

常见陷阱:

  • 同步观察者会阻塞主流程——订单创建后同步发邮件,如果邮件服务慢,订单创建也慢
  • 事件循环——A 事件触发 B 事件,B 事件又触发 A 事件
  • 事务边界——事件发布在事务提交前还是提交后,直接影响数据一致性

八、代理模式:控制访问的替身

8.1 核心概念

代理模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问。代理和被代理对象实现相同的接口,客户端无感知。

代理模式的三种类型:

  • 静态代理:编译时生成代理类,需要为每个被代理类手写代理
  • JDK 动态代理:运行时生成代理类,要求被代理类必须实现接口
  • CGLIB 动态代理:运行时生成代理类,通过继承实现,不需要接口

文章配图
文章配图

图7:代理模式三种实现对比——静态代理、JDK动态代理、CGLIB代理

8.2 工程实践

Spring AOP 是代理模式的巅峰应用:

java
// @Transactional 注解的背后就是代理模式
@Service
public class UserService {
    @Transactional
    public void createUser(User user) {
        userMapper.insert(user);
        // 如果这里抛异常,事务自动回滚
    }
}

// Spring 实际执行的是:
// 1. 创建 UserService 的代理对象
// 2. 调用 createUser() 时,代理对象先执行事务拦截器
// 3. 事务拦截器:开启事务 → 调用目标方法 → 提交/回滚事务

JDK 动态代理 vs CGLIB 代理:

特性 JDK 动态代理 CGLIB 代理
原理 基于接口,java.lang.reflect.Proxy 基于继承,ASM 字节码操作
要求 必须实现接口 不需要接口,但不能代理 final 类/方法
性能 反射调用,创建开销小 字节码生成,创建开销大,调用快
Spring 默认 有接口时默认使用 无接口时使用

MyBatis Mapper 代理是另一个经典案例:

java
// 你只定义了一个接口,MyBatis 通过动态代理为你生成实现
@Mapper
public interface UserMapper {
    User selectById(Long id);
}

// MyBatis 内部做的事情:
// 1. 通过 JDK 动态代理创建 Mapper 接口的代理对象
// 2. 调用方法时,代理对象拦截调用
// 3. 根据方法名查找对应的 SQL(XML 或注解)
// 4. 执行 SQL,映射结果,返回

代理模式 vs 装饰器模式:代理模式强调的是"控制访问"(权限检查、延迟加载、远程调用),装饰器模式强调的是"增强功能"。AOP 中既有代理的影子(控制事务边界),也有装饰器的影子(添加日志、缓存)。

代理模式的性能影响:

  • 每次代理调用都会多一层方法调用栈,但影响微乎其微(纳秒级)
  • 真正影响性能的是代理逻辑本身(比如 AOP 切面中的反射、数据库查询等)
  • Spring 的 @Transactional 代理是单例代理,不会有额外对象创建开销

九、六种模式的关系与组合

9.1 模式之间的关联

这六种模式不是孤立的,它们在实际工程中经常组合使用:

  • 工厂 + 策略:工厂创建策略对象,策略执行算法。Spring 的 InstantiationStrategy 正是如此。
  • 模板方法 + 策略:模板方法定义骨架,某些步骤用策略模式实现。JdbcTemplate 的 StatementCallback 就是策略接口。
  • 责任链 + 观察者:Spring Security 的 Filter Chain 中,每个 Filter 既是责任链上的节点,也是安全事件的观察者。
  • 代理 + 责任链:Spring AOP 的拦截器链——多个 MethodInterceptor 组成责任链,而 AOP 本身又是通过代理实现。
  • 工厂 + 代理:Spring 的 BeanPostProcessor 可以在 Bean 初始化后生成代理对象,本质上是用工厂创建代理。

9.2 Spring 中的组合范例

以 Spring 处理一个 HTTP 请求的全链路为例:

code
请求到达 → DispatcherServlet(前端控制器)
  → HandlerMapping(策略模式:根据请求选择 Handler)
  → HandlerInterceptor 链(责任链模式:preHandle → handler → postHandle)
  → HandlerAdapter(适配器模式:调用真正的 Controller)
    → Controller 中注入的 Service(代理模式:AOP 增强)
      → Service 中的业务逻辑(策略模式/模板方法模式)
  → 返回结果

十、设计模式的选择原则

10.1 什么时候用哪种模式

你的问题 合适的模式
创建对象的逻辑复杂,或需要根据条件动态选择具体类 工厂模式
大量 if-else 或 switch 分支选择不同算法 策略模式
多个流程有相同的骨架,但部分步骤不同 模板方法
多个处理者需要依次处理同一个请求 责任链
一个对象的状态变化需要通知多个其他对象 观察者
需要对某个对象的方法调用做增强或控制 代理模式

文章配图
文章配图

图8:六种设计模式选择决策树

10.2 不要过度设计

设计模式是解决特定问题的手段,不是目的。几个实用原则:

  1. 先写简单的代码,再重构到模式:不要一开始就想着"我要用策略模式",先写 if-else,等分支超过 3 个时再重构
  2. 模式不是越多越好:一个类实现了 3 种模式,大概率是过度设计了
  3. 关注业务语义,而不是模式名称:代码评审时说"这里用责任链解耦了审批流程"比"这里用了责任链模式"更有价值
  4. Spring 已经帮你做了很多:先理解 Spring 用了哪些模式,再用这些模式写自己的业务代码

十一、总结

这六种设计模式是 Java 后端工程师日常工作中最常用的模式。它们不是面试的八股,而是你读 Spring 源码时的钥匙、写业务代码时的工具、做架构设计时的积木。

记住三点:

  1. 模式服务于业务——先有业务问题,再有模式选择
  2. Spring 是最好的老师——阅读 Spring 源码,理解模式在真实工程中的用法
  3. 组合优于继承——策略模式比模板方法更灵活,因为组合比继承更容易扩展

面试官视角提问

以下问题来自真实面试场景,建议在理解文章内容后思考,尝试用自己的话回答。

Q1:Spring 的 BeanFactory 和 ApplicationContext 分别用了什么设计模式?设计上有何不同?为什么需要两个接口?

Q2:你在项目中用策略模式替代了复杂的 if-else,但后来发现策略类数量膨胀到 30+,维护成本很高。你会怎么优化?有没有遇到过策略模式反被滥用的情况?

Q3:模板方法模式和策略模式的核心区别是什么?在什么场景下你更倾向于用模板方法而不是策略模式?为什么说"组合优于继承"在这个选择中体现得很明显?

Q4:Spring Security 的 Filter Chain 和 Spring MVC 的 HandlerInterceptor 都用了责任链模式,它们的实现有什么不同?如果让你设计一个可动态增减处理器的责任链,你会怎么设计?

Q5:Spring 事件机制中,@EventListener 默认是同步的。如果一个订单创建事件需要触发 5 个监听器,其中一个调用外部服务耗时 3 秒,会对系统产生什么影响?你会怎么解决?使用 @Async 又带来了什么新问题?

Q6:Spring AOP 什么时候用 JDK 动态代理,什么时候用 CGLIB?如果一个类实现了接口但你也想代理它没有在接口中定义的方法,会发生什么?@Transactional 自调用失效是什么原因,怎么解决?

Q7:观察者模式和发布-订阅模式有什么区别?为什么说"消息队列是实现发布-订阅的技术手段,但不是观察者模式"?在你的项目中,什么场景用 Spring 事件就够了,什么场景必须上消息队列?

Q8:请设计一个电商订单系统,要求:支持多种支付方式(微信、支付宝、银联)、订单创建后自动发通知和更新统计、不同金额的订单走不同审批流程、订单状态变更需要记录日志和执行后置操作。请说明你会用到哪些设计模式,以及为什么这么设计。


参考回答

A1:BeanFactory vs ApplicationContext

设计模式:

  • BeanFactory 是工厂方法模式的体现——定义了获取 Bean 的接口,由 DefaultListableBeanFactory 等具体工厂实现
  • ApplicationContext 在工厂方法之上,额外使用了模板方法模式(refresh() 方法)、观察者模式(事件发布/监听)、策略模式(ResourceLoader 选择不同资源加载策略)

设计上的不同:

维度 BeanFactory ApplicationContext
实例化时机 延迟加载(lazy-init) 预初始化(默认 eager)
功能范围 仅 IoC 容器基本功能 IoC + AOP + 事件 + 国际化 + 资源加载
BeanPostProcessor 需手动注册 自动注册
使用场景 内存敏感环境(如 Applet) 企业级应用(99% 场景)

为什么需要两个接口?

  • 单一职责原则:BeanFactory 只做 IoC 容器,接口简洁;ApplicationContext 是它的超集,提供更丰富的企业级能力
  • 向后兼容:BeanFactory 是最初的设计,ApplicationContext 是后续扩展,保留前者保证兼容性
  • 分层设计:BeanFactory 是底层"发动机",ApplicationContext 是"整车"——你可以只换发动机(比如用 StaticApplicationContext 做单元测试),而不影响上层接口

A2:策略模式膨胀的优化

优化策略(按优先级排列):

  1. 策略分组 + 工厂路由:将 30+ 策略按业务领域分组(如支付策略、优惠策略、通知策略各自独立),每个领域有独立的策略工厂,避免一个巨型策略工厂
java
// 不要把所有策略塞进一个 Map
// 而是按领域拆分
Map<String, PaymentStrategy> paymentStrategies;
Map<String, DiscountStrategy> discountStrategies;
  1. 策略 + 规格模式(Specification Pattern):不是用简单的 type 字符串匹配,而是让每个策略声明自己的适用条件,由路由引擎自动匹配
java
public interface DiscountStrategy {
    boolean supports(Order order);  // 策略自己声明适用条件
    BigDecimal calculate(Order order);
}
// 路由时自动匹配
DiscountStrategy s = strategies.stream()
    .filter(st -> st.supports(order))
    .findFirst().orElse(defaultStrategy);
  1. 用规则引擎替代简单策略:当策略数量膨胀到 30+,且规则频繁变化,考虑引入规则引擎(Drools、EasyRules),将策略配置化而不是硬编码
  2. 策略模式反被滥用的情况(我面试时常问的跟进问题):
    • 一个策略里只有一行代码,也要拆成独立的策略类——过度设计
    • 策略之间逻辑高度相似(80% 相同),没有抽取公共抽象层——代码重复
    • 策略类依赖了太多外部服务,导致单元测试困难——职责不清
    • 核心判断标准:如果你删掉策略模式,用 if-else 重写,代码量减少且可读性提高,那你就是在滥用策略模式

A3:模板方法 vs 策略模式

核心区别:

维度 模板方法 策略模式
复用方式 继承(子类覆写父类方法) 组合(注入策略接口)
控制权 父类控制流程(好莱坞原则:"Don't call us, we'll call you") Context 委托给策略,策略有更多自主权
扩展粒度 只能替换算法中的某些步骤 可以替换整个算法
灵活性 子类只能覆写父类暴露的钩子方法 策略可以随时替换,甚至运行时动态切换
代码复用 父类实现公共步骤,子类复用 策略之间代码复用需要额外手段(抽象类等)

什么时候用模板方法:

  • 算法骨架非常稳定,比如 JdbcTemplate 的"获取连接→执行→释放资源"流程几十年不变
  • 需要严格控制流程顺序,不允许子类改变调用次序
  • 有大量公共代码需要复用(如 DataExporter 示例中的校验、写入逻辑)

"组合优于继承"在这个选择中的体现:

  • 模板方法用继承,子类与父类强耦合——父类一改,所有子类受影响
  • 策略模式用组合,策略接口不变时,Context 和策略可以独立演化
  • 实际工程中,优先考虑策略模式——除非你真的很确定骨架不会变
  • 这就是为什么 Spring 的 JdbcTemplate 把 StatementCallback 作为策略接口注入,而不是让用户继承 JdbcTemplate 来覆写方法

A4:Filter Chain vs HandlerInterceptor + 动态责任链设计

实现不同:

维度 Servlet Filter Chain Spring HandlerInterceptor
标准 Java EE / Servlet 规范 Spring MVC 专有
作用范围 所有请求(包括静态资源) 仅进入 DispatcherServlet 的请求
粒度 只能访问 Request/Response 对象 可以访问 Handler(Controller 方法)
链控制 chain.doFilter() 传递 preHandle() 返回 true/false
执行顺序 Filter 链 → Servlet → Filter 链返回 preHandle → Handler → postHandle → afterCompletion
中断后 请求直接返回,不进入 Servlet 返回 false 后,后续拦截器和 Handler 都不执行

Spring Security Filter Chain 的特殊之处:

  • Spring Security 的 FilterChainProxy 内部维护了多个安全过滤器链(SecurityFilterChain),根据 URL 匹配选择不同的链
  • 每个链包含多个 Security Filter(UsernamePasswordAuthenticationFilter → BasicAuthenticationFilter → ... → FilterSecurityInterceptor),这些 Filter 本身就是责任链

动态责任链设计(面试官想听的回答):

java
// 1. 定义处理器接口
public interface ChainHandler<T> {
    void handle(T context, Chain<T> chain);
    int order();  // 排序用
    default boolean enabled() { return true; }  // 动态开关
}

// 2. 链的实现
public class Chain<T> {
    private final List<ChainHandler<T>> handlers;
    private int index = 0;

    public Chain(List<ChainHandler<T>> handlers) {
        this.handlers = handlers;
    }

    public void proceed(T context) {
        while (index < handlers.size()) {
            ChainHandler<T> handler = handlers.get(index);
            if (handler.enabled()) {
                index++;
                handler.handle(context, this);  // handler 内部决定是否调用 chain.proceed()
                return;
            }
            index++;
        }
    }

    // 动态增删
    public void addHandler(ChainHandler<T> handler) {
        handlers.add(handler);
        handlers.sort(Comparator.comparingInt(ChainHandler::order));
    }
    public void removeHandler(Class<?> handlerType) {
        handlers.removeIf(h -> handlerType.isInstance(h));
    }
}

关键设计要点:

  • 用 enabled() 方法支持运行时开关(通过配置中心动态启停)
  • 每个 handler 调用 chain.proceed() 决定是否继续传递——控制权交给 handler
  • 用 order() 决定顺序,新增 handler 时自动排序
  • 考虑线程安全——如果运行时动态增删,用 CopyOnWriteArrayList 或加锁

A5:Spring 事件同步阻塞与异步化

影响分析:

假设订单创建接口的 P99 目标是 200ms,5 个同步监听器各自耗时:

  • 监听器1(发通知):50ms
  • 监听器2(更新统计):20ms
  • 监听器3(调用外部服务):3000ms ← 致命
  • 监听器4(记录日志):10ms
  • 监听器5(发券):30ms

总耗时 = 50 + 20 + 3000 + 10 + 30 = 3110ms,远超 200ms 目标,用户体验极差。

解决方案(按推荐顺序):

  1. **@Async** 异步化(最常用):
java
@EventListener
@Async
public void handleOrderCreated(OrderCreatedEvent event) { ... }

需要配置线程池:

java
@Bean("eventExecutor")
public Executor eventExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(50);
    executor.setQueueCapacity(500);
    executor.setRejectedExecutionHandler(new CallerRunsPolicy());
    return executor;
}
  1. **@TransactionalEventListener**:对于需要"事务提交后"再执行的监听器(如发通知),使用事务事件监听器,避免事务未提交就执行
  2. 消息队列解耦:对于耗时 3 秒的外部服务调用,发一条 MQ 消息,由消费者异步处理,彻底解耦

使用 **@Async** 带来的新问题:

  • 事务一致性:异步执行时原事务可能已提交,如果监听器依赖事务内的数据,可能读到脏数据 → 用 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
  • 异常丢失:异步线程的异常不会抛给调用方,静默失败 → 必须配置全局 AsyncUncaughtExceptionHandler
  • 线程池拒绝:流量高峰时队列满,任务被拒绝 → 配置 CallerRunsPolicy 或 DiscardOldestPolicy,并监控线程池指标
  • 上下文传递:RequestContextHolder、SecurityContext 等线程绑定的上下文在异步线程中丢失 → 使用 TaskDecorator 传递上下文
  • 顺序性:异步后执行顺序不可控 → 如果监听器之间有依赖,用 @Order + 同步模式,或通过 MQ 的 topic 分区保证顺序

A6:JDK 动态代理 vs CGLIB + 自调用失效

Spring 的选择逻辑(DefaultAopProxyFactory):

java
public AopProxy createAopProxy(AdvisedSupport config) {
    if (config.isProxyTargetClass() || !hasUserSuppliedInterfaces(config)) {
        // proxyTargetClass=true 或目标类没有实现接口 → CGLIB
        return new ObjenesisCglibAopProxy(config);
    }
    // 默认:有接口 → JDK 动态代理
    return new JdkDynamicAopProxy(config);
}

Spring Boot 2.x 起默认行为变化:spring.aop.proxy-target-class=true(默认用 CGLIB),因为 Spring Boot 认为大部分场景直接用 CGLIB 更简单

类实现了接口但想代理非接口方法:

  • 如果使用 JDK 动态代理(默认行为),代理对象只能调用接口中定义的方法
  • 如果你拿到的是 JDK 代理对象,调用 ((UserService) proxy).nonInterfaceMethod() 会抛出 ClassCastException
  • 解决方案:@EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB

**@Transactional** 自调用失效:

java
@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        // ... 事务生效
        this.updateInventory(order);  // ← 自调用,事务不生效!
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateInventory(Order order) {
        // 这里的事务注解不会生效
    }
}

失效原因:@Transactional 是通过 AOP 代理实现的。this.updateInventory() 是直接调用目标对象的方法,绕过了代理,事务拦截器不会被触发。

解决方案:

  1. 注入自己(最常见):@Autowired private OrderService self; 然后用 self.updateInventory(order)
  2. 拆到另一个 Service(推荐):将 updateInventory 移到 InventoryService 中,通过注入调用
  3. AopContext.currentProxy():((OrderService) AopContext.currentProxy()).updateInventory(order),需要 @EnableAspectJAutoProxy(exposeProxy = true)
  4. 使用 AspectJ 编译期织入(不推荐,太重):不走代理,直接修改字节码

A7:观察者模式 vs 发布-订阅模式

核心区别:

维度 观察者模式 发布-订阅模式
耦合度 主题知道观察者(有观察者列表) 发布者和订阅者完全解耦,通过 Broker 通信
通信方式 主题直接调用观察者的 update() 方法 发布者发消息到 Topic,Broker 推送给订阅者
作用范围 同一进程内(JVM 内) 跨进程、跨服务、跨机房
典型实现 Spring ApplicationEvent、Java Observer Kafka、RocketMQ、RabbitMQ

"消息队列是实现发布-订阅的技术手段,但不是观察者模式":

  • 观察者模式是设计模式(概念层面的抽象),定义的是角色和交互关系
  • 消息队列是中间件(技术层面的实现),是发布-订阅模式的一种工程落地方式
  • 就像"策略模式"是概念,"Spring 的 List<DiscountStrategy> 自动注入"是具体实现

场景选择:

场景 Spring 事件 消息队列
同一服务内,两个模块之间解耦 ✅ 最佳选择 ❌ 杀鸡用牛刀
需要事务一致性(事务提交后执行) ✅ @TransactionalEventListener ❌ 需要额外设计
跨服务调用(订单服务 → 通知服务) ❌ 做不到 ✅ 唯一选择
需要消息持久化,保证不丢 ❌ 重启丢失 ✅ 消息持久化到磁盘
需要重试、死信队列 ❌ 需自己实现 ✅ 内置支持
流量削峰(双十一订单洪峰) ❌ 内存扛不住 ✅ MQ 缓冲
数据同步(Canal 监听 binlog → MQ → 多消费者) ❌ 做不到 ✅ 标准方案

面试加分点:能说出"Spring 事件 + 本地事务表 + 定时补偿"的中间方案——在不需要 MQ 的场景下,用 Spring 事件 + 数据库事务表实现"最终一致性"的轻量级方案。


A8:电商订单系统设计综合题

支付方式(工厂模式 + 策略模式):

  • 用工厂模式创建支付渠道实例(PaymentFactory.getPayment(channel))
  • 用策略模式让每个支付渠道实现统一的 PaymentStrategy 接口(pay()、refund()、query())
  • Spring 自动注入 List<PaymentStrategy>,按渠道类型路由

订单创建后自动发通知 + 更新统计(观察者模式):

  • 发布 OrderCreatedEvent 事件
  • 通知监听器:@Async 异步发送短信/推送
  • 统计监听器:@TransactionalEventListener(phase = AFTER_COMMIT) 保证事务提交后再更新统计
  • 如果需要跨服务通知,用消息队列发送 ORDER_CREATED 消息

不同金额的审批流程(责任链模式):

java
// 审批链:金额 < 1000 → 经理审批
//         1000 ≤ 金额 < 10000 → 经理 → 总监
//         金额 ≥ 10000 → 经理 → 总监 → VP
ApprovalChain chain = new ApprovalChain();
chain.addHandler(new ManagerHandler())    // 所有订单经理先审批
     .addHandler(new DirectorHandler())   // >= 1000 总监审批
     .addHandler(new VPHandler());        // >= 10000 VP 审批

订单状态变更记录日志 + 后置操作(观察者模式 + 代理模式):

  • 用观察者模式监听 OrderStatusChangedEvent,日志记录器、后置操作处理器各自监听
  • 用代理模式(AOP)在 OrderService 的状态变更方法上织入日志切面:@AfterReturning("execution(* OrderService.updateStatus(..)) && args(orderId, status)", argNames = "orderId,status")

整体架构:

code
订单创建请求
  → OrderController(接收请求)
    → OrderService.createOrder()
      ◆ 代理模式:@Transactional 事务代理
      ◆ 工厂模式 + 策略模式:PaymentStrategy 支付
      ◆ 观察者模式:发布 OrderCreatedEvent
        → @Async 通知监听器(发短信/推送)
        → @TransactionalEventListener 统计监听器(更新销量)
        → MQ 消息(跨服务通知)
      ◆ 责任链模式:根据金额走审批链
      ◆ 观察者模式:状态变更发布 OrderStatusChangedEvent
        → 日志监听器(记录操作日志)
        → 后置操作监听器(释放优惠券、更新库存等)

为什么这样设计:

  • 支付方式、审批流程、通知方式都是变化点,用策略/责任链/观察者封装变化
  • 每种模式只解决一个维度的问题,单一职责
  • 新增支付渠道只需实现 PaymentStrategy 接口,开闭原则
  • 各组件通过事件/接口通信,低耦合

这是面试中常见的"系统设计 + 设计模式"综合题,面试官想看的不是你把所有模式列一遍,而是你为什么选这个模式而非另一个,以及模式之间的组合方式。