Observer 与 Pub/Sub 有何不同
1. 总览
Observer 和 Pub/Sub 都用来解决“一个事件发生,多处需要知道”的问题。很多开发者混淆这两个概念,因为它们的思想相似,但在实现方式和耦合程度上有所不同。本文帮助你清楚地区分这两个概念,并提供了各种方式的 Java 代码示例,教你何时选择纯 Observer 模式,何时选择通过 message broker 的 Pub/Sub 架构。
2. Observer Pattern 是什么
Observer 是 GoF 经典行为型模式(behavioral pattern)之一。其思想是:一个 Subject(或 Publisher)维护一个 Observer 列表,当状态改变时,Subject 直接调用每个已注册 Observer 的方法。
public interface OrderObserver {
void onOrderPaid(Order order);
}
public class Order {
private final List<OrderObserver> observers = new ArrayList<>();
private String status = "NEW";
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void markAsPaid() {
this.status = "PAID";
for (OrderObserver observer : observers) {
observer.onOrderPaid(this);
}
}
}
解释:
observers是直接持有感兴趣对象引用的列表。markAsPaid()依次在每个 observer 上调用onOrderPaid(),同一线程中,同步执行。Order知道谁在监听它——这是与 Pub/Sub 区分的关键点。
public class EmailNotifier implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
System.out.println("为已支付订单发送确认邮件");
}
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
为已支付订单发送确认邮件
2.1. Observer 的核心特点
- 直接耦合:
Subject持有对Observer的引用,需要相互导入接口。 - 同步:notify 调用会阻塞,直到所有 observer 处理完毕。
- 进程内:通常在同一个 JVM 内运行,不涉及网络。
- Subject 知道 Observer 存在:如果忘记调用
removeObserver(),容易产生内存泄漏,因为Subject一直持有已废弃 observer 的引用。
注意:经典 Observer pattern 本身没有安全的 unsubscribe 机制。如果你在长生命周期的应用中使用(比如 UI 组件、注册后销毁的监听器),记得在对象销毁时调用
removeObserver(),避免内存泄漏。
3. Pub/Sub 是什么
Pub/Sub(发布/订阅)通过一个中间层——称为 broker、event bus 或 topic——完全解耦 Publisher 和 Subscriber。Publisher 不知道谁在订阅,只是把消息“发射”到某个 topic/channel。
public record OrderPaidEvent(String orderId, double amount) {}
public class OrderService {
private final EventBus eventBus;
public OrderService(EventBus eventBus) {
this.eventBus = eventBus;
}
public void payOrder(String orderId, double amount) {
// 处理支付...
eventBus.publish("order.paid", new OrderPaidEvent(orderId, amount));
}
}
解释:
OrderService不知道EmailNotifier或InventoryService是否存在。- 它只向
"order.paid"topic 发布,剩下的由 broker 处理。 - Subscriber 向 broker 注册,而不是直接向
OrderService注册。
3.1. 进程内 Pub/Sub 与 Spring Events
Spring Boot 内置了一个简单的 event bus,使用 ApplicationEventPublisher。这是应用层的 Pub/Sub,还不需要 Kafka/RabbitMQ 这样的外部 broker。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
public class OrderPaidEvent extends ApplicationEvent {
private final String orderId;
public OrderPaidEvent(String orderId) {
super(orderId);
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void payOrder(String orderId) {
publisher.publishEvent(new OrderPaidEvent(orderId));
}
}
@Component
public class EmailEventListener {
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("为订单 " + event.getOrderId() + " 发送邮件");
}
}
解释:
OrderService通过ApplicationEventPublisher——由 Spring 管理的 bean、充当内部 broker 的角色——发布 event。EmailEventListener不被OrderService知晓;Spring 自动扫描带@EventListener的方法,并根据事件类型路由到合适的 handler。- 默认情况下,
@EventListener与 publisher 同步在同一线程中运行——这容易造成混淆,很多人认为 Pub/Sub 总是异步的。
要实现真正的异步,加上 @Async:
@Component
public class EmailEventListener {
@Async
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("为订单 " + event.getOrderId() + " 发送邮件");
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
}
@EnableAsync激活代理,使 Spring 在独立的 thread pool 上运行@Async方法。- 现在
payOrder()立即返回,不等待EmailEventListener处理完成。
3.2. 通过 Message Broker 的 Pub/Sub(Kafka)
在更大规模的系统中,“broker” 不再是 JVM 中的一个 bean,而是独立的系统(Kafka、RabbitMQ、SQS 等)。Publisher 和 subscriber 可能位于不同的服务、不同的进程、不同的机器上。
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
@Service
public class OrderService {
private final KafkaTemplate<String, OrderPaidEvent> kafkaTemplate;
public OrderService(KafkaTemplate<String, OrderPaidEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void payOrder(String orderId, double amount) {
kafkaTemplate.send("order-paid-topic", new OrderPaidEvent(orderId, amount));
}
}
@Component
public class EmailConsumer {
@KafkaListener(topics = "order-paid-topic", groupId = "email-service")
public void consume(OrderPaidEvent event) {
System.out.println("邮件服务处理订单 " + event.orderId());
}
}
OrderService和EmailConsumer可能位于两个完全独立的微服务中,独立部署。- Kafka 在 topic 中保存消息,consumer 按自己的速率消费——这是真正的 Pub/Sub,具有缓冲、重放、多个独立 consumer group 等能力。
4. 直接对比
| 对比项 | Observer | Pub/Sub |
|---|---|---|
| 耦合程度 | Subject 直接了解 Observer(通过接口) | Publisher 不知道 Subscriber 是否存在 |
| 中间层 | 无,直接调用方法 | 有 broker/event bus 在中间 |
| 同步/异步 | 默认同步,同一线程 | 可同步(Spring Event)或异步(Kafka、RabbitMQ) |
| 范围 | 单个进程/模块内 | 进程内(event bus)或跨多个服务/进程 |
| 消息持久性 | 无,Subject 失效就丢失 | 外部 broker(Kafka)可保存,允许重放 |
| Publisher-Subscriber 数量 | 通常 1 个 Subject - 多个 Observer | 多个 Publisher - 多个 Subscriber,通过多个 topic |
最重要的区别:Observer 是两个对象间的直接关系,Pub/Sub 是通过中间层的间接关系。本质上,Pub/Sub 可看作 Observer 模式的“松耦合”版本,通过引入 broker 来消除直接依赖。
5. 何时用 Observer,何时用 Pub/Sub
使用 Observer 当:
- 逻辑在同一模块/类中,observer 数量固定且变化不大。
- 你需要即时同步响应(比如在保存前验证数据)。
- 不需要过度解耦——比如 UI 组件监听 model 的变化。
class ValidationObserver implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
if (order.getStatus() == null) {
throw new IllegalStateException("订单状态无效");
}
}
}
使用 Pub/Sub 当:
- 多个独立服务需要了解某个事件,但不应相互依赖。
- 需要灵活地添加/删除 subscriber,不改动 publisher 代码。
- 需要消息持久性(如果 subscriber 离线,消息会等待其重新上线)。
- 系统是微服务架构,通过网络通信。
提示:不要仅为了替换同一个 JVM 中的
List<Observer>就使用 Kafka/RabbitMQ——broker 的运维成本不值得这种小问题。反过来,也别试图用纯 Observer 模式硬撑跨服务的业务逻辑,因为你得自己实现 broker 已提供的网络、重试、序列化等全套机制。
6. Observer 行为测试
class OrderObserverTest {
@Test
void shouldNotifyAllObserversWhenOrderIsPaid() {
Order order = new Order();
List<String> notified = new ArrayList<>();
order.addObserver(o -> notified.add("email"));
order.addObserver(o -> notified.add("inventory"));
order.markAsPaid();
assertThat(notified).containsExactly("email", "inventory");
}
}
解释:
- 用 lambda 代替
OrderObserver的实现类,因为接口只有一个方法——写测试时更简洁。 assertThat(...).containsExactly(...)验证 observer 的调用顺序与注册顺序相同——这是同步 Observer 的特点,不同于 Pub/Sub 通过 broker(Kafka 可能多个 consumer instance 并行处理)。
7. 常见错误
- 误认为 Spring
@EventListener是异步的:默认是同步的,listener 中的异常会向上抛给 publisher,除非你捕捉它。 - Observer 持有引用但忘记移除:导致内存泄漏,尤其在应用中对象生命周期短(request-scope)但注册到 singleton 时。
- 滥用异步 Pub/Sub 处理需要事务一致性的逻辑:如果 publisher 和 subscriber 必须同时成功或失败(原子性),异步 Pub/Sub 不适合——应该考虑 Saga 模式或 transactional outbox。
- topic 命名不清晰:在大系统中,用模糊的名字如
"event1"会让人不知道谁订阅谁,失去了解耦的好处。
8. 总结
Observer 和 Pub/Sub 都想解决“通知多个感兴趣方”的问题,但在耦合程度和作用范围上不同。Observer 适合直接关系、同步、进程内的通信;Pub/Sub 适合完全解耦 publisher 和 subscriber,尤其在需要消息持久性和多个独立 consumer 的微服务系统中。理解这些差异能帮你选对工具:不是每时每刻都需要 Kafka,也不是 List<Observer> 总能胜任你的需求。