Verwenden des JSON-Patches in Spring-REST-APIs

1. Einleitung

Von den verschiedenen verfügbaren HTTP-Methoden spielt die HTTP-PATCH-Methode eine einzigartige Rolle. Es ermöglicht uns, Teilaktualisierungen auf HTTP-Ressourcen anzuwenden.

In diesem Tutorial erfahren Sie, wie Sie die HTTP PATCH-Methode zusammen mit dem JSON Patch-Dokumentformat verwenden, um teilweise Aktualisierungen auf unsere RESTful-Ressourcen anzuwenden.

2. Der Anwendungsfall

Betrachten wir zunächst ein Beispiel für eine HTTP- Kundenressource , die im JSON-Dokument dargestellt wird:

{ "id":"1", "telephone":"001-555-1234", "favorites":["Milk","Eggs"], "communicationPreferences": {"post":true, "email":true} }

Nehmen wir an, dass die Telefonnummer dieses Kundenhat sich geändert und der Kunde hat einen neuen Artikel zu seiner Liste der Lieblingsprodukte hinzugefügt. Dies bedeutet, dass wir nur die Telefon- und Favoritenfelder des Kunden aktualisieren müssen .

Wie würden wir das machen?

Die beliebte HTTP-PUT-Methode kommt zuerst in den Sinn. Da der PUT jedoch eine Ressource vollständig ersetzt, ist es keine geeignete Methode, Teilaktualisierungen elegant anzuwenden. Darüber hinaus müssen die Clients ein GET durchführen, bevor die Aktualisierungen angewendet und gespeichert werden.

Hier bietet sich die HTTP-PATCH-Methode an.

Lassen Sie uns die HTTP PATCH-Methode und die JSON-Patch-Formate verstehen.

3. Die HTTP PATCH-Methode und das JSON-Patch-Format

Die HTTP-PATCH-Methode bietet eine gute Möglichkeit, teilweise Aktualisierungen auf Ressourcen anzuwenden. Infolgedessen müssen Clients nur die Unterschiede in ihren Anforderungen senden.

Schauen wir uns ein einfaches Beispiel für eine HTTP-PATCH-Anforderung an:

PATCH /customers/1234 HTTP/1.1 Host: www.example.com Content-Type: application/example If-Match: "e0023aa4e" Content-Length: 100 [description of changes]

Der HTTP PATCH-Anforderungshauptteil beschreibt, wie die Zielressource geändert werden sollte, um eine neue Version zu erstellen. Darüber hinaus variiert das Format zur Darstellung der [Beschreibung der Änderungen] je nach Ressourcentyp. Für JSON-Ressourcentypen lautet das zur Beschreibung der Änderungen verwendete Format JSON-Patch.

Einfach ausgedrückt verwendet das JSON-Patch-Format eine „Reihe von Vorgängen“, um zu beschreiben, wie die Zielressource geändert werden sollte. Ein JSON-Patch-Dokument ist ein Array von JSON-Objekten. Jedes Objekt im Array repräsentiert genau eine JSON-Patch-Operation.

Schauen wir uns nun die JSON-Patch-Operationen zusammen mit einigen Beispielen an.

4. JSON-Patch-Vorgänge

Ein JSON Patch - Betrieb wird durch ein einzelnes dargestellt op - Objekt.

Hier definieren wir beispielsweise eine JSON-Patch-Operation, um die Telefonnummer des Kunden zu aktualisieren:

{ "op":"replace", "path":"/telephone", "value":"001-555-5678" }

Jede Operation muss ein Pfadmitglied haben . Außerdem müssen einige Operationsobjekte auch ein from- Member enthalten . Der Wert des Pfads und von Mitgliedern ist ein JSON-Zeiger. Es bezieht sich auf einen Ort innerhalb des Zieldokuments. Diese Position kann auf einen bestimmten Schlüssel oder ein Array-Element im Zielobjekt verweisen.

Lassen Sie uns nun kurz die verfügbaren JSON-Patch-Vorgänge betrachten.

4.1. Die Add- Operation

Wir verwenden die Add- Operation, um einem Objekt ein neues Mitglied hinzuzufügen. Wir können es auch verwenden, um ein vorhandenes Mitglied zu aktualisieren und einen neuen Wert am angegebenen Index in das Array einzufügen.

Fügen wir beispielsweise "Brot" zur Favoritenliste des Kunden bei Index 0 hinzu:

{ "op":"add", "path":"/favorites/0", "value":"Bread" }

Die geänderten Kundendaten nach dem Hinzufügen wären:

{ "id":"1", "telephone":"001-555-1234", "favorites":["Bread","Milk","Eggs"], "communicationPreferences": {"post":true, "email":true} }

4.2. Das Entfernen Betrieb

