DDD實踐:實現基於快照機制的變更追蹤

2023-08-22 06:00:27

王有志,一個分享硬核Java技術的互金摸魚俠
加入Java人的提桶跑路群:共同富裕的Java人

去年我們在重構專案中落地了DDD,當時花了點時間研究了下阿里巴巴大淘寶技術釋出的《阿里技術專家詳解DDD系列》,其中第三講《阿里技術專家詳解DDD系列 第三講 - Repository模式》中提到了一項技術--變更追蹤

簡單來說,變更追蹤是記錄物件進行業務操作後發生的改變,通過這些改變來決定如何更新資料庫,文章中提到了兩種實現變更追蹤方案:

  1. 基於Snapshot的方案:當資料從DB裡取出來後,在記憶體中儲存一份snapshot,然後在資料寫入時和snapshot比較。常見的實現如Hibernate。
  2. 基於Proxy的方案:當資料從DB裡取出來後,通過weaving的方式將所有setter都增加一個切面來判斷setter是否被呼叫以及值是否變更,如果變更則標記為Dirty。在儲存時根據Dirty判斷是否需要更新。常見的實現如Entity Framework。

不過由於只給出了Snapshot方案的部分實現程式碼,導致很多讀者對產生了疑惑。

我們在工程實踐中借鑑了Snapshot方案的設計,並根據自身的業務情況做出了一些調整,下面就和大家分享我們在工程中的實踐。

疊「BUFF」

  • 今天的主題是實現變更追蹤而不是DDD,所以儘量不要把DDD的「戰火」引過來;
  • 以下程式碼未經過嚴格的測試,可能存在BUG,歡迎大家批評指正和討論。

開始前的準備工作

聚合與Repository介面的定義

正式開始前,我們先做一些簡單的準備工作,主要是DDD設計中的介面定義,首先是定義介面Aggregate和Identifier:

public interface Aggregate<ID extends Identifier> extends Serializable {
	ID getId();
}

public interface Identifier extends Serializable {
	Serializable value();
}

接著定義Repository介面並提供3個基礎能力:

public interface Repository<T extends Aggregate<ID>, ID extends Identifier> {

	/**
   * 儲存
	 * @param aggregateRoot
	 * @throws IllegalAccessException
	 */
	void save(T aggregateRoot) throws IllegalAccessException;

	/**
   * 刪除
	 * @param aggregateRoot
	 */
	void remove(T aggregateRoot);

	/**
   * 查詢
	 * @param identifier
	 * @return
	 */
	T find(ID identifier);
}

Repository是Service(業務邏輯)與DAO(Data Access Object,資料存取物件)間的「橋樑」,用於隔離業務邏輯與資料庫之間的依賴,幫助我們遮蔽在資料庫發生變更時對業務邏輯產生的影響,這點是DDD設計相關的內容,我們在這裡不過多的討論。

領域物件與Repository服務的定義

我們定義一個簡單書籍和圖片的實體:

@Getter
@Setter
public class Book implements Aggregate<BookId> {

	private BookId bookId;

	private String bookName;

	private String bookDesc;

	private Long words;

	private List<Image> images;

	private List<String> contents;

	@Override
	public BookId getId() {
		return this.bookId;
	}
}

@Getter
@Setter
public class BookId implements Identifier {

	private Long bookId;

	@Override
	public Serializable value() {
		return this.bookId;
	}
}

@Getter
@Setter
public class Image implements Aggregate<ImageId> {

	private ImageId imageId;

	private String imageUrl;

	@Override
	public ImageId getId() {
		return this.imageId;
	}
}

@Getter
@Setter
public class ImageId implements Identifier {

	private long imageId;

	@Override
	public Serializable value() {
		return this.imageId;
	}
}

在有些DDD的實踐規範中,實體中是不允許出現Getter方法和Setter方法的,這裡為了方便提供測試資料,直接使用了lombok的註解新增Getter方法和Setter方法。

最後我們來定義實體Book的Repository服務:

public interface BookRepository extends Repository<Book, BookId> {

}

public class BookRepositoryImpl implements BookRepository {

	@Override
	public void save(Book aggregateRoot) {
		// 實現儲存邏輯
	}

	@Override
	public void remove(Book aggregateRoot) {
		// 實現刪除邏輯
	}

	@Override
	public Book find(BookId identifier) {
		Book book = new Book();
		// 實現查詢邏輯
		return book;
	}
}

