跳转到内容

观察者模式 (Observer)

一个对象的状态变了,好几处逻辑都要跟着动:写审计、刷缓存、发告警、记指标。如果在改状态的地方把这几件事全写死,代码会又长又难改,加一个「状态变了要做的动作」就得动核心逻辑。

观察者模式的做法是:主题只负责在状态变化时「通知」所有订阅者,谁关心这件事谁自己注册成观察者,主题不依赖具体有哪些动作,观察者之间也互不依赖。这样发布方和订阅方解耦,增删监听逻辑不用改主题代码。


有的业务里,一件事发生之后要触发多件后续动作:例如配置改了要刷新本地缓存、写审计日志、可能还要发一条内部通知;或者某条记录状态变更后要更新统计、触发工作流。最直接的写法是在「改状态」的那段代码里,把「写审计」「刷缓存」「发通知」全调一遍。这样会有几个问题:改状态的地方越来越臃肿;加一种后续动作就要改同一处;这些后续动作往往属于不同模块,硬写在一起耦合度高,也不好单测。

更稳妥的做法是:改状态的那一方只发「我变了」的信号,不关心谁在听;需要响应的模块自己订阅这个信号,收到后做自己的事。这样「发生了什么」和「发生之后谁要做什么」分开,主题和观察者都只依赖抽象接口,符合开闭原则。观察者模式就是在代码里把这种「主题 + 多个观察者、一变就通知」的结构显式做出来。


  • 主题 (Subject):维护一个观察者列表,提供「注册」「注销」方法;内部状态变化时,遍历列表,对每个观察者调用其更新方法(例如 update(state)onEvent(event))。主题不关心观察者具体做什么,只负责发通知。
  • 观察者 (Observer):定义一个统一的「被通知」接口,例如 update(State state)onEvent(Event e)。具体观察者实现这个接口,在方法里写自己的逻辑(写库、发消息、刷缓存等)。主题只依赖这个接口,不依赖具体实现。
  • 推还是拉:可以是主题把新状态(或事件对象)作为参数传给观察者(推),也可以是只通知「我变了」,观察者再主动向主题查询最新状态(拉)。后端里用「推」更常见,把事件对象传过去,观察者用事件里的数据即可。

客户端(或容器)在启动或配置阶段把观察者注册到主题上;之后只要主题状态变化、调用「通知所有观察者」的方法,订阅方就会自动执行,无需在业务代码里写一长串调用。


主题持有观察者列表并在状态变化时遍历通知;观察者只实现更新接口。

classDiagram
    class Subject {
        -observers List~Observer~
        +attach(Observer o) void
        +detach(Observer o) void
        +notifyObservers() void
    }
    class Observer {
        <<interface>>
        +update()* void
    }
    class ConcreteSubject {
        -state String
    }
    class ConcreteObserver {
        +update() void
    }
    Subject <|-- ConcreteSubject
    Observer <|.. ConcreteObserver
    Subject o-- Observer

四、最小示例:主题 + 观察者列表

Section titled “四、最小示例:主题 + 观察者列表”

不依赖框架,用列表和接口把「注册 → 改状态 → 通知」跑通。

public interface Observer {
void update(String state);
}
public class Subject {
private final java.util.List<Observer> observers = new java.util.ArrayList<>();
private String state;
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void setState(String state) {
this.state = state;
for (Observer o : observers) o.update(state);
}
}
public class ConcreteObserver implements Observer {
@Override
public void update(String state) {
System.out.println("收到更新: " + state);
}
}
Subject subject = new Subject();
subject.attach(new ConcreteObserver());
subject.setState("新状态"); // 输出: 收到更新: 新状态

下面把「主题」换成事件发布、「观察者」换成基于容器的监听器,用事件对象承载状态,适合在后端多 Bean 之间解耦。


五、实战:配置变更与审计事件

Section titled “五、实战:配置变更与审计事件”

后台里常见一种需求:某类配置被更新(例如系统参数、开关)时,要触发审计记录、本地缓存刷新、以及可选的通知或指标。如果这些逻辑都写在「更新配置」的 Service 里,方法会越来越长,且缓存、审计、通知分属不同模块,混在一起不利于维护。用观察者思路:更新配置时只发布一个「配置已变更」事件,审计、缓存、日志等各自作为监听器订阅该事件,收到后做自己的事。这样配置更新的核心逻辑只负责改数据和发事件,不加新监听器就不用动这段代码。

下面用「配置项变更」作为事件,多个监听器分别做审计、缓存失效、日志。事件和监听器都以 Bean 形式存在,通过容器自带的事件机制完成「主题 → 观察者」的派发。

