介绍 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 社区所有即将举行的活动。

查看全部