BookRepository介面的意義是方便自定義Repository方法,BookRepositoryImpl是BookRepository具體的實現,這裡我們只使用3個基礎功能即可,具體的實現邏輯是呼叫DAO實現增刪改查,並藉助Convert工具實現DO與實體的轉換,我們這裡就省略這部分內容了,實際上是我懶得寫了

變更追蹤的實現

RepositorySupport的實現

變更追蹤的核心是在呼叫Repository的基礎能力時進行實體物件的追蹤,並在儲存時對比實體物件的變化,具體的執行邏輯如下:

  • 呼叫Repository#find時,複製實體物件的快照,新增的變更追蹤的容器中;
  • 呼叫Repository#save時,對比當前實體物件與快照,返回兩者間的差異;
  • 呼叫Repository#remove時,刪除變更追蹤容器中實體物件的快照。

在我們的工程實踐中,核心設計採用了阿里巴巴在《阿里技術專家詳解DDD系列 第三講 - Repository模式》給出的方案,但在具體的實現細節上,我們做了一些調整,接下來就和大家分享下我們的設計。

首先來實現通用支撐類RepositorySupport,提供可複用的變更追蹤能力:

public abstract class RepositorySupport<T extends Aggregate<ID>, ID extends Identifier> implements Repository<T, ID> {
	
	private final AggregateTracingManager<T, ID> aggregateTracingManager;

	public RepositorySupport() {
		this.aggregateTracingManager = new ThreadLocalTracingManager<>();
	}

	/**
   * 由繼承RepositorySupport的子類實現
	 */
	protected abstract T onSelect(ID id);
	protected abstract void onInsert(T aggregate);
	protected abstract void onUpdate(T aggregate, AggregateDifference<T, ID> aggregateDifference);
	protected abstract void onDelete(T aggregate);
	
	/**
   * 主動追蹤
	 * @param id
	 * @return
	 */
	public void attach(T aggregate) {
		this.aggregateTracingManager.attach(aggregate);
	}

	/**
   * 差異對比
	 * @param aggregate
	 * @return
   * @throws IllegalAccessException
	 */
	protected AggregateDifference<T, ID> different(T aggregate) throws IllegalAccessException {
		return this.aggregateTracingManager.different(aggregate);
	}
	
	/**
   * 解除追蹤
	 * @param id
	 * @return
	 */
	public void detach(T aggregate) {
		this.aggregateTracingManager.detach(aggregate);
	}

	@Override
	public T find(ID identifier) {
		T aggregate = this.onSelect(identifier);
		if (aggregate != null) {
			this.aggregateTracingManager.attach(aggregate);
		}
		return aggregate;
	}

	@Override
	public void save(T aggregate) throws IllegalAccessException {
		AggregateDifference<T, ID> aggregateDifference = this.aggregateTracingManager.different(aggregate);
		if (DifferenceTypeEnum.ADDED.equals(aggregateDifference.getDifferentType())) {
			this.onInsert(aggregate);
		} else {
			this.onUpdate(aggregate, aggregateDifference);
		}
		this.aggregateTracingManager.merge(aggregate);
	}

	@Override
	public void remove(T aggregate) {
		this.onDelete(aggregate);
		this.aggregateTracingManager.detach(aggregate);
	}
}

我們依次對通用支撐類RepositorySupport中的成員變數和方法進行說明。

首先是RepositorySupport中唯一的成員變數AggregateTracingManager,該類的功能是完成變更追蹤快照的管理,包括物件追蹤,差異對比和解除追蹤等

接著是繼承RepositorySupport的實現類需要重寫的方法:

  • RepositorySupport#onSelect,由RepositorySupport中實現的Repository#find呼叫,與直接實現Repository#find相同,通過DAO查詢資料,並轉換為實體物件;
  • RepositorySupport#onInsert,由RepositorySupport中實現的Repository#save呼叫,與直接實現Repository#save類似,通過DAO儲存資料,此時為新增資料的儲存;
  • RepositorySupport#onUpdate,由RepositorySupport中實現的Repository#save呼叫,與直接實現Repository#save類似,通過DAO儲存資料,此時為修改資料的儲存;
  • RepositorySupport#onDelete,由RepositorySupport中實現的Repository#remove呼叫,與直接實現Repository#remove相同,通過DAO刪除資料。

接著是Repository中定義的提供變更追蹤能力的方法:

  • RepositorySupport#attach,主動追蹤,當實體的Repository介面中自定義查詢方法時,實現類可以通過該方法實現物件的變更追蹤;
  • RepositorySupport#different,差異對比,當實體的Repository介面中自定義儲存方法時,實現類可以通過該方法獲取當前實體物件與快照的差異;
  • RepositorySupport#detach,解除追蹤,當實體的Repository介面中自定義刪除方法時,實現類可以通過該方法解除物件的變更追蹤。

