Статьи

Убегайте от праздника «пустых» проверок: правильно выполняйте PATCH с помощью JSON Patch

Сегодня мы поговорим о REST (ful) сервисах и API, точнее, об одной специфической теме, с которой сталкиваются многие опытные разработчики. Чтобы взглянуть на вещи в перспективе, мы поговорим о веб-API, где принципы REST (ful) придерживаются протокола HTTP и активно используют семантику методов HTTP и (обычно, но не обязательно) используют JSON для представления состояния.

Выделяется один конкретный метод HTTP , и хотя его значение звучит довольно просто, реализация далека от этого. Да, мы смотрим на тебя, патч . Так в чем же проблема? Это просто обновление, верно? Да, по сути семантика метода PATCH в контексте веб-служб REST (ful) на основе HTTP является частичным обновлением ресурса. Теперь, как бы вы это сделали, разработчик Java? Здесь начинается самое интересное.

Давайте рассмотрим очень простой пример API управления книгами, смоделированный с использованием новейшей спецификации JSR 370: Java API для RESTful Web Services (JAX-RS 2.1) (которая наконец-то включает аннотацию @PATCH !) И потрясающую платформу Apache CXF . Наш ресурс — это очень упрощенный класс Книги .

1
2
3
4
5
public class Book {
    private String title;
    private Collection>String< authors;
    private String isbn;
}

Как бы вы реализовали частичное обновление, используя метод PATCH ? К сожалению, решение о грубой силе, null пир, является очевидным победителем здесь.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@PATCH
@Path("/{isbn}")
@Consumes(MediaType.APPLICATION_JSON)
public void update(@PathParam("isbn") String isbn, Book book) {
    final Book existing = bookService.find(isbn).orElseThrow(NotFoundException::new);
         
    if (book.getTitle() != null) {
        existing.setTitle(book.getTitle());
    }
 
    if (book.getAuthors() != null) {
        existing.setAuthors(book.getAuthors());
    }
         
    // And here it goes on and on ...
    // ...
}

В двух словах, это нулевой клон PUT . Возможно, кто-то может утверждать, что это отчасти работает, и объявить здесь победу. Но, надеюсь, для большинства из нас этот подход явно имеет много недостатков и никогда не должен приниматься. Альтернативы? Да, безусловно, RFC-6902: патч JSON , пока еще не является официальным стандартом, но он уже там.

RFC-6902: патч JSON кардинально меняет игру, выражая последовательность операций для применения к документу JSON . Чтобы проиллюстрировать идею в действии, давайте начнем с простого примера изменения названия книги, описанного в терминах желаемого результата.

1
{ "op": "replace", "path": "/title", "value": "..." }

Выглядит чисто, а как насчет добавления авторов? Легко …

1
{ "op": "add", "path": "/authors", "value": ["...", "..."] }

Потрясающе, распродано, но … в плане реализации, кажется, требуется довольно много работы, не так ли? Не совсем, если мы полагаемся на новейшую и лучшую версию JSR 374: Java API для JSON Processing 1.1, которая полностью поддерживает RFC-6902: JSON Patch Вооружившись правильными инструментами, на этот раз давайте сделаем это правильно.

1
2
3
org.glassfish
    javax.json
    1.1.2

Интересно, что не многие знают о том, что Apache CXF и вообще любая JAX-RS- инфраструктура для жалоб тесно интегрируется с JSON-P и поддерживает его основные типы данных. В случае Apache CXF это просто вопрос добавления зависимости модуля cxf-rt-rs-extension-providers :

1
2
3
org.apache.cxf
    cxf-rt-rs-extension-providers
    3.2.2

И регистрируя JsrJsonpProvider с помощью фабричного компонента вашего сервера, например:

01
02
03
04
05
06
07
08
09
10
11
12
@Configuration
public class AppConfig {
    @Bean
    public Server rsServer(Bus bus, BookRestService service) {
        JAXRSServerFactoryBean endpoint = new JAXRSServerFactoryBean();
        endpoint.setBus(bus);
        endpoint.setAddress("/");
        endpoint.setServiceBean(service);
        endpoint.setProvider(new JsrJsonpProvider());
        return endpoint.create();
    }
}

Со всеми связанными частями наша операция PATCH может быть реализована с использованием JSR 374: Java API только для JSON Processing 1.1 , всего в несколько строк:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Service
@Path("/catalog")
public class BookRestService {
    @Inject private BookService bookService;
    @Inject private BookConverter converter;
 
    @PATCH
    @Path("/{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    public void apply(@PathParam("isbn") String isbn, JsonArray operations) {
        final Book book = bookService.find(isbn).orElseThrow(NotFoundException::new);
        final JsonPatch patch = Json.createPatch(operations);
        final JsonObject result = patch.apply(converter.toJson(book));
        bookService.update(isbn, converter.fromJson(result));
    }
}

