Версионирование эндпоинтов — это просто

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Команда Spring АйО перевела и адаптировала доклад "Endpoint versioning made simple" Бауке Найхаус (Bouke Nijhuis) с последнего Devoxx Belgium. 

В докладе автор объясняет, зачем нужно версионировать API, и подробно сравнивает различные подходы к реализации этой задачи.


Что такое версионирование эндпоинтов?

Начать следует с объяснения того, что такое версионирование эндпоинтов. Эндпоинты могут иметь несколько версий. Другими словами, взаимодействуя с сервисом, надо указывать версию эндпоинта при каждом его вызове. Из многочисленных способов сделать это мы приведем лишь несколько примеров, отражающих наиболее распространенные варианты. Вы можете поставить версию в середине между API и ресурсом:

api/v1/resource

Вы также можете указать ее в конце как параметр запроса:

/api/resource?version=1

Другой вопрос, требующий прояснения — это зачем нам вообще нужно версионирование эндпоинтов. И ответ на него следующий: версионирование эндпоинтов обеспечивает дополнительную гибкость в контракте между фронтендом и бэкендом или потребителями и производителями (consumers/producers). Наилучший способ объяснить, почему оно нам необходимо — это объяснить, что случится, если у нас его не будет. 

Без версионирования эндпоинтов выпуск релизов становится опасным. Если вы не используете версионирование эндпоинтов, и у вас присутствуют фронтенд и бэкенд, и в программном продукте произошли радикальные изменения, относящиеся к контракту между этими двумя компонентами, вы должны запустить их точно в один и тот же момент времени. А это довольно сложно. В случае неудачного деплоймента одного из компонентов отсутствие версионирования эндпоинтов может привести к даунтайму всей системы, который продлится от нескольких минут до нескольких часов.

Поэтому мы делаем версионирование эндпоинтов.

Оно позволяет использовать так называемую “разницу на шаг” (pace difference). Это означает, что можно подготовить бэкенд, добавить новые версии к бэкенду и затем поднять этот новый бэкенд, и он будет содержать две версии: старую и новую; при этом фронтэнд все еще будет общаться со старой версией эндпоинта. В более поздний момент времени произойдет релиз нового фронтенда, и он будет общаться с новой версией эндпоинта на бэкенде. Это положит конец необходимости релизить оба компонента в одно и то же время.

Версионирование эндпоинтов также экономит множество шагов и спасает нас от многочисленных рисков. Однако оно создает и некоторые дополнительные сложности, о чем мы поговорим далее.

В чем основная сложность традиционного подхода?

Давайте посмотрим на то, что происходит при разработке нового проекта, который  использует версионированные эндпоинты.

В процессе разработки команда регулярно добавляла новые ресурсы и их версии. В какой-то момент потребителям стало трудно уследить за всеми версиями. Проиллюстрировать это можно простым примером:

У нас есть три ресурса: Customer (клиент), Product (товар) и Order (заказ).  На начальной стадии развития проекта у всех ресурсов есть по одноиу эндпоинту, и, соответственно, версия этого  эндпоинта — v1, как показано на рисунке.  Чтобы создать один order, необходимы customer и product, и в скобках написано (c1, p1), что означает, что вам нужен customer версии v1 и product версии v1, чтобы создать order версии v1. Пока все просто. Но если добавить новые версии, это будет выглядеть вот так: 

Новые версии обозначены розовым цветом, и справа под orders вы видите, что вам уже нужны orders версии v2, которые требуют customers версии v2, и затем v3 уже нуждается в двух видах входящей информации, и с добавлением большего количества версий ситуация все больше усложняется. И будет еще хуже, если добавить еще один ресурс. 

После долгих раздумий о возможных решениях данной проблемы родилась идея глобального версионирования. О ней и поговорим в данной статье далее. 

Традиционный подход к версионированию предполагает, что существует номер версии для каждого ресурса и каждого метода. Например, если вы хотите применить метод GET к ресурсу Car, вы используете версию 5, если вы хотите применить метод PUT к ресурсу Car, вам нужна версия 8, а если вы хотите применить метод POST к ресурсу Car, вы используете версию 11. При глобальном версионировании предполагается,что один номер версии будет применяться ко всему API.

Итак, каждый раз, когда вы меняете что-то в контракте где-то в вашем API, вы можете увеличить номер версии на 1. Возможно, это прозвучит как реально странная идея, поэтому мы попробуем сделать ее более понятной далее. 

Требования к предлагаемому решению

Помимо самой идеи глобального версионирования, автор доклада также формулирует несколько обязательных требований к ее практической реализации. Соответственно, описывая предложенные опции по реализации глобального версионирования. Мы каждый раз будем проверять их на выполнение приведенных требований. Естественно, в идеале все требования должны выполняться.   

  • Итак, первое требование состоит в том, что потребители должны иметь возможность использовать один и тот же номер версии для всех эндпоинтов, именно в этом состоит вся идея глобального версионирования. Заметьте, что используется формулировка “иметь возможность”, но при этом подчеркивается, что потребитель не обязан так делать. Вы все еще можете использовать специфические эндпоинты для метода со специфической версией, например, если там есть баг или у вас есть какая-то другая реальная причина. Однако, без достаточно веской причины так делать не рекомендуется, так как это подрывает базовую идею глобального версионирования.

  • Во-вторых, Минимум дополнительного кода. Конечно, добавление новой функциональности потребует несколько лишних строк кода, но важно стремиться к тому, чтобы их было как можно меньше.

  • И в-третьих, добавление любого ресурса, версии или метода должно минимально влиять на остальную кодовую базу, за пределами новой функциональности. Добавление новой функциональности неизбежно влечёт за собой написание нового кода. Однако важно, чтобы это минимально влияло на существующую кодовую базу. В идеале изменения не должны затрагивать другие части системы, но на практике добиться полного отсутствия влияния практически невозможно.