最後是RepositorySupport中對Repository介面的實現,實現中確定了RepositorySupport#onSelectRepositorySupport#onInsertRepositorySupport#onUpdateRepositorySupport#onDelete方法的呼叫時機,並通過AggregateTracingManager來管理追蹤物件:

  • RepositorySupport#find的實現中,通過RepositorySupport#onSelect查詢實體物件,並決定是否呼叫AggregateTracingManager#attach進行變更追蹤;
  • RepositorySupport#save的實現中,呼叫AggregateTracingManager#different獲取當前實體物件與快照間的差異,並根據差異的型別選擇執行RepositorySupport#onInsertRepositorySupport#onUpdate,最後呼叫AggregateTracingManager#merge將變更後的物件合併到變更追蹤容器中;
  • RepositorySupport#remove的實現中,呼叫RepositorySupport#onDelete刪除資料,並呼叫AggregateTracingManager#detach解除物件的追蹤。

AggregateTracingManager的實現

AggregateTracingManager提供了管理變更追蹤的能力,介面設計如下:

public interface AggregateTracingManager<T extends Aggregate<ID>, ID extends Identifier> {

	/**
   * 變更追蹤
	 * @param aggregate
	 */
	void attach(T aggregate);

	/**
   * 解除追蹤
	 * @param aggregate
	 */
	void detach(T aggregate);

	/**
   * 對比差異
	 * @param aggregate
	 * @return
	 */
	AggregateDifference<T, ID> different(T aggregate) throws IllegalAccessException;

	/**
   * 合併變更
	 * @param aggregate
	 */
	void merge(T aggregate);
}

接著提供一個AggregateTracingManager的實現類,我們的工程中同樣選擇了ThreadLocal來實現執行緒隔離:

public class ThreadLocalTracingManager<T extends Aggregate<ID>, ID extends Identifier> implements AggregateTracingManager<T, ID> {

	private final ThreadLocal<TraceContext<T, ID>> context;

	public ThreadLocalTracingManager() {
		this.context = ThreadLocal.withInitial(MapContext::new);
	}

	@Override
	public void attach(T aggregate) {
		this.context.get().tracing(aggregate.getId(), aggregate);
	}

	@Override
	public void detach(T aggregate) {
		this.context.get().remove(aggregate.getId());
	}

	@Override
	public AggregateDifference<T, ID> different(T aggregate) throws IllegalAccessException {
		T snapshot = this.context.get().find(aggregate.getId());
		return DifferentUtils.different(snapshot, aggregate);
	}

	@Override
	public void merge(T aggregate) {
		attach(aggregate);
	}
}

最後是定義變更追蹤中用於儲存快照的容器TraceContext介面:

public interface TraceContext<T extends Aggregate<ID>, ID extends Identifier> {

	void add(ID id, T aggregate);

	T find(ID id);

	void remove(ID id);
}

TraceContext的功能比較簡單,提供了3個方法:

  • void add(ID id, T aggregate),新增追蹤物件;
  • T find(ID id),獲取追蹤物件的快照;
  • void remove(ID id),刪除追蹤物件。

這裡我提供一個使用HashMap做儲存容器的簡單實現:

public class MapContext<T extends Aggregate<ID>, ID extends Identifier> implements TraceContext<T, ID> {

	private final Map<ID, T> snapshots;

	public MapContext() {
		this.snapshots = new HashMap<>();
	}

	@Override
	public void add(ID id, T aggregate) {
		T snapshot = SnapshotUtils.snapshot(aggregate);
		this.snapshots.put(aggregate.getId(), snapshot);
	}

	@Override
	public T find(ID id) {
		for (Map.Entry<ID, T> entry : this.snapshots.entrySet()) {
			ID entryId = entry.getKey();
			if (id.getClass().equals(entryId.getClass()) && entryId.value().equals(id.value())) {
				return entry.getValue();
			}
		}
		return snapshots.get(id);
	}

	@Override
	public void remove(ID id) {
		this.snapshots.remove(id);
	}
}

至此,我們已經完成了變更追蹤的整體框架。實際上我們在工程中實現的AggregateTracingManager和TraceContext會更加複雜,並新增了一些具有我司特色的功能,這裡大家可以根據各自的情況做出不同的實現。

變更追蹤中的工具類實現

