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是根据 JSON Patch 规范包含零个或多个操作的ArrayNode)。例如,要将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检索存储的影子副本,并与给定对象进行比较。为了保持差异同步流程,一旦生成补丁,给定对象将被复制到影子副本上。

值得注意的是,DiffSyncPatch对象一起工作,Patch对象与任何特定的补丁表示方式无关。因此,DiffSync本身也与补丁表示方式无关。

将 DiffSync 应用于 Web

在一个节点上创建和应用补丁有点意义不大。差异同步真正发挥作用的地方是当两个或多个节点共享和操作相同的资源时,并且您需要每个节点保持同步(尽可能合理)。因此,Spring Sync 还提供DiffSyncController,这是一个 Spring MVC 控制器,它处理HTTP PATCH请求,将差异同步应用于资源。

配置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

PersistenceCallbackRegistryPersistenceCallback对象的注册表,DiffSyncController将通过它检索和持久化它修补的资源。PersistenceCallback接口使DiffSyncController能够与资源的特定于应用程序的持久性选择分离。例如,这是一个使用Spring DataCrudRepository来持久化Todo对象的PersistenceCallback实现:

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。

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

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

获取Spring新闻通讯

关注Spring新闻通讯

订阅

领先一步

VMware提供培训和认证,以加快您的进度。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部