Der Entfernungsvorgang entfernt einen Wert am Zielort. Außerdem kann es ein Element aus einem Array am angegebenen Index entfernen.

Entfernen wir zum Beispiel die Kommunikationspräferenzen für unseren Kunden:

{ "op":"remove", "path":"/communicationPreferences" }

Die geänderten Kundendaten nach dem Entfernungsvorgang wären:

{ "id":"1", "telephone":"001-555-1234", "favorites":["Bread","Milk","Eggs"], "communicationPreferences":null }

4.3. Der Ersatz Betrieb

Die Ersetzungsoperation aktualisiert den Wert am Zielort mit einem neuen Wert.

Lassen Sie uns als Beispiel die Telefonnummer für unseren Kunden aktualisieren:

{ "op":"replace", "path":"/telephone", "value":"001-555-5678" }

Die geänderten Kundendaten nach dem Ersetzungsvorgang wären:

{ "id":"1", "telephone":"001-555-5678", "favorites":["Bread","Milk","Eggs"], "communicationPreferences":null }

4.4. Der Umzug Betrieb

Der Verschiebevorgang entfernt den Wert am angegebenen Speicherort und fügt ihn dem Zielspeicherort hinzu.

Verschieben wir beispielsweise "Brot" vom Anfang der Favoritenliste des Kunden zum Ende der Liste:

{ "op":"move", "from":"/favorites/0", "path":"/favorites/-" }

Die geänderten Kundendaten nach dem Umzug wären:

{ "id":"1", "telephone":"001-555-5678", "favorites":["Milk","Eggs","Bread"], "communicationPreferences":null } 

Die / Favoriten / 0 und / Favoriten / - im obigen Beispiel sind JSON-Zeiger auf die Start- und Endindizes des Favoriten- Arrays.

4.5. Die Kopie Bedienung

Der Kopiervorgang kopiert den Wert am angegebenen Speicherort an den Zielspeicherort.

Beispiel: Duplizieren Sie "Milch" in der Favoritenliste :

{ "op":"copy", "from":"/favorites/0", "path":"/favorites/-" }

Die geänderten Kundendaten nach dem Kopiervorgang wären:

{ "id":"1", "telephone":"001-555-5678", "favorites":["Milk","Eggs","Bread","Milk"], "communicationPreferences":null }

4.6. Der Testbetrieb

Die Testoperation testet, dass der Wert am "Pfad" gleich dem "Wert" ist. Da die PATCH-Operation atomar ist, sollte die PATCH verworfen werden, wenn eine ihrer Operationen fehlschlägt. Mit dem Testvorgang kann überprüft werden, ob die Voraussetzungen und Nachbedingungen erfüllt sind.

Testen wir zum Beispiel, ob die Aktualisierung des Telefonfelds des Kunden erfolgreich war:

{ "op":"test", "path":"/telephone", "value":"001-555-5678" } 

Lassen Sie uns nun sehen, wie wir die obigen Konzepte auf unser Beispiel anwenden können.

5. HTTP-PATCH-Anforderung im JSON-Patch-Format

Wir werden unseren Kundenanwendungsfall erneut prüfen .

Hier ist die HTTP-PATCH-Anforderung, um eine teilweise Aktualisierung der Telefon- und Favoritenliste des Kunden im JSON-Patch-Format durchzuführen :

curl -i -X PATCH //localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[ {"op":"replace","path":"/telephone","value":"+1-555-56"}, {"op":"add","path":"/favorites/0","value":"Bread"} ]' 

Am wichtigsten ist, dass der Inhaltstyp für JSON-Patch-Anforderungen application / json-patch + json ist . Der Anforderungshauptteil ist außerdem ein Array von JSON-Patch-Operationsobjekten:

[ {"op":"replace","path":"/telephone","value":"+1-555-56"}, {"op":"add","path":"/favorites/0","value":"Bread"} ]

Wie würden wir eine solche Anfrage auf der Serverseite verarbeiten?

Eine Möglichkeit besteht darin, ein benutzerdefiniertes Framework zu schreiben, das die Operationen nacheinander auswertet und als atomare Einheit auf die Zielressource anwendet. Dieser Ansatz klingt eindeutig kompliziert. Dies kann auch zu einer nicht standardisierten Art des Verbrauchs von Patchdokumenten führen.

Glücklicherweise müssen wir die Verarbeitung von JSON-Patch-Anforderungen nicht von Hand vornehmen.

Die ursprünglich in JSR 353 definierte Java-API für JSON-Verarbeitung 1.0 oder JSON-P 1.0 führte die Unterstützung für den JSON-Patch in JSR 374 ein. Die JSON-P-API bietet den JsonPatch- Typ zur Darstellung der JSON-Patch-Implementierung.

