容器類
我們在實際編碼中,常常會遇到各種容器類,他們有時叫做POJO,有時又叫做DTO,VO, DO等,這些類只具有容器的作用,具有完全的get,set方法,作為資訊載體,作資料傳輸用。
其實,很多地方都可以看做將物件看做容器。比如,一些起到標籤作用的介面,Serializable, **Aware介面等;註解元資訊,比如@Order提供排序資訊,@Value提供值注入,@Autowired提供屬性注入等。
這種容器類一般很少具有物件導向的方法,又叫做貧血模型。與之對應的是充血模型,適合用於複雜系統,Spring原始碼顯然就是充血模型佔絕大多數,基於了物件導向的思想:物件=資料+行為。但是Spring中也經常見到許多容器類,有些類的一部分也可以看做容器。
函式式模型
函數語言程式設計思想下,一個物件通常值為不變的。這樣做的好處有很多,僅列舉一些:便於支援併發,是執行緒安全的;物件是無狀態的,極大地簡化程式碼的理解,因為每一次值的變化都是顯性的;便於編譯器最佳化效能,比如延遲計算,實現物件複用;便於測試,因為沒有副作用,每一次執行方法返回的結果都唯一。
這種物件稱為值物件,Lombok中使用@Value標註,JDK14開始引入的record關鍵字實現了相同功能。比如Map容器對Key的要求就是值物件。以後有時間我再寫一下函式式的一些基本思想。
舉例1:MVC模型
MVC模型中,Model, View, Controller中我們使用的資料傳輸物件一般就使用POJO。一般情況下,一個請求過來,SpringMvc 可以將Json格式的字串反序列化為POJO,一般為**Request。進行一系列計算或資料庫互動後,返回可序列化的POJO物件,SpringMvc 將其序列化為Json。
舉例2:各種配置的屬性
我們經常使用的@Value、@ConfigurationProperties註解可以快速配置物件的屬性,配合@RefreshScope可以實現動態重新整理配置。
@Value註解的中的值還支援EL表示式,供我們靈活取用。
舉例3:Spring 事件
觀察者設計模式中的事件都是值物件。一個事件可以被多個listener監聽,如果有一個listener改變了事件的某些屬性,其他listener無法確定事件是否被更改過,系統執行的結果就不穩定。
我們簡單地看下refresh模板方法,在容器刷下完畢之後,會呼叫finishRefresh方法,這個方法會發布事件ContextRefreshedEvent
。我們看一下ContextRefreshedEvent的原始碼就會發現,其值只在構造器建立物件時確定,不包含set方法。
public class ContextRefreshedEvent extends ApplicationContextEvent {
public ContextRefreshedEvent(ApplicationContext source) {
super(source);
}
}
public abstract class ApplicationContextEvent extends ApplicationEvent {
public ApplicationContextEvent(ApplicationContext source) {
super(source);
}
public final ApplicationContext getApplicationContext() {
return (ApplicationContext) getSource();
}
}
public abstract class ApplicationEvent extends EventObject {
private static final long serialVersionUID = 7099057708183571937L;
private final long timestamp;
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}
public final long getTimestamp() {
return this.timestamp;
}
}
public class EventObject implements java.io.Serializable {
private static final long serialVersionUID = 5516075349620653480L;
protected transient Object source;
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");
this.source = source;
}
public Object getSource() {
return source;
}
public String toString() {
return getClass().getName() + "[source=" + source + "]";
}
}
也可以用IDEA提供的Structure功能看,一目瞭然。
打斷點到對應位置
模板方法refresh
舉例4:Monad
前置知識:我們知道Java Stream支援以下操作:filter, map, flatMap, collect, reduce。以上這些操作就是我們經常使用的,入參通常為函式。
monad是一個很複雜的概念,也很難解釋。我們可以從使用的角度來簡化理解,其就是一個容器,這個容器包裝了其他的物件,這個容器包裝了物件的行為,可以使用map、flatMap等方法。
不嚴格地說,如果沒有Null,Optional就是一個Monad,資料庫查詢的返回值應該是Optional
java8 引入Stream流後,集合型別也可以看做Monad,比如members.stream().filter(validPredicate).map(Member::getSalary).mapToInt(x → x).sum()
monad中的 flatMap 可以避免套娃。
在Java中常見的套娃就是Try Catch,使用vavr庫中的Try可以解決,Try.of(邏輯1).faltMap(邏輯2),程式碼執行後,Try包裝的物件要麼是正常業務執行的返回值,要麼是遇到的Exception。
比如我想查詢資料庫,查詢公司職員A的主管。
member = repo.getByName(a);
manager = member.map(Member::getManager).flatMap(repo::getByName);
總結
- 閱讀原始碼的過程中,我們更應該關注的是類的行為,或者說是對於資料的處理。而對於資料類的存放位置,可以放在次要的位置。
- 資料的表示可以是POJO類、值型別、陣列、基本資料型別以及他們之間的組合,我們在編碼過程中更應該注意其結構的可讀性和是否選取 Immutable(不變)來表示。
- 關注資料的傳遞,可以說資料的傳遞表達了系統的架構。有時候雖然我們並不知道資料的傳遞過程,但是我們更在意資料傳遞的結果。
關於第 3 點舉一個例子,使用@Order 註解可以為一組類進行排序,再閱讀原始碼之前,我們雖然不知道是哪個具體的類去處理的,但是其處理的過程必然包括了以下過程:獲取註解值,對原有類與排序號進行關聯,一個通用的排序方法排序。
SpringBoot自動配置了許多類,我們雖然不知道具體的配置是如何對映到我們在容器中使用的Bean,但是可以確定資料的傳遞必然經歷了 配置 → 解析 → 註冊Bean的過程。從實用的角度出發,多數情況下,我們不必知道一個Bean的建立的具體邏輯,我們只是拿來就用,按需配置,我們需要了解的是這個Bean可以做什麼。
事實上,Spring專案中,除了少數的方法類(比如各種**Strategy),隨便拿出一個類,基本都可以看做容器類,後續我會結合一些程式碼再進行詳細分析,敬請關注。