5.1 事件对象(把「谁变了、变什么样」装进去)

Section titled “5.1 事件对象(把「谁变了、变什么样」装进去)”

事件里带上变更的 key、旧值、新值、操作人等信息,监听器按需使用。

package com.example.demo.observer;
import lombok.Builder;
import lombok.Data;
import java.time.Instant;
@Data
@Builder
public class ConfigChangedEvent {
private String key;
private String oldValue;
private String newValue;
private String operator;
private Instant occurredAt;
}

配置服务在更新成功后,发布一个 ConfigChangedEvent,由容器转发给所有监听该事件的 Bean。发布方不依赖任何「写审计」「刷缓存」的类,只依赖事件发布接口。

package com.example.demo.observer;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
@RequiredArgsConstructor
public class ConfigService {
private final ApplicationEventPublisher eventPublisher;
private final Map<String, String> store = new ConcurrentHashMap<>();
public void set(String key, String value, String operator) {
String oldValue = store.get(key);
store.put(key, value);
ConfigChangedEvent event = ConfigChangedEvent.builder()
.key(key)
.oldValue(oldValue)
.newValue(value)
.operator(operator)
.occurredAt(Instant.now())
.build();
eventPublisher.publishEvent(event);
}
}

每个监听器只关心自己那一件事,方法签名统一是「接收事件」,容器会在事件发布时调用它们。下面用到的 AuditLogEntryAuditLogRepositoryLocalConfigCache 按你项目里已有的审计表和本地缓存实现即可。

// 审计实体与仓储(示意)
@Data @Builder
class AuditLogEntry {
private String action, targetKey, oldValue, newValue, operator;
private java.time.Instant occurredAt;
}
@Repository
class AuditLogRepository {
public void save(AuditLogEntry e) { /* 落库 */ }
}
package com.example.demo.observer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class ConfigChangeAuditListener {
private final AuditLogRepository auditLogRepository;
@EventListener
public void onConfigChanged(ConfigChangedEvent event) {
auditLogRepository.save(AuditLogEntry.builder()
.action("CONFIG_CHANGED")
.targetKey(event.getKey())
.oldValue(event.getOldValue())
.newValue(event.getNewValue())
.operator(event.getOperator())
.occurredAt(event.getOccurredAt())
.build());
}
}
// 本地配置缓存(示意):按 key 失效即可
@Component
class LocalConfigCache {
public void invalidate(String key) { /* 清除该 key 的缓存 */ }
}
package com.example.demo.observer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ConfigCacheInvalidationListener {
private final LocalConfigCache localConfigCache;
public ConfigCacheInvalidationListener(LocalConfigCache localConfigCache) {
this.localConfigCache = localConfigCache;
}
@EventListener
public void onConfigChanged(ConfigChangedEvent event) {
localConfigCache.invalidate(event.getKey());
}
}
package com.example.demo.observer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ConfigChangeLogListener {
@EventListener
public void onConfigChanged(ConfigChangedEvent event) {
log.info("config changed: key={}, operator={}, old={}, new={}",
event.getKey(), event.getOperator(), event.getOldValue(), event.getNewValue());
}
}

这里 AuditLogRepositoryLocalConfigCache 可以是你的审计表仓储和本地缓存实现,监听器只负责「收到事件后调它们」。若希望审计、日志异步执行、不阻塞配置更新,可以给对应方法加 @Async(并开启 @EnableAsync),事件发布仍是同步的,只是观察者的执行放到线程池里。

配置更新的入口(例如 Controller 或别的 Service)只调 ConfigService.set(key, value, operator)。Service 更新存储后 publishEvent(ConfigChangedEvent),容器会把事件派发给所有 @EventListener 方法参数类型匹配的 Bean,于是审计、缓存失效、日志会依次执行。主题相当于「事件发布 + 事件类型」,观察者就是这些带 @EventListener 的 Bean;增删监听器只需增删 Bean 或方法,不用改 ConfigService。


观察者模式把「状态变化」和「变化之后要做的多件事」解耦:主题只维护订阅列表并在变化时通知,观察者实现统一接口、各自处理。后端里用「事件 + 监听器」实现同一思想很常见,发布方只发事件,订阅方用 @EventListener 等方法接收,互不依赖具体实现。上面示例里配置变更只发一个事件,审计、缓存、日志分别由不同 Bean 监听,符合开闭原则;若要再加一个「发站内信」的监听器,只需新写一个 Bean 并监听同一事件即可,无需改配置服务。