介紹 Spring Sync

工程技術 | Craig Walls | 2014年10月22日 | ...

今天早些時候,我釋出了 Spring Sync 的第一個里程碑版本。這是一個透過採用基於補丁的交換方式,解決客戶端應用程式和 Spring 後端之間高效通訊的新專案。由於這是一個新專案,我認為現在是時候向您展示 Spring Sync 的功能了。

這裡給出的示例參考了 Spring REST Todos 示例和/或該示例專案中的 Todo 類。

建立和應用補丁

在最低層,Spring Sync 提供了一個庫,用於生成和應用 Java 物件的補丁。Patch 類是該庫的核心,它捕獲可以應用於物件以使其與另一個物件同步的更改。

Patch 類旨在通用,不直接與任何特定的補丁表示形式關聯。也就是說,它受到 JSON Patch 的啟發,Spring Sync 支援將 Patch 例項建立並序列化為 JSON Patch。Spring Sync 的未來版本可能包含對其他補丁表示形式的支援。

建立補丁最簡單的方法是計算兩個 Java 物件之間的差異

Todo original = ...;
Todo modified = ...;
Patch patch = Diff.diff(original, modified);

在這裡,Diff.diff() 方法將比較兩個 Todo 物件,並生成一個描述它們之間差異的 Patch

一旦有了 Patch,可以透過將物件傳遞給 apply() 方法將其應用於物件

Todo patched = patch.apply(original, Todo.class);

請注意,diff()apply() 方法是互逆的。因此,在將補丁應用於原始物件後,這些示例中打了補丁的 Todo 應該與修改後的 Todo 完全相同。

如前所述,Patch 與任何特定的補丁表示形式是解耦的。但 Spring Sync 提供 JsonPatchMaker 作為工具類,用於將 Patch 物件與 Jackson JsonNode 例項進行相互轉換,其中 JsonNode 是一個包含零個或多個操作的 ArrayNode,符合 JSON Patch 規範。例如,將 Patch 轉換為包含 JSON Patch 的 JsonNode

JsonNode jsonPatchNode = JsonPatchMaker.toJsonNode(patch);

類似地,可以從 JsonNode 建立 Patch 物件,如下所示

Patch patch = JsonPatchMaker.fromJsonNode(jsonPatchNode);

請注意,JsonPatchMaker 是用於將 Patch 物件與 JSON Patch 進行(反)序列化的臨時解決方案。它將在後續版本中被更永久的解決方案取代。

應用差異同步 (Differential Synchronization)

建立補丁需要您擁有物件的“之前”和“之後”例項,以便計算差異。雖然它沒有將它們稱為“之前”和“之後”,但 Neil Fraser 在一篇論文中描述的 差異同步 (Differential Synchronization) 演算法本質上定義了一種控制方式,透過該方式可以在兩個或多個網路節點(可能是客戶端和伺服器,但不一定僅適用於客戶端-伺服器場景)之間建立、共享和應用補丁。

應用差異同步時,每個節點都維護資源的兩個副本

  • 本地節點自己的工作副本,可以進行修改。
  • 影子副本,是本地節點對遠端節點工作副本外觀的理解。

節點可以對其本地資源副本進行任何所需的修改。節點會定期透過比較本地節點與為遠端節點維護的影子副本生成補丁。然後將補丁傳送到遠端節點。一旦傳送了補丁,節點就會將其本地副本複製到影子副本上,假設遠端節點將應用該補丁,因此其對遠端節點資源的理解與本地資源同步。

收到補丁後,節點必須將該補丁應用於它為傳送補丁的節點保留的影子副本以及它自己的本地副本(本地副本本身可能也發生了變化)。

Spring Sync 透過其 DiffSync 類支援差異同步。要建立 DiffSync,必須為其提供 ShadowStore 和它可以應用補丁的物件型別

ShadowStore shadowStore = new MapBasedShadowStore();
shadowStore.setRemoteNodeId("remoteNode");
DiffSync diffSync = new DiffSync(shadowStore, Todo.class);

一旦有了 DiffSync,您就可以使用它將 Patch 應用於物件

Todo patched = diffSync.apply(patch, todo);

apply() 方法將補丁應用於給定物件以及該物件的影子副本。如果尚未建立影子副本,它將透過深度克隆給定物件來建立一個。

ShadowStoreDiffSync 維護遠端節點影子副本的地方。對於任何給定節點,可能有多個影子儲存,每個遠端節點一個。如示例所示,其 remoteNodeId 屬性用於唯一標識遠端節點。在客戶端-伺服器拓撲中,伺服器可以使用會話 ID 來標識遠端節點。同時,客戶端(可能僅與一箇中心伺服器共享資源)可以使用任何想要的識別符號來標識伺服器節點。

DiffSync 也可以用於從儲存的影子副本建立 Patch

Patch patch = diffSync.diff(todo);

建立補丁時,將從 ShadowStore 中檢索儲存的影子副本並與給定物件進行比較。為了與差異同步流程保持一致,一旦生成補丁,給定物件將被複制到影子副本上。

值得注意的是,DiffSync 使用 Patch 物件,這些物件與任何特定的補丁表示形式是解耦的。因此,DiffSync 本身也與補丁表示形式解耦。

