Creating and applying patches
At its lowest level, Spring Sync provides a library for producing and applying patches to Java objects. ThePatch
class is the centerpiece of this library, capturing the changes that
can be applied to an object to bring it in sync with another object. The
Patch class aims to be generic, not associated directly with any particular representation of a patch. That said, it is inspired by JSON Patch and Spring Sync provides support for creating and serializing Patch instances as JSON Patch. Future versions of Spring Sync may include support for other patch representations.The easiest way to create a patch is to perform a difference between two Java objects:
Todo original = ...;
Todo modified = ...;
Patch patch = Diff.diff(original, modified);
Diff.diff() method will compare the two Todo objects and produce a Patch that describes the difference between them.Once you have a
Patch, it can be applied to an object by passing in the object to the apply() method:Todo patched = patch.apply(original, Todo.class);
diff() and apply() methods are the inverse of each other. Therefore, the patched Todo in these examples should be identical to the modified Todo after applying the patch to the original.As I mentioned,
Patch is decoupled from any particular patch representation. But Spring Sync offers JsonPatchMaker as a utility class to convert Patch objects to/from Jackson JsonNode instances where the JsonNode is an ArrayNode containing zero or more operations per the JSON Patch specification. For example, to convert a Patch to a JsonNode containing JSON Patch:JsonNode jsonPatchNode = JsonPatchMaker.toJsonNode(patch);
Patch object can be created from a JsonNode like this:Patch patch = JsonPatchMaker.fromJsonNode(jsonPatchNode);
JsonPatchMaker is a temporary solution to (de)serializing Patch objects to/from JSON Patch. It will be replaced with a more permanent solution in a later release.Applying Differential Synchronization
Creating patches requires that you have both before and after instances of an object from which to calculate the difference. Although it doesn't refer to them as "before" and "after", the Differential Synchronization algorithm described in a paper by Neil Fraser essentially defines a controller manner by which patches can be created, shared, and applied between two or more network nodes (perhaps client and server, but not necessarily applicable only to client-server scenarios).When applying Differential Synchronization, each node maintains two copies of a resource:
- The local node's own working copy that it may change.
- A shadow copy which is the local node's understanding of what a remote node's working copy looks like.
Upon receiving a patch, a node must apply the patch to the shadow that it keeps for the node that sent the patch and to its own local copy (which may have had changes itself).
Spring Sync supports Differential Synchronization through its
DiffSync class. To create a DiffSync, you must supply it with a ShadowStore and the object type that it can apply patches for:ShadowStore shadowStore = new MapBasedShadowStore();
shadowStore.setRemoteNodeId("remoteNode");
DiffSync diffSync = new DiffSync(shadowStore, Todo.class);
DiffSync in hand, you can use it to apply a Patch to an object:Todo patched = diffSync.apply(patch, todo);
apply() method will apply the patch to both the
given object as well as the shadow copy of that same object. If no
shadow copy has yet been created, it will create one by deep-cloning the
given object.The
ShadowStore is where DiffSync maintains
its copy of shadow copies for a remote node. For any given node, there
may be multiple shadow stores, one for each remote node it deals with.
As you can see in the example, its remoteNodeId property is
set to uniquely identify the remote node. In a client-server topology,
the server may use the session ID to identify the remote node.
Meanwhile, the clients (which are probably only sharing the resource
with one central server) may use any identifier they want to identify
the server node.DiffSync can also be used to create a Patch from a stored shadow copy:Patch patch = diffSync.diff(todo);
ShadowStore
and compared with the given object. In keeping with the Differential
Synchronization flow, the given object will be copied over the shadow
once the patch is produced.It's worth noting that
DiffSync works with Patch objects which are decoupled from any particular patch representation. Therefore, DiffSync itself is decoupled from the patch representation as well.Taking DiffSync to the web
Creating and applying patches on a single node is somewhat pointless. Where Differential Synchronization really shines is when two or more nodes are sharing and manipulating the same resource and you need each node to remain in sync (as much as is reasonable). Therefore, Spring Sync also offersDiffSyncController a Spring MVC controller that handles HTTP PATCH requests, applying Differential Synchronization to a resource.The easiest way to configure
DiffSyncController is to create a Spring configuration class that is annotated with @EnableDifferentialSynchronization and extend the DiffSyncConfigurerAdapter class:@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 declares a DiffSyncController bean, providing it with a PersistenceCallbackRegistry and a ShadowStore. The
PersistenceCallbackRegistry is a registry of PersistenceCallback objects through which DiffSyncController will retrieve and persist resources it patches. The PersistenceCallback interface enables DiffSyncController to be decoupled from the application-specific persistence choices for the resource. As an example, here's an implementation of PersistenceCallback that works with a Spring Data CrudRepository to persist Todo objects: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;
}
}
ShadowStore given to DiffSyncController, it will be a MapBasedShadowStore by default. But you can override the getShadowStore() method from DiffSyncConfigurerAdapter to specify a different shadow store implementation. For example, you may configure a Redis-based shadow store like this:@Autowired
private RedisOperations<String, Object> redisTemplate;
@Override
public ShadowStore getShadowStore() {
return new RedisShadowStore(redisTemplate);
}
ShadowStore you
choose, a session-scoped bean will be declared, ensuring that each
client will receive their own instance of the shadow store.As it handles PATCH requests,
DiffSyncController will apply one cycle of the Differential Sychronization flow:- It will apply the patch to the server copy of the resource and to the shadow copy for the client who sent the PATCH.
- It will create a new patch by comparing its local resource with the shadow copy.
- It will replace the shadow copy with the local copy of the resource.
- It will send the new patch on the response to the client.
Patch and DiffSync, DiffSyncController is decoupled from any particular patch format. Spring Sync does provide JsonPatchHttpMessageConverter, however, so that DiffSyncController can receive and response with JSON Patch-formated patches, given "application/json-patch+json" as the content type. Conclusion
As you've seen here, Spring Sync aims to provide a means of efficient communication and synchronization between a client and a server (or any set of nodes that share a resource). It provides low-level support for producing and applying patches as well as higher-level support for working with Differential Synchronization. Although it comes with support for JSON Patch, it is largely independent of any specific patch format.This is just the beginning. Among other things, we're looking to...
- Complement
DiffSyncController's HTTP-based Differential Synchronization with WebSocket/STOMP for full-duplex patch communication. - Continued refinement of the Differential Synchronization implementation to support resource versioning and other techniques to avoid patching conflicts.
- Support for using Spring Sync in client-side Android applications.