由於《阿里技術專家詳解DDD系列 第三講 - Repository模式》文中的重點是介紹變更追蹤這項技術,因此忽略了幾個較為關鍵的工具類的實現,導致很多人在落地這項技術上遇到了困境,這裡我結合工程中的實踐,結合我個人的思考,給大家提供一個設計思路。

SnapshotUtils的實現

SnapshotUtils用於實現Aggregate的拷貝,因為在MapContext#find方法的實現中是通過型別與值的對比來獲取物件,因此我們在SnapshotUtils的實現中只需要實現深拷貝即可:

public class SnapshotUtils {

	@SuppressWarnings("unchecked")
	public static <T extends Aggregate<ID>, ID extends Identifier> T snapshot(T aggregate) throws IOException, ClassNotFoundException {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
		objectOutputStream.writeObject(aggregate);

		ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
		ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
		T snapshot = (T) objectInputStream.readObject();

		objectOutputStream.close();
		byteArrayOutputStream.close();

		objectInputStream.close();
		byteArrayInputStream.close();
		return snapshot;
	}
}

據我推測阿里巴巴大淘寶技術在文中使用的SnapshotUtils中除了Identifier外的其餘欄位是深拷貝,我們的實踐中允許Identifier也進行深拷貝,所以可以通過序列化與反序列化的方式進行深拷貝。

除了序列化的方式外,還有很多其他的方式可以實現深拷貝,我見過使用JSON工具來回倒騰實現深拷貝,或者可以使用BeanUtil等等。

Tips:有些工具的使用是有前提的,比如需要Getter和Setter方法,又或者使用序列化的方式需要繼承Serializable介面。

使用Java Objec Diff實現DiffUtils

DiffUtils用於實現兩個Java物件間的對比,因為此類需求較少所以市面上可供使用的開源工具並不是很多,相對來說Java Objec Diff是使用較為廣泛的開源專案,不過該專案最新版本是2018年更新的0.95版本,作者應該是停止維護Java Object Diff了,或是由於該專案屬於工具類專案,目前已經達到了較為完備的狀態,不需要進行太多的維護工作了。

我們先來使用Java Objec Diff專案實現一個簡單的Java物件對比工具,引入Java Objec Diff的依賴:

<dependency>
	<groupId>de.danielbechler</groupId>
	<artifactId>java-object-diff</artifactId>
	<version>0.95</version>
</dependency>

基於Java Objec Diff專案構建DiffUtils,這裡給出一個簡單的實現:

public class DiffUtils {

	public static EntityDiff diff(Object snapshot, Object obj) {
		DiffNode diffNode = ObjectDifferBuilder.buildDefault().compare(obj, snapshot);

		if (!diffNode.hasChanges()) {
			return EntityDiff.EMPTY;
		}

		EntityDiff entityDiff = new EntityDiff();
		entityDiff.setHasChanges(true);
		diffNode.visit((node, visit) -> {
			boolean hasChanges = node.hasChanges();
			Object objValue = node.canonicalGet(obj);
			Object snapshotValue = node.canonicalGet(snapshot);
			// 處理其他的邏輯和構建EntityDiff物件
		});

		return entityDiff;
	}
}

@Getter
@Setter
public class EntityDiff {

	public static final EntityDiff EMPTY = new EntityDiff();

	private boolean hasChanges;

	// 省略其餘屬性的實現

	public EntityDiff() {

	}
}

EntityDiff的結構可以根據自身工程的需求進行客製化化,我這裡只是為了展示如何通過Java Objec Diff專案構建DiffUtils。

具有我司特色的DifferentUtils

接下來就該我來獻醜了。

因為我們有一些客製化化的需求(具體原因已經記不得了),所以當時沒有選擇使用Java Objec Diff專案而是實現了具有我司特色的Java物件的對比工具類DifferentUtils。

首先是我們定義的4種差異狀態:

public enum DifferenceType {

	/**
     * 新增
     */
	ADDED(),

	/**
     * 刪除
     */
	REMOVED(),

	/**
     * 修改
     */
	MODIFIED(),

	/**
     * 無變化
     */
	UNTOUCHED()
}

接著我們對結果進行了封裝,分為兩層,第一層是標記Aggregate差異的AggregateDifference:

@Getter
@Setter
public class AggregateDifference<T extends Aggregate<ID>, ID extends Identifier> {

	/**
   * 快照物件
	 */
	private T snapshot;

	/**
   * 追蹤物件
   */
	private T aggregate;

	/**
   * 差異型別
   */
	private DifferenceType differentType;

	/**
   * 欄位差異
   */
	private Map<String, FieldDifference> fieldDifferences;
}

