從Spring中學到的【2】--容器類

大華dijkstra發表於2022-12-29

容器類

我們在實際編碼中,常常會遇到各種容器類,他們有時叫做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功能看,一目瞭然。
image

打斷點到對應位置
image

模板方法refresh
image

舉例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);

總結

  1. 閱讀原始碼的過程中,我們更應該關注的是類的行為,或者說是對於資料的處理。而對於資料類的存放位置,可以放在次要的位置。
  2. 資料的表示可以是POJO類、值型別、陣列、基本資料型別以及他們之間的組合,我們在編碼過程中更應該注意其結構的可讀性和是否選取 Immutable(不變)來表示。
  3. 關注資料的傳遞,可以說資料的傳遞表達了系統的架構。有時候雖然我們並不知道資料的傳遞過程,但是我們更在意資料傳遞的結果。

關於第 3 點舉一個例子,使用@Order 註解可以為一組類進行排序,再閱讀原始碼之前,我們雖然不知道是哪個具體的類去處理的,但是其處理的過程必然包括了以下過程:獲取註解值,對原有類與排序號進行關聯,一個通用的排序方法排序。

SpringBoot自動配置了許多類,我們雖然不知道具體的配置是如何對映到我們在容器中使用的Bean,但是可以確定資料的傳遞必然經歷了 配置 → 解析 → 註冊Bean的過程。從實用的角度出發,多數情況下,我們不必知道一個Bean的建立的具體邏輯,我們只是拿來就用,按需配置,我們需要了解的是這個Bean可以做什麼。

事實上,Spring專案中,除了少數的方法類(比如各種**Strategy),隨便拿出一個類,基本都可以看做容器類,後續我會結合一些程式碼再進行詳細分析,敬請關注。

相關文章