BookConverter выполняет преобразование между классом Book и его представлением JSON (и наоборот), которое мы делаем вручную, чтобы проиллюстрировать еще одну возможность, предоставляемую JSR 374: Java API для JSON Processing 1.1 .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
public class BookConverter {
    public Book fromJson(JsonObject json) {
        final Book book = new Book();
        book.setTitle(json.getString("title"));
        book.setIsbn(json.getString("isbn"));
        book.setAuthors(
            json
                .getJsonArray("authors")
                .stream()
                .map(value -> (JsonString)value)
                .map(JsonString::getString)
                .collect(Collectors.toList()));
        return book;
    }
 
    public JsonObject toJson(Book book) {
        return Json
            .createObjectBuilder()
            .add("title", book.getTitle())
            .add("isbn", book.getIsbn())
            .add("authors", Json.createArrayBuilder(book.getAuthors()))
            .build();
    }
}

В завершение давайте обернем этот простой веб-API JAX-RS 2.1 в красивый конверт Spring Boot .

1
2
3
4
5
6
@SpringBootApplication
public class BookServerStarter {   
    public static void main(String[] args) {
        SpringApplication.run(BookServerStarter.class, args);
    }
}

И запустить его.

1
mvn spring-boot:run

Чтобы завершить обсуждение, давайте немного поиграем с более реалистичными примерами, намеренно добавив в наш каталог неполную книгу .

01
02
03
04
05
06
07
08
09
10
11
12
13
$ curl -i -X POST http://localhost:19091/services/catalog -H "Content-Type: application\json" -d '{
       "title": "Microservice Architecture",
       "isbn": "978-1491956250",
       "authors": [
           "Ronnie Mitra",
           "Matt McLarty"
       ]
   }'
 
HTTP/1.1 201 Created
Date: Tue, 20 Feb 2018 02:30:18 GMT
Location: http://localhost:19091/services/catalog/978-1491956250
Content-Length: 0

Есть несколько неточностей, которые мы хотели бы исправить в описании этой книги, а именно, чтобы завершить заголовок «Архитектура микросервиса: согласование принципов, практик и культуры» , и включить пропавших соавторов, Ираклия Надареишвили и Майка Амундсена . С API, который мы разработали минуту назад, это не просто.

1
2
3
4
5
6
7
8
$ curl -i -X PATCH http://localhost:19091/services/catalog/978-1491956250 -H "Content-Type: application\json" -d '[
       { "op": "add", "path": "/authors/0", "value": "Irakli Nadareishvili" },
       { "op": "add", "path": "/authors/-", "value": "Mike Amundsen" },
       { "op": "replace", "path": "/title", "value": "Microservice Architecture: Aligning Principles, Practices, and Culture" }
   ]'
 
HTTP/1.1 204 No Content
Date: Tue, 20 Feb 2018 02:38:48 GMT

Ссылка на путь первых двух операций может выглядеть немного запутанно, но больше не бояться, давайте проясним это. Поскольку authors — это коллекция (или с точки зрения типов данных JSON , массива), мы могли бы использовать обозначение индекса массива RFC-6902: JSON Patch, чтобы точно указать, куда мы хотели бы вставить новый элемент. Первая операция использует индекс '0' для обозначения позиции головы, а вторая — '-' для упрощения, скажем, «добавить в конец коллекции». Если мы получим книгу сразу после обновления, мы увидим, что наши изменения будут применены в точности так, как мы просили.

01
02
03
04
05
06
07
08
09
10
11
12
$ curl http://localhost:19091/services/catalog/978-1491956250
 
{
    "title": "Microservice Architecture: Aligning Principles, Practices, and Culture",
    "isbn": "978-1491956250",
    "authors": [
        "Irakli Nadareishvili",
        "Ronnie Mitra",
        "Matt McLarty",
        "Mike Amundsen"
    ]
}

Чисто, просто и мощно. Чтобы быть справедливым, есть цена, которую нужно заплатить, это форма дополнительных JSON- манипуляций (чтобы применить патч), но стоит ли это усилий? Я верю, что это …

В следующий раз, когда вы собираетесь разрабатывать новые блестящие веб-API REST (ful) , пожалуйста, серьезно подумайте о RFC-6902: JSON Patch, чтобы поддержать реализацию ваших ресурсов в PATCH . Я полагаю, что более тесная интеграция с JAX-RS также идет (если не там), чтобы напрямую поддерживать класс JSONPatch и его семейство.

И наконец, что не менее важно, в этой статье мы затронули только реализацию на стороне сервера, но JSR 374: Java API для JSON Processing 1.1 также включает в себя удобные клиентские леса, предоставляя полноценный программный контроль над исправлениями.

1
2
3
4
5
final JsonPatch patch = Json.createPatchBuilder()
    .add("/authors/0", "Irakli Nadareishvili")
    .add("/authors/-", "Mike Amundsen")
    .replace("/title", "Microservice Architecture: Aligning Principles, Practices, and Culture")
    .build();

Полные источники проекта доступны на Github .

Опубликовано на Java Code Geeks с разрешения Андрея Редько, партнера нашей программы JCG . См. Оригинальную статью здесь: Избегайте праздника «пустых» проверок: правильно выполняйте PATCH с помощью JSON Patch (RFC-6902)

Мнения, высказанные участниками Java Code Geeks, являются их собственными.