JSON-P ist jedoch nur eine API. Um mit der JSON-P-API arbeiten zu können, müssen wir eine Bibliothek verwenden, die sie implementiert. Für die Beispiele in diesem Artikel verwenden wir eine solche Bibliothek namens json-patch.

Let's now look at how we can build a REST service that consumes HTTP PATCH requests using the JSON Patch format described above.

6. Implementing JSON Patch in a Spring Boot Application

6.1. Dependencies

The latest version of json-patch can be found from the Maven Central repository.

To begin with, let's add the dependencies to the pom.xml:

 com.github.java-json-tools json-patch 1.12 

Now, let's define a schema class to represent the Customer JSON document :

public class Customer { private String id; private String telephone; private List favorites; private Map communicationPreferences; // standard getters and setters }

Next, we'll look at our controller method.

6.2. The REST Controller Method

Then, we can implement HTTP PATCH for our customer use case:

@PatchMapping(path = "/{id}", consumes = "application/json-patch+json") public ResponseEntity updateCustomer(@PathVariable String id, @RequestBody JsonPatch patch) { try { Customer customer = customerService.findCustomer(id).orElseThrow(CustomerNotFoundException::new); Customer customerPatched = applyPatchToCustomer(patch, customer); customerService.updateCustomer(customerPatched); return ResponseEntity.ok(customerPatched); } catch (JsonPatchException | JsonProcessingException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } catch (CustomerNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } } 

Let's now understand what is going on in this method:

  • To begin with, we use the @PatchMapping annotation to mark the method as a PATCH handler method
  • When a patch request with the application/json-patch+json “Content-Type” arrives, Spring Boot uses the default MappingJackson2HttpMessageConverter to convert the request payload to a JsonPatch instance. As a result, our controller method will receive the request body as a JsonPatch instance

Within the method:

  1. First, we call the customerService.findCustomer(id) method to find the customer record
  2. Subsequently, if the customer record is found, we invoke the applyPatchToCustomer(patch, customer) method. This applies the JsonPatch to the customer (more on this later)
  3. We then invoke the customerService.updateCustomer(customerPatched) to save the customer record
  4. Finally, we return a 200 OK response to the client with the patched Customer details in the response

Most importantly, the real magic happens in the applyPatchToCustomer(patch, customer) method:

private Customer applyPatchToCustomer( JsonPatch patch, Customer targetCustomer) throws JsonPatchException, JsonProcessingException { JsonNode patched = patch.apply(objectMapper.convertValue(targetCustomer, JsonNode.class)); return objectMapper.treeToValue(patched, Customer.class); } 
  1. To begin with, we have our JsonPatch instance that holds the list of operations to be applied to the target Customer
  2. We then convert the target Customer into an instance of com.fasterxml.jackson.databind.JsonNode and pass it to the JsonPatch.apply method to apply the patch. Behind the scenes, the JsonPatch.apply deals with applying the operations to the target. The result of the patch is also a com.fasterxml.jackson.databind.JsonNode instance
  3. We then call the objectMapper.treeToValue method, which binds the data in the patched com.fasterxml.jackson.databind.JsonNode to the Customer type. This is our patched Customer instance
  4. Finally, we return the patched Customer instance

Let's now run some tests against our API.

6.3. Testing

To begin with, let's create a customer using a POST request to our API:

curl -i -X POST //localhost:8080/customers -H "Content-Type: application/json" -d '{"telephone":"+1-555-12","favorites":["Milk","Eggs"],"communicationPreferences":{"post":true,"email":true}}' 

We receive a 201 Created response:

HTTP/1.1 201 Location: //localhost:8080/customers/1 

The Location response header is set to the location of the new resource. It indicates that the id of the new Customer is 1.

Next, let's request a partial update to this customer using a PATCH request:

curl -i -X PATCH //localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[ {"op":"replace","path":"/telephone","value":"+1-555-56"}, {"op":"add","path":"/favorites/0","value": "Bread"} ]'

We receive a 200OK response with the patched customer details:

HTTP/1.1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Fri, 14 Feb 2020 21:23:14 GMT {"id":"1","telephone":"+1-555-56","favorites":["Bread","Milk","Eggs"],"communicationPreferences":{"post":true,"email":true}}

7. Conclusion

In this article, we looked at how to implement JSON Patch in Spring REST APIs.

To begin with, we looked at the HTTP PATCH method and its ability to perform partial updates.

We then looked into what is JSON Patch and understood the various JSON Patch operations.

Schließlich haben wir erläutert, wie eine HTTP-PATCH-Anforderung in einer Spring Boot-Anwendung mithilfe der json-patch-Bibliothek verarbeitet wird.

Wie immer ist der Quellcode für die in diesem Artikel verwendeten Beispiele auf GitHub verfügbar.