將 DiffSync 應用於 Web

在單個節點上建立和應用補丁意義不大。差異同步真正發揮作用的地方在於,當兩個或多個節點共享和操作同一資源時,並且您需要每個節點(在合理範圍內)保持同步。因此,Spring Sync 還提供了 DiffSyncController,這是一個處理 HTTP PATCH 請求的 Spring MVC 控制器,將差異同步應用於資源。

配置 DiffSyncController 最簡單的方法是建立一個用 @EnableDifferentialSynchronization 註解並擴充套件 DiffSyncConfigurerAdapter 類的 Spring 配置類

@Configuration
@EnableDifferentialSynchronization
public class DiffSyncConfig extends DiffSyncConfigurerAdapter {

	@Autowired
	private PagingAndSortingRepository<Todo, Long> repo;
	
	@Override
	public void addPersistenceCallbacks(PersistenceCallbackRegistry registry) {
		registry.addPersistenceCallback(new JpaPersistenceCallback<Todo>(repo, Todo.class));
	}
	
}

除其他事項外,@EnableDifferentialSynchronization 聲明瞭一個 DiffSyncController bean,併為其提供了 PersistenceCallbackRegistryShadowStore

PersistenceCallbackRegistry 是一個 PersistenceCallback 物件的登錄檔,DiffSyncController 透過它檢索和持久化它所打補丁的資源。PersistenceCallback 介面使得 DiffSyncController 可以與資源的應用特定持久化選擇解耦。例如,以下是 PersistenceCallback 的一個實現,它使用 Spring DataCrudRepository 來持久化 Todo 物件

package org.springframework.sync.diffsync.web;

import java.util.List;

import org.springframework.data.repository.CrudRepository;
import org.springframework.sync.diffsync.PersistenceCallback;

class JpaPersistenceCallback<T> implements PersistenceCallback<T> {
	
	private final CrudRepository<T, Long> repo;
	private Class<T> entityType;

	public JpaPersistenceCallback(CrudRepository<T, Long> repo, Class<T> entityType) {
		this.repo = repo;
		this.entityType = entityType;
	}
	
	@Override
	public List<T> findAll() {
		return (List<T>) repo.findAll();
	}
	
	@Override
	public T findOne(String id) {
		return repo.findOne(Long.valueOf(id));
	}
	
	@Override
	public void persistChange(T itemToSave) {
		repo.save(itemToSave);
	}
	
	@Override
	public void persistChanges(List<T> itemsToSave, List<T> itemsToDelete) {
		repo.save(itemsToSave);
		repo.delete(itemsToDelete);
	}

	@Override
	public Class<T> getEntityType() {
		return entityType;
	}
	
}

對於提供給 DiffSyncControllerShadowStore,預設將是 MapBasedShadowStore。但您可以覆蓋 DiffSyncConfigurerAdapter 中的 getShadowStore() 方法來指定不同的影子儲存實現。例如,您可以像這樣配置基於 Redis 的影子儲存

@Autowired
private RedisOperations<String, Object> redisTemplate;

@Override
public ShadowStore getShadowStore() {
	return new RedisShadowStore(redisTemplate);
}

無論您選擇哪種 ShadowStore 實現,都將宣告一個會話範圍的 bean,確保每個客戶端都將擁有自己的影子儲存例項。

由於它處理 PATCH 請求,DiffSyncController 將應用差異同步流程的一個週期

  1. 它將補丁應用於資源的伺服器副本以及傳送 PATCH 的客戶端的影子副本。
  2. 它將透過比較其本地資源與影子副本建立一個新補丁。
  3. 它將用資源的本地副本替換影子副本。
  4. 它將在響應中將新補丁傳送給客戶端。

就像 PatchDiffSync 一樣,DiffSyncController 與任何特定的補丁格式是解耦的。然而,Spring Sync 確實提供了 JsonPatchHttpMessageConverter,這樣在內容型別為 "application/json-patch+json" 的情況下,DiffSyncController 可以接收和響應 JSON Patch 格式的補丁。

結論

正如您在此處所見,Spring Sync 旨在提供一種在客戶端和伺服器(或任何共享資源的節點集)之間進行高效通訊和同步的方法。它提供了用於生成和應用補丁的低階支援,以及用於差異同步的高階支援。雖然它支援 JSON Patch,但它很大程度上獨立於任何特定的補丁格式。

這僅僅是個開始。除其他事項外,我們正在考慮...

  • 透過 WebSocket/STOMP 補充 DiffSyncController 基於 HTTP 的差異同步,實現全雙工補丁通訊。
  • 繼續完善差異同步實現,以支援資源版本控制和其他避免補丁衝突的技術。
  • 支援在客戶端 Android 應用程式中使用 Spring Sync。

請關注該專案並告訴我們您的想法。歡迎隨時 提交錯誤報告和改進建議,我們也非常歡迎您 分叉程式碼 並提交拉取請求。

如果您想了解更多關於 Spring Sync 的資訊,請檢視以下資源

訂閱 Spring 新聞通訊

訂閱 Spring 新聞通訊,保持聯絡

訂閱

搶先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲取支援

Tanzu Spring 透過一個簡單的訂閱,為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進位制檔案。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視全部