第二層是比較Aggregate欄位差異的FieldDifference:

@Getter
@Setter
public class FieldDifference {

	/**
	 * 欄位名
	 */
	private String name;

	/**
	 * 欄位型別
	 */
	private Type type;

	/**
	 * 快照值
	 */
	private Object snapshotValue;

	/**
	 * 當前值
	 */
	private Object tracValue;

	/**
	 * 差異型別
	 */
	private DifferenceType differenceType;
}

以及3個實現類,標記Java中原生型別的JavaTypeFieldDifference,標記集合型別的CollectionFieldDifference,以及標記實現Aggregate介面的AggregareFieldDifference:

public class JavaTypeFieldDifference extends FieldDifference {
}

@Getter
@Setter
public class CollectionFieldDifference extends FieldDifference {

	/**
	 * 集合元素差異
	 */
	private List<FieldDifference> elementDifference;

	public CollectionFieldDifference(String name, Type type, Object snapshotValue, Object tracValue) {
		super(name, type, snapshotValue, tracValue);
		this.elementDifference = new ArrayList<>();
	}
	public CollectionFieldDifference(String name, Type type, Object snapshotValue, Object tracValue, DifferenceType differenceType) {
		super(name, type, snapshotValue, tracValue, differenceType);
		this.elementDifference = new ArrayList<>();
	}
}

@Getter
@Setter
public class AggregareFieldDifference extends FieldDifference {

	private Map<String, FieldDifference> fieldDifferences;

	private final Identifier identifier;

	public AggregareFieldDifference(String name, Type type, Object snapshotValue, Object tracValue, DifferenceType differenceType, Identifier identifier) {
		super(name, type, snapshotValue, tracValue, differenceType);
		this.identifier = identifier;
		this.fieldDifferences = new HashMap<>();
	}
}

可以看到,我們在工程實踐中並不支援Map型別的欄位進行對比,這是因為在我們落地的DDD工程規範中,實現Aggregate介面的類中不允許出現Map型別的欄位,只允許Java的8種基礎型別(包裝型別),String,List,值物件以及實體。

準備工作完成後,我們開始實現DifferentUtils,首先定義方法宣告,與上面的DiffUtils#diff存在一些差異,主要在泛型的使用上:

public class DifferentUtils {
	public static <T extends Aggregate<ID>, ID extends Identifier> AggregateDifference<T, ID> different(T snapshot, T aggregate) throws IllegalAccessException {
		// 待實現
	}
}

接著我們處理兩個入參可能為null的情況進行處理,總計有4種情況:

  • snapsho == null && aggregate == null,此時認為是DifferenceType.UNTOUCHED
  • snapshot == null && aggregate != null,此時認為是DifferenceType.ADDED
  • snapshot != null && aggregate == null,此時認為是DifferenceType.REMOVED
  • snapshot != null && aggregate != null,這種情況需要對比欄位的差異。

此時我們可以得到用於入參為null時,返回DifferenceType的方法:

private static DifferenceType basicDifferentType(Object snapshot, Object aggregate) {
	if (snapshot == null && aggregate == null) {
		return DifferenceType.UNTOUCHED;
	}
	if (snapshot == null) {
		return DifferenceType.ADDED;
	}
	if (aggregate == null) {
		return DifferenceType.REMOVED;
	}
	return null;
}

我們直接在DifferentUtils#different中呼叫DifferentUtils#basicDifferentType,並補充snapshot和aggregate均不為null時的處理:

public static <T extends Aggregate<ID>, ID extends Identifier> AggregateDifference<T, ID> different(T snapshot, T aggregate) throws IllegalAccessException {
	DifferenceType basicDifferenceType = basicDifferentType(snapshot, aggregate);
	if (basicDifferenceType != null) {
		return new AggregateDifference<>(snapshot, aggregate, basicDifferenceType);
	}

	Field[] fields = ReflectionUtils.getFields(aggregate);
	// 標記Aggregate
	DifferenceType aggregateDifferentType = aggregateDifferentType(fields, snapshot, aggregate);
	// 構建AggregateDifference物件
	AggregateDifference<T, ID> aggregateDifference = new AggregateDifference<>(snapshot, aggregate, aggregateDifferentType);
	Map<String, FieldDifference> fieldDifferences = aggregateDifference.getFieldDifferences();
	// 對比欄位差異
	setDifferences(snapshot, aggregate, fields, fieldDifferences);
	return aggregateDifference
}

DifferentUtils#aggregateDifferentType方法,該方法用於對Aggregate進行標記:

