介绍 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 的临时解决方案。它将在后续版本中被更永久的解决方案取代。

应用差异同步

创建补丁需要您拥有对象的“之前”和“之后”实例,以便计算差异。尽管 Neil Fraser 在一篇论文中描述的 差异同步 算法没有将它们称为“之前”和“之后”,但它实质上定义了一种控制器方式,通过这种方式可以在两个或多个网络节点(可能是客户端和服务器,但不一定仅适用于客户端-服务器场景)之间创建、共享和应用补丁。

应用差异同步时,每个节点维护资源的两个副本

  • 本地节点自己的工作副本,它可以进行更改。
  • 一个影子副本,它是本地节点对远程节点工作副本的理解。

节点可以对其本地资源的副本进行任何所需的更改。节点会定期通过比较本地节点与其为远程节点维护的影子副本生成补丁。然后将其发送到远程节点。一旦发送了补丁,节点会将其本地副本覆盖到影子副本上,假设远程节点将应用补丁,因此它对远程节点资源的理解与本地资源同步。

收到补丁后,节点必须将补丁应用于其为发送补丁的节点保留的影子副本以及其自己的本地副本(该副本本身可能已发生更改)。

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,这是一个 Spring MVC 控制器,用于处理 HTTP PATCH 请求,将差异同步应用于资源。

配置 DiffSyncController 最简单的方法是创建一个带有 @EnableDifferentialSynchronization 注解的 Spring 配置类,并扩展 DiffSyncConfigurerAdapter

@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,为其提供一个 PersistenceCallbackRegistry 和一个 ShadowStore

PersistenceCallbackRegistryPersistenceCallback 对象的注册表,DiffSyncController 通过它检索和持久化其修补的资源。PersistenceCallback 接口使 DiffSyncController 能够与应用程序特定的资源持久化选择解耦。例如,这是一个 PersistenceCallback 的实现,它与 Spring Data CrudRepository 一起使用以持久化 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,因此 DiffSyncController 可以以 JSON Patch 格式接收和响应补丁,前提是内容类型为 "application/json-patch+json"。

结论

正如您在这里所看到的,Spring Sync 旨在提供客户端和服务器(或共享资源的任何一组节点)之间高效通信和同步的方法。它提供了生成和应用补丁的低级支持,以及使用差异同步的高级支持。尽管它支持 JSON Patch,但它在很大程度上独立于任何特定的补丁格式。

这仅仅是个开始。除其他事项外,我们正在寻求...

  • 用 WebSocket/STOMP 补充 DiffSyncController 基于 HTTP 的差异同步,以实现全双工补丁通信。
  • 持续完善差异同步实现,以支持资源版本控制和其他避免补丁冲突的技术。
  • 支持在客户端 Android 应用程序中使用 Spring Sync。

请关注该项目,并告诉我们您的想法。欢迎您 提交错误报告和改进建议,我们当然也欢迎您 分叉代码 并提交拉取请求。

如果您想了解更多关于 Spring Sync 的信息,请查看以下资源

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有