观察者模式 (Observer)
一个对象的状态变了,好几处逻辑都要跟着动:写审计、刷缓存、发告警、记指标。如果在改状态的地方把这几件事全写死,代码会又长又难改,加一个「状态变了要做的动作」就得动核心逻辑。
观察者模式的做法是:主题只负责在状态变化时「通知」所有订阅者,谁关心这件事谁自己注册成观察者,主题不依赖具体有哪些动作,观察者之间也互不依赖。这样发布方和订阅方解耦,增删监听逻辑不用改主题代码。
一、问题从哪来?
Section titled “一、问题从哪来?”有的业务里,一件事发生之后要触发多件后续动作:例如配置改了要刷新本地缓存、写审计日志、可能还要发一条内部通知;或者某条记录状态变更后要更新统计、触发工作流。最直接的写法是在「改状态」的那段代码里,把「写审计」「刷缓存」「发通知」全调一遍。这样会有几个问题:改状态的地方越来越臃肿;加一种后续动作就要改同一处;这些后续动作往往属于不同模块,硬写在一起耦合度高,也不好单测。
更稳妥的做法是:改状态的那一方只发「我变了」的信号,不关心谁在听;需要响应的模块自己订阅这个信号,收到后做自己的事。这样「发生了什么」和「发生之后谁要做什么」分开,主题和观察者都只依赖抽象接口,符合开闭原则。观察者模式就是在代码里把这种「主题 + 多个观察者、一变就通知」的结构显式做出来。
二、主题、观察者与通知流程
Section titled “二、主题、观察者与通知流程”- 主题 (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@Builderpublic class ConfigChangedEvent { private String key; private String oldValue; private String newValue; private String operator; private Instant occurredAt;}5.2 发布方:配置更新时发事件
Section titled “5.2 发布方:配置更新时发事件”配置服务在更新成功后,发布一个 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@RequiredArgsConstructorpublic 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); }}5.3 观察者:审计、缓存、日志
Section titled “5.3 观察者:审计、缓存、日志”每个监听器只关心自己那一件事,方法签名统一是「接收事件」,容器会在事件发布时调用它们。下面用到的 AuditLogEntry、AuditLogRepository、LocalConfigCache 按你项目里已有的审计表和本地缓存实现即可。
// 审计实体与仓储(示意)@Data @Builderclass AuditLogEntry { private String action, targetKey, oldValue, newValue, operator; private java.time.Instant occurredAt;}@Repositoryclass 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@RequiredArgsConstructorpublic 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 失效即可@Componentclass 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@Componentpublic 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@Componentpublic class ConfigChangeLogListener { @EventListener public void onConfigChanged(ConfigChangedEvent event) { log.info("config changed: key={}, operator={}, old={}, new={}", event.getKey(), event.getOperator(), event.getOldValue(), event.getNewValue()); }}这里 AuditLogRepository、LocalConfigCache 可以是你的审计表仓储和本地缓存实现,监听器只负责「收到事件后调它们」。若希望审计、日志异步执行、不阻塞配置更新,可以给对应方法加 @Async(并开启 @EnableAsync),事件发布仍是同步的,只是观察者的执行放到线程池里。
5.4 调用链
Section titled “5.4 调用链”配置更新的入口(例如 Controller 或别的 Service)只调 ConfigService.set(key, value, operator)。Service 更新存储后 publishEvent(ConfigChangedEvent),容器会把事件派发给所有 @EventListener 方法参数类型匹配的 Bean,于是审计、缓存失效、日志会依次执行。主题相当于「事件发布 + 事件类型」,观察者就是这些带 @EventListener 的 Bean;增删监听器只需增删 Bean 或方法,不用改 ConfigService。
观察者模式把「状态变化」和「变化之后要做的多件事」解耦:主题只维护订阅列表并在变化时通知,观察者实现统一接口、各自处理。后端里用「事件 + 监听器」实现同一思想很常见,发布方只发事件,订阅方用 @EventListener 等方法接收,互不依赖具体实现。上面示例里配置变更只发一个事件,审计、缓存、日志分别由不同 Bean 监听,符合开闭原则;若要再加一个「发站内信」的监听器,只需新写一个 Bean 并监听同一事件即可,无需改配置服务。