public static <T extends Aggregate<ID>, ID extends Identifier> DifferenceType aggregateDifferentType(Field[] fields, T snapshot, T aggregate) throws IllegalAccessException {
  DifferenceType differenceType = basicDifferentType(snapshot, aggregate);
  if (differenceType != null) {
	  return differenceType;
  }

  boolean unchanged = true;
  for (Field field : fields) {
	  field.setAccessible(true);

		// 處理需要跳過的情形
		if (shouldSkipClass(field.getType())) {
			continue;
		}

	  if (Collection.class.isAssignableFrom(field.getType())) {
			ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
			Class<?> parameterizedClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
			if (Aggregate.class.isAssignableFrom(parameterizedClass) || Map.class.isAssignableFrom(parameterizedClass)) {
				continue;
			}
		}

		// 對比欄位差異
		Object aggregateValue = field.get(aggregate);
		Object snapshotValue = field.get(snapshot);
		if (snapshotValue == null && aggregateValue == null) {
			continue;
		} else if (snapshotValue == null) {
			unchanged = false;
			continue;
		}
		unchanged = snapshotValue.equals(aggregateValue) & unchanged;
	}
  return unchanged ? DifferenceType.UNTOUCHED : DifferenceType.MODIFIED;
}

private static boolean shouldSkipClass(Class<?> clazz) {
	return Identifier.class.isAssignableFrom(clazz) || Aggregate.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz);
}

因為該方法需要在其它位置複用,所以開始時先呼叫了DifferentUtils#aggregateDifferentType處理null的狀態;接著是跳過需要特殊處理的型別,這些型別要麼是單獨處理,要麼是不需要處理,以及當欄位的型別為Collection時,某些泛型型別也不需要處理;最後是通過Object#equals方法進行對比,並返回相應的修改狀態。

DifferentUtils#setDifferences的實現,該方法遍歷Aggregate的欄位,並對比每個欄位的差異:

private static <T extends Aggregate<ID>, ID extends Identifier> void setDifferences(T snapshot, T aggregate, Field[] fields, Map<String, FieldDifference> fieldDifferences) throws IllegalAccessException {
  for (Field field : fields) {
	  if (Identifier.class.isAssignableFrom(field.getType())) {
			continue;
		}
	
		String filedName = ReflectionUtils.getFieldName(field);
		field.setAccessible(true);

		Object snapshotValue = snapshot == null ? null : field.get(snapshot);
		Object aggregateValue = aggregate == null ? null : field.get(aggregate);
		if (snapshotValue == null && aggregateValue == null) {
			continue;
		}
	  // 對比每個欄位的差異
		FieldDifference fieldDifference = compareFiled(field, snapshotValue, aggregateValue);
		fieldDifferences.put(filedName, fieldDifference);
	}
}

DifferentUtils#compareFiled的實現,該方法將欄位進行分類對比:

@SuppressWarnings("unchecked")
private static <T extends Aggregate<ID>, ID extends Identifier> FieldDifference compareFiled(Field field, Object snapshotValue, Object aggregateValue) throws IllegalAccessException {
  ComparableType comparableType = ComparableType.comparableType(aggregateValue == null ? snapshotValue : aggregateValue);
  if (ComparableType.AGGREGATE_TYPE.equals(comparableType)) {
	  return compareAggregateType(field, (T) snapshotValue, (T) aggregateValue);
  } else if (ComparableType.COLLECTION_TYPE.equals(comparableType)) {
	  return compareCollectionType(field, snapshotValue, aggregateValue);
  } else if (ComparableType.JAVA_TYPE.equals(comparableType)) {
	  return compareJavaType(field, snapshotValue, aggregateValue);
  } else {
	  throw new UnsupportedOperationException();
  }
}

/**
 * 可比較的欄位型別
 */
enum ComparableType {
	AGGREGATE_TYPE(),
	COLLECTION_TYPE(),
	JAVA_TYPE(),
	OTHER_TYPE();
	
	public static ComparableType comparableType(@NonNull Object obj) {
		if (obj instanceof Aggregate) {
			return AGGREGATE_TYPE;
		} else if (obj instanceof Collection) {
			return COLLECTION_TYPE;
		} else if (obj instanceof Map) {
			return OTHER_TYPE;
		} else {
			return JAVA_TYPE;
		}
	}
}

DifferentUtils#compareJavaType的實現,該方法對比了Java型別欄位的差異:

private static FieldDifference compareJavaType(Field field, Object snapshotValue, Object aggregateValue) {
	String filedName = ReflectionUtils.getFieldName(field);
	Type type = field.getGenericType();
	DifferenceType differenceType = javaDifferentType(snapshotValue, aggregateValue);
	return new JavaTypeFieldDifference(filedName, type, snapshotValue, aggregateValue, differenceType);
}

public static DifferenceType javaDifferentType(Object snapshot, Object aggregate) {
	DifferenceType differenceType = basicDifferentType(snapshot, aggregate);
	if (differenceType != null) {
		return differenceType;
	}

	if (snapshot.equals(aggregate)) {
		return DifferenceType.UNTOUCHED;
	} else {
		return DifferenceType.MODIFIED;
	}
}

DifferentUtils#compareAggregateType的實現,該方法對比實現Aggregate介面的型別的欄位進行對比,通過遞迴不斷向下深入直到型別為Java型別:

private static <T extends Aggregate<ID>, ID extends Identifier> FieldDifference compareAggregateType(Field field, T snapshotValue, T aggregateValue) throws IllegalAccessException {
  String filedName = ReflectionUtils.getFieldName(field);
  Type type = field.getGenericType();

  Aggregate<?> notNullValue = snapshotValue == null ? aggregateValue : snapshotValue;
  Field[] entityFields = ReflectionUtils.getFields(notNullValue);
  Identifier id = notNullValue.getId();

  DifferenceType differenceType = aggregateDifferentType(entityFields, snapshotValue, aggregateValue);
  AggregareFieldDifference aggregareFieldDifference = new AggregareFieldDifference(filedName, type, snapshotValue, aggregateValue, differenceType, id);
  Map<String, FieldDifference> fieldDifferences = aggregareFieldDifference.getFieldDifferences();
  setDifferences(snapshotValue, aggregateValue, entityFields, fieldDifferences);
  return aggregareFieldDifference;
}

DifferentUtils#compareCollectionType的實現,該方法用於對比集合型別的

@SuppressWarnings("unchecked")
private static <T extends Aggregate<ID>, ID extends Identifier> FieldDifference compareCollectionType(Field field, Object snapshotValue, Object aggregateValue) throws IllegalAccessException {
  String filedName = ReflectionUtils.getFieldName(field);
  Type type = field.getGenericType();

  ParameterizedType parameterizedType = (ParameterizedType) type;
  Class<?> genericityClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];

  // 處理泛型為Java型別的集合
  if (!Aggregate.class.isAssignableFrom(genericityClass) && !Map.class.isAssignableFrom(genericityClass)) {
	  Collection<?> snapshotValues = (Collection<?>) snapshotValue;
	  Collection<?> aggregateValues = (Collection<?>) aggregateValue;
	  DifferenceType differenceType = collectionDifferentType(genericityClass, snapshotValues, aggregateValues);
	  return new CollectionFieldDifference(filedName, type, snapshotValue, aggregateValue, differenceType);
  }

  // 處理泛型為實現Aggreagte介面的型別的集合
  Collection<T> snapshotValues = (Collection<T>) snapshotValue;
  Collection<T> aggregateValues = (Collection<T>) aggregateValue;

  Map<Serializable, T> snapshotMap = snapshotValues.stream().collect(Collectors.toMap(snapshot -> snapshot.getId().value(), snapshot -> snapshot));
  Map<Serializable, T> aggregateMap = aggregateValues.stream().collect(Collectors.toMap(aggregate -> aggregate.getId().value(), aggregate -> aggregate));

  CollectionFieldDifference collectionFieldDifference = new CollectionFieldDifference(filedName, type, snapshotValue, aggregateValue);

  boolean unchanged = true;
  // snapshotMap與aggregateMap的交集,snapshotMap對aggregateMap的補集
  for (Serializable key : snapshotMap.keySet()) {
	  T snapshotElement = snapshotMap.get(key);
	  T aggregateElement = aggregateMap.get(key);
	  FieldDifference fieldDifferent = compareFiled(field, snapshotElement, aggregateElement);
	  unchanged = DifferenceType.UNTOUCHED.equals(fieldDifferent.getDifferenceType()) & unchanged;
	  collectionFieldDifference.getElementDifference().add(fieldDifferent);
  }
  // aggregateMap對snapshotMap的補集
  for (Serializable key : aggregateMap.keySet()) {
	  if (snapshotMap.get(key) != null) {
		  continue;
	  }
	  T aggregateElement = aggregateMap.get(key);
	  FieldDifference fieldDifferent = compareFiled(field, null, aggregateElement);
	  unchanged = DifferenceType.UNTOUCHED.equals(fieldDifferent.getDifferenceType()) & unchanged;
	  collectionFieldDifference.getElementDifference().add(fieldDifferent);
  }
  if (unchanged) {
	  collectionFieldDifference.setDifferenceType(DifferenceType.UNTOUCHED);
  } else {
	  collectionFieldDifference.setDifferenceType(DifferenceType.MODIFIED);
  }
  return collectionFieldDifference;
}

public static DifferenceType collectionDifferentType(Class<?> typeArguments, Collection<?> snapshot, Collection<?> aggregate) {
  if (CollectionUtils.isEmpty(snapshot) && CollectionUtils.isEmpty(aggregate)) {
		return DifferenceType.UNTOUCHED;
	}
	if (CollectionUtils.isEmpty(snapshot)) {
		return DifferenceType.ADDED;
	}
	if (CollectionUtils.isEmpty(aggregate)) {
		return DifferenceType.REMOVED;
	}
	if (specialHandingClass(typeArguments)) {
		return snapshot.size() == aggregate.size() ? DifferenceType.UNTOUCHED : DifferenceType.MODIFIED;
	}
	return snapshot.equals(aggregate) ? DifferenceType.UNTOUCHED : DifferenceType.MODIFIED;
}

private static boolean specialHandingClass(Class<?> clazz) {
	return shouldSkipClass(clazz) || Collection.class.isAssignableFrom(clazz);
}

我們將Collection型別的欄位分為兩類,泛型為Java型別的和泛型為實現Aggregate介面的。當集合的泛型為Java型別時,只需要使用Object#equals方法進行對比即可;當集合的泛型為Collection或Aggregate時(集合的泛型不應該出現Map或Identifier),先對數量進行對比,標記整體的變化,接著來對比每個Aggregate的差異,並進行標記。

我的想法是,先將List<T>轉換為Map<Serializable, T>,Map的key儲存Id,value儲存物件本身,這樣可以得到兩個Map:

  • Map<Serializable, T> snapshotMap
  • Map<Serializable, T> aggregateMap

先遍歷snapshotMap,取出aggregateMap中Id與之對應的物件進行比較,並一一標記,這裡處理的是snapshotMap與aggregateMap的交集,以及snapshotMap對aggregateMap的補集(即snapshotMap中有而aggregateMap中無的),實際上,我們這裡處理的是snapshotMap的全集;再遍歷aggregateMap,跳過snapshotMap中Id與之對應的物件,這裡我們處理的是aggregateMap對snapshotMap的補集(即aggregateMap中有而snapshotMap中無的);這樣,我們就處理完了兩個集合中的元素,最後再根據每個元素對比的結果標記集合的差異型別即可。

好了,以上就是具有我司特色的DifferentUtils工具類的實現,因為沒有研究過Java Object Diff的原始碼,因此不太清楚自己與大佬的差距究竟有多遠,歡迎大家提出自己的想法一起討論。

Tips:鑑於保密的原因,DifferentUtils及相關類都經過不同程度的修改,且修改後的實現並沒有經過嚴格的評審和測試,可能會出現各種各樣的BUG~~

ReflectionUtils的實現

變更追蹤的實現中還有一個反射相關的工具類ReflectionUtils,該工具類的實現可大可小,往小了可以像我下面實現的這樣:

public class ReflectionUtils {

	public static Field[] getFields(Object obj) {
		return obj.getClass().getDeclaredFields();
	}

	public static String getFieldName(Field field) {
		return field.getName();
	}
}

往大了可以加入快取等優化措施,例如ReflectionUtils#getFields加入快取Map<Class<?>, Field[]> fieldMap,將首次獲取到的結果新增到快取中,以此來提高反射工具的效能。

結語

好了,到這裡我們就一起實現了基於快照機制的變更追蹤,文章中的程式碼還比較潦草,像是毛坯房,目的是和大家分享實現過程和設計,如果要真正的在生產環境中落地,還需要做「精裝修」,這裡舉幾個我們的「精裝修」例子:

  • TraceContext的實現中,容器我們選擇了WeakHashMap,用於實現「自動」執行AggregateTracingManager#detach
  • AggregateTracingManager中我們加入了設定項,實現某些功能的設定化,這裡涉及客製化業務就不過多展開了;
  • ReflectionUtils中加入了快取機制,以此提高反射的效率。

好了,今天就到這裡了,Bye~~

推薦閱讀


如果本文對你有幫助的話,還請多多點贊支援。如果文章中出現任何錯誤,還請批評指正。最後歡迎大家關注分享硬核Java技術的金融摸魚俠王有志,我們下次再見!