Отправная точка

Для первой практической демонстрации было использовано базовое приложение на Spring Boot, со следующим контроллером:

Примечание: код, используемый в данной статье, можно найти у автора доклада на GitHub.

package io.github.boukenijhuis.dynamicversionurl;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

   @GetMapping("/v1/a")
   public String a1() {
       return "a1";
   }

   @GetMapping("/v1/b")
   public String b1() {
       return "b1";
   }
}

Что мы здесь видим? В приложении присутствуют два эндпоинта, и они оба используют @GetMapping. Есть ресурс a, который находится в версии 1, и он всегда возвращает a1.  Есть ресурс b1, аналогичный a1.

Каждый раз при добавлении эндпоинтов к коду будет меняться эндпоинт a, так что это будет a версии 2, который возвращает a2, затем это будет эндпоинт версии 3, который возвращает a3. Однако, мы используем глобальное версионирование. Поэтому каждый раз, когда что-то добавляется к ресурсу a, номер версии ресурса b тоже должен увеличиваться. Поэтому тут вы тоже увидите эндпоинты версии 2 и 3 для ресурса, но они будут всегда возвращать b1.

Если запустить тесты, проверяющие корректную работу эндпоинтов, то тесты для a1 и b1 пройдут, что логично, а вот тесты для a2 и b2 не пройдут, потому что эти эндпоинты пока не реализованы.

package io.github.boukenijhuis.dynamicversionurl;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(MyController.class)
class MyControllerTest {

   @Autowired
   MockMvc mockMvc;

   @Test
   void a1() throws Exception {
       testEndpoint("/v1/a", "a1");
   }

   @Test
   void b1() throws Exception {
       testEndpoint("/v1/b", "b1");
   }

   @Test
   void a2() throws Exception {
       testEndpoint("/v2/a", "a2");
   }

   @Test
   void b2() throws Exception {
       testEndpoint("/v2/b", "b1");
   }

   private void testEndpoint(String path, String response) throws Exception {
       mockMvc.perform(get(path))
               .andExpect(status().isOk())
               .andExpect(content().string(response))
               .andReturn();
   }
}

Теперь добавим эндпоинт второй версии. 

package io.github.boukenijhuis.dynamicversionurl;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

   @GetMapping("/v1/a")
   public String a1() {
       return "a1";
   }

   @GetMapping("/v2/a")
   public String a2() {
       return "a2";
   }

   @GetMapping("/v1/b")
   public String b1() {
       return "b1";
   }
}

Итак, у нас теперь есть эндпоинт версии v2 для ресурса a. На этом этапе пройдут три теста, и не пройдет только тест для b2, опять таки потому что этот эндпоинт все еще не реализован.

Но мы уже можем проверить, насколько данное решение соответствует предъявляемым требованиям.

  • Единая версия для всех эндпоинтов. Сейчас это условие не выполняется. Запрос к эндпоинту v2 для ресурса b не работает, что мы видели в тестах.

  • Минимум дополнительного кода — Для реализации механизма не потребовалось писать дополнительный код. Мы еще не добавили сам механизм, поэтому этот пункт зеленый.

  • Минимальное влияние на кодовую базу. Добавление нового ресурса, версии или метода не должно сильно влиять на кодовую базу. В данном случае был добавлен метод a2 для реализации эндпоинта, но другие части кода это не затронуло. Следовательно, этот пункт тоже зеленый. 

Требования к решению

Единая версия для всех эндпоинтов.

Источник: https://habr.com/ru/companies/spring_aio/articles/859910/


Интересные статьи

Интересные статьи

Меня зовут Илья Зеленский. Помните басню Крылова "мартышка и очки"? Смысл басни был, как важно владеть знаниями и правильно применять их. Поэтому я хочу поделиться своим опытом запуска действительно с...
Добрый день! Хочу поделиться своим опытом по миграции Корпоративного портала и CRM Битрикс24 с одного физического сервера на 2 виртуальные машины.В связи с нарастающим количеством заявок программистам...
Могут ли российские BI-платформы все-таки заменить зарубежные системы? Мы много раз возвращались к этому вопросу, потому что сегодня именно он интересует и даже беспокоит многих руководителей. И, наве...
Хочу поделиться опытом автоматизации экспорта заказов из Aliexpress в несколько CRM. Приведенные примеры написаны на PHP, но библиотеки для работы с Aliexpress есть и для...
Субботний вечер омрачен скандалом - сайт не работает, провайдер негодяй, админы - не специалисты, а сервера - решето. Вызов принят, или почему при всей нелюбви к 1С-Битри...