背景###
最近在做應用配置化的事情。 應用配置化,就是將應用中一些頻繁變化的變量通過配置比如JSON串的形式存儲到配置平台。當新業務需要增加新的枚舉時,只要在配置平台修改對應配置,就能實時推送給應用更新,無需應用重新發布。頻繁變化的變量包括文案、分流比例、枚舉等。
配置更新的實時推送機制,讓人很容易就想到觀察者模式。 觀察者模式的要點在於:
- 在對象之間建立一對多的關系,當某一個對象發生變化的時候,相關對象都能得到通知並更新自己。
- 有利於【模型/控制/視圖】的分離,即著名的MVC框架。
設計結構###
觀察者模式的設計結構如下:
- Observable: 被觀察者抽象。當被觀察者持有的某個狀態改變時,將通知所有關注此狀態的觀察者。Observable 的方法主要有:添加、刪除、通知觀察者對象;設置或清除更新狀態。這里 Observable 大多數方法使用了同步關鍵字 synchronized,因為很可能在多線程環境下訪問。
- Observer: 觀察者抽象。 當被觀察者的某個關注狀態更新時,觀察者將得到通知。觀察者的基本方法是 update 。
- Config : 配置,具體的被觀察者。
- Application: 應用,具體的觀察者。
極簡實現###
這里借助 jdk 的Observable, Observer 提供觀察者的一個極簡實現。
package zzz.study.patterns.observer.realconfig.simple;
import java.util.Observable;
public class Config<T> extends Observable {
private T conf;
public Config(T conf) {
this.conf = conf;
}
public T getConf() {
return conf;
}
public void update(T config) {
this.conf = config;
setChanged();
}
}
package zzz.study.patterns.observer.realconfig.simple;
import java.util.Observable;
import java.util.Observer;
public class Application implements Observer {
public void update(Observable o, Object arg) {
Config obj = (Config)o;
Object conf = obj.getConf();
System.out.println("conf updated: " + conf);
}
}
package zzz.study.patterns.observer.realconfig.simple;
public class ConfigRealUpdating {
public static void main(String[] args) {
Config config = new Config<>(5);
Application app = new Application();
config.addObserver(app);
config.update(6);
config.notifyObservers();
}
}
擴展優化###
多個配置與應用####
極簡實現提供了對觀察者模式基本思想的直接理解。但極簡實現是遠遠不夠的。它只有一個被觀察者和一個應用對象。假設有兩個應用 A,B以及兩個配置 aconf, bconf , A 關注 aconf 的變化, B 關注 bconf 的變化。怎么辦呢? 有兩種方案:
- 在 Conf 中同時添加 aconf, bconf ,這樣需要做一個從 Conf 到 Application 的映射關系。而且 aconf, bconf 的配置對象很可能不一樣,需要做些特殊處理。 這樣無疑會讓 Conf 類變得更復雜。
- 分別建立子類 AConf, BConf ,以及 AApplication , BApplication ,AConf 添加 AApplication 觀察者; BConf 添加 BApplication 觀察者; Application 變成 AbstractApplication。這樣,就將關注的配置與應用分離開。美中不足的是,這樣的子類會更多。
代碼實現如下:
public class AConfig extends Config<String> {
public AConfig(String conf) {
super(conf);
}
}
public class BConfig extends Config<Long> {
public BConfig(Long conf) {
super(conf);
}
}
public abstract class AbstractApplication implements Observer {
public void update(Observable o, Object arg) {
Config obj = (Config)o;
Object conf = obj.getConf();
handle(conf);
}
public abstract void handle(Object conf);
}
public class AApplication extends AbstractApplication {
@Override
public void handle(Object conf) {
System.out.println("A Conf updated: " + conf);
}
}
public class BApplication extends AbstractApplication {
@Override
public void handle(Object conf) {
Long num = (Long)conf;
if (num != null && num > 0) {
String info = String.format("factor(%d) = %d", num, factor(num));
System.out.println(info);
}
}
public Long factor(Long num) {
if (num < 0) { return 0L; }
if (num == 0) {return 1L; }
return num * factor(num-1);
}
}
public class ConfigRealUpdating {
public static void main(String[] args) {
Config aConfig = new AConfig("Haha");
Config bConfig = new BConfig(-1L);
AbstractApplication aApp = new AApplication();
AbstractApplication bApp = new BApplication();
aConfig.addObserver(aApp);
bConfig.addObserver(bApp);
aConfig.update("I am changed");
aConfig.notifyObservers();
bConfig.update(9L);
bConfig.notifyObservers();
}
}
設計結構變成:
停下來重新思考下,通過擴展更多子類來實現是否有些沖動?需要思考兩個問題:
- 配置的差異究竟是什么?為什么需要用不同子類來實現? 顯然,如果都是原子變量,用泛型就能搞定; 配置的差異體現在:單個配置還是組合關聯配置。具有很大差異的配置,才值得用不同子類來表達。
- 應用的差異究竟是什么?為什么需要用不同子類來實現? 這是由於不同應用對同一配置的處理很可能是不一樣的。這里用子類代表的是監聽配置的多個應用。
交互行為管理####
現在,思考一個更加實際的問題:假設應用A監聽配置a的變化,做HAa的處理,監聽配置b的變化,做HAb的處理,監聽配置c的變化,做HAc的處理,……;應用B監聽配置a的變化,做HBa的處理,監聽配置b的變化,做HBb的處理,監聽配置c的變化,做HBc的處理,……。 也就是說,有多個應用,同時監聽多個配置,做不同的處理,該怎么辦?
此時,不能將觀察者列表放在 Observable 了。 實際上,jdk 里的 Observable 擔負了兩個責任:(1) 狀態變化; (2) 通知觀察者。 現在,希望將職責(2) 移到專門負責交互的一個對象。這個對象可采用中介者模式來實現。希望達成效果:
- 配置僅負責狀態變化;
- 應用僅負責根據相應狀態執行相應邏輯;
- 增加一個中介者,監聽配置變化,並通知相應應用。中介者還需要提供注冊和銷除配置與應用映射關系的職責。
現在,需要造點輪子了。 要解決這個交互,需要構造一個三元組<config, app, updateFunc>集合。由於 java 沒有提供元組功能,因此,可以將后兩者包裝成一個對象ConfigObserver,配置與應用行為可抽象為一個Map[Config, List[ConfigObserver]] 。注意,這里 Config, ConfigObserver 都需要跟集合交互,為保證存取正確,必須覆寫 equals 和 hashCode 方法。 這里配置和應用都采用一個ID 來標識(因為配置變更時,值已經改變了,所以不能用值作為equals的依據)。
代碼實現如下(所有類都放在包 zzz.study.patterns.observer.realconfig.interact 下):
ID.java 封裝了 id 相關的功能
package zzz.study.patterns.observer.realconfig.interact;
import java.util.concurrent.atomic.AtomicLong;
/**
* 封裝了ID以及根據ID來比較的功能
*/
public abstract class ID {
private static AtomicLong gloalId = new AtomicLong(0);
// 通過id字段標識配置及應用
protected Long id;
public void setId() {
this.id = gloalId.addAndGet(1);
}
public <T> boolean equals(Object obj, Class<T> cls) {
if (obj == null || ! (cls.isInstance(obj))) {
return false;
}
return ((ID)obj).id.equals(this.id);
}
public int hashCode() {
return id.hashCode();
}
}
應用類:
package zzz.study.patterns.observer.realconfig.interact;
public abstract class Application extends ID {
@Override
public boolean equals(Object c) {
return equals(c, Application.class);
}
@Override
public int hashCode() {
return super.hashCode();
}
}
public class AApp extends Application {
public AApp() {
super.setId();
}
public Object haa(Config c) {
System.out.println("haa: " + c.getConf());
return c;
}
public Object hab(Config c) {
System.out.println("hab: " + c.getConf());
return c;
}
public Object hac(Config c) {
System.out.println("hac: " + c.getConf());
return c;
}
}
public class BApp extends Application {
public BApp() {
super.setId();
}
public Object hba(Config c) {
System.out.println("hba: " + c.getConf());
return c;
}
public Object hbb(Config c) {
System.out.println("hbb: " + c.getConf());
return c;
}
public Object hbc(Config c) {
System.out.println("hbc: " + c.getConf());
return c;
}
}
配置類:
package zzz.study.patterns.observer.realconfig.interact;
import lombok.Getter;
@Getter
public class Config<T> extends ID {
private T conf;
private ObserverMediator mediator;
public Config(T conf, ObserverMediator mediator) {
super.setId();
this.conf = conf;
this.mediator = mediator;
}
public void update(T config) {
this.conf = config;
mediator.notifyAll(this);
}
@Override
public boolean equals(Object c) {
return equals(c, Config.class);
}
@Override
public int hashCode() {
return super.hashCode();
}
}
package zzz.study.patterns.observer.realconfig.interact;
import java.util.function.Function;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ConfigObserver {
private Application app;
private Function<Config, Object> updateFunc;
@Override
public boolean equals(Object c) {
if (c == null || ! (c instanceof ConfigObserver)) {
return false;
}
ConfigObserver cmp = (ConfigObserver) c;
return cmp.getApp().equals(this.getApp());
}
@Override
public int hashCode() {
return app.hashCode();
}
}
中介者:
package zzz.study.patterns.observer.realconfig.interact;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 中介者模式,管理觀察者與被觀察者的交互
* 實際應用中,可作為一個Spring Singleton Bean來管理
*/
public class ObserverMediator {
private Map<Config, List<ConfigObserver>> mediator = new HashMap<>();
/**
* 應用啟動時初始化
*/
public void init() {
}
public synchronized boolean register(Config config, ConfigObserver configObserver) {
List<ConfigObserver> oberverList = mediator.get(config);
if (oberverList == null) {
oberverList = new ArrayList<>();
}
oberverList.add(configObserver);
mediator.put(config, oberverList);
return true;
}
public synchronized boolean unregister(Config config, ConfigObserver configObserver) {
List<ConfigObserver> oberverList = mediator.get(config);
if (oberverList == null) {
return false;
}
oberverList.remove(configObserver);
mediator.put(config, oberverList);
return true;
}
public synchronized boolean notifyAll(Config config) {
List<ConfigObserver> configObservers = mediator.get(config);
configObservers.forEach(
observer -> observer.getUpdateFunc().apply(config)
);
return true;
}
}
客戶端使用:
package zzz.study.patterns.observer.realconfig.interact;
import java.util.concurrent.TimeUnit;
public class ConfigRealUpdating {
public static void main(String[] args) {
// 中介者是全局管理者,是最先存在的
ObserverMediator mediator = new ObserverMediator();
// 這一步在配置平台實現, 可以采用注解的方式注入到應用中,配置僅與中介者交互
Config aConfig = new Config("Haha", mediator);
Config bConfig = new Config(-1L, mediator);
Config cConfig = new Config(true, mediator);
AApp a = new AApp();
BApp b = new BApp();
// 這一步可以通過應用啟動時注冊到分布式服務發現系統上來完成
mediator.register(aConfig, new ConfigObserver(a, conf -> a.haa(conf)));
mediator.register(bConfig, new ConfigObserver(a, conf -> a.hab(conf)));
mediator.register(cConfig, new ConfigObserver(a, conf -> a.hac(conf)));
mediator.register(aConfig, new ConfigObserver(b, conf -> b.hba(conf)));
mediator.register(bConfig, new ConfigObserver(b, conf -> b.hbb(conf)));
mediator.register(cConfig, new ConfigObserver(b, conf -> b.hbc(conf)));
// 核心: 更新與通知
aConfig.update("I am changed");
sleep(2000);
bConfig.update(9L);
sleep(2000);
mediator.unregister(cConfig, new ConfigObserver(b, conf -> b.hbc(conf)));
cConfig.update(false);
sleep(2000);
}
private static void sleep(long millis) {
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
設計結構變為:
設計說明:
- 實際應用中, Config, Application 不應該繼承 ID 類,而應采用委托的方式: ID 為單例組件注入到 Config, Application 中提供ID功能。
- 如果通知功能足夠復雜,那么應從中介者中抽離出來,專門作為通知者角色; 中介者僅負責配置與應用的注冊與銷毀,供通知者使用。
- ConfigObserver 的功能可以通過應用注冊監聽器來實現;它的本質就是監聽器。
實際配置系統###
業界開源的動態配置平台有攜程的Apollo配置。可以想象一下主流程:
STEP1: 在配置平台,首先要注冊應用。這樣就建立 Application 對象;
STEP2: 在配置平台的某個應用下,新建某個配置,就會在 Apollo 系統內將 Config 與 Application 聯系起來;
STPE3: 當應用啟動時,通過SpringXML配置的方式, 告知 Apollo 該應用所需要的配置,實際上形成了該應用的配置訂閱; Apollo 在運行時將配置與應用關聯起來;
STEP4: 當用戶在配置平台更改某個配置時,通過觀察者模式,就可以將配置改動通知到連接上的應用;
STEP5: 應用可以通過注冊配置的監聽器,來處理配置改動。
實際中,可以支持更多功能,比如更靈活的配置對象類型、配置的名字空間、配置的共享、通過注解的方式實時推送更新的配置等。
小結###
本文講解了通過觀察者模式來實現配置動態更新實時推送的基本原理和實現。觀察者模式是解耦變化與關聯變化的設計結構,即:當一個對象的某個狀態變化后,需要通知關注該對象該狀態的所有對象。關注者不需要知道其他關注者,被觀察者也不需要知道觀察者。