Команда Spring АйО подготовила перевод статьи о том, как Spring Framework 7 приносит нативную поддержку API-версионирования — темы, которая годами оставалась на разработчиках и собирала тонны костылей. Теперь Spring Framework предлагает единый, продуманный механизм как для серверной, так и для клиентской стороны. Что это меняет для архитектуры и разработки на Spring — разберёмся в статье.


Введение

Версионирование API — тема непростая. Большинство статей приводит разные способы реализации, но почти не даёт рекомендаций. Там, где советы всё же есть, они расходятся кардинально. Например, Рой Филдинг выступает против версионирования. При этом оно широко распространено, но стандарта или единого подхода — нет.

Комментарий от Михаила Поливахи – Эксперта сообщества Spring АйО

Для тех, кто не в курсе – Roy Fielding является автором архитектурного подхода REST и hypermedia как явления.

Отсуствие версионирования поясняется именно тем, что клиент не должен знать, какую именно версию API он использует - всё разрулит hypermedia.

На деле у hypermedia и HATEOAS, по моим эмперическим данным, адопшен довольно слабый по ряду причин. Это отдельная, очень большая тема. В неё мы углубляться не будем. Если будет нужно - напишем отдельную статью.

Кроме того, разные приложения имеют разные требования, которые зависят от предметной области, динамики компании, текущего этапа развития и многого другого. Поэтому всегда приходится учитывать, насколько подходы из статей подходят вашей конкретной ситуации.

И всё это в сочетании с тем, что разные варианты передачи версий (путь, header, media type и т. д.) достаточно легко выразить через аннотации @RequestMapping, привело к тому, что в Spring долгое время не было официальной поддержки API Versioning. До сих пор. Что же изменилось?

Как это часто бывает, новая возможность начинается с пользовательского запроса — в данном случае с замечания, не смотря на то, что реализовать API Versioning в Spring-приложении и возможно, это на самом деле требует значительных усилий. Просмотрев подпункты в umbrella-issue, мы увидели, что это действительно так. Масса отзывов и интерес к теме после открытия issue только подтвердили, что необходимость — реальная.

Цель этого поста — не обсуждать практики API Versioning. Для более подробного введения можно посмотреть мой доклад на конференции Spring I/O в этом году.

В целом задача этой возможности — предоставить строительные блоки для очень распространённой практики и распространённой потребности.

Обработка на стороне сервера

В основе серверной поддержки лежит ApiVersionStrategy — ключевой контракт, содержащий сведения о всех настройках приложения для API Versioning. Он умеет извлекать, разбирать и проверять версии в запросе, знает диапазон поддерживаемых версий и помогает отправлять подсказки о депрекейте в ответе.

Его можно настроить через MVC-конфигурацию или WebFlux-конфигурацию. Например:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

	@Override
	public void configureApiVersioning(ApiVersionConfigurer configurer) {
		configurer.useRequestHeader("API-Version");
	}
}

Для приложений Spring Boot есть аналогичные параметры. Например:

spring.mvc.apiversion.use.header=API-Version

После настройки эта конфигурация становится доступна для поддержки API Versioning при обработке запросов.

Для аннотированных контроллеров вы можете использовать новый атрибут version в аннотации @RequestMapping и её специализированных формах, например @GetMapping:

@RestController
public class AccountController {

	@GetMapping(path = "/account/{id}", version = "1.1") 
	public Account getAccount() {
	}
}

Для функциональных эндпоинтов можно использовать предикат version:

RouterFunction<ServerResponse> route = RouterFunctions.route()
	.GET("/hello-world", version("1.2"),
		request -> ServerResponse.ok().body("Hello World")).build();

Если версия API находится в header, query-параметре или media type, то в маппингах больше ничего делать не нужно.

Если версия API находится в пути, она должна быть объявлена как переменная URI (с любым именем), например "/api/{version}". Обычно такой префикс лучше вынести в общую конфигурацию Path Matching, чтобы не повторять его в каждом обработчике.

По умолчанию версия запроса парсится как семантическая версия — с major, minor и patch, где minor и patch равны 0, если отсутствуют. Это важно, чтобы можно было сравнивать версии. Парсер можно настроить или заменить, если вы хотите использовать дату или другой формат.

В маппингах версия может быть фиксированной, например "1.2", или базовой, например "1.2+". Базовая версия полезна в сценариях инкрементальных изменений, когда новые версии затрагивают лишь отдельные endpoints — тогда нет необходимости создавать дополнительные методы контроллера для тех endpoints, которые не изменились.

Например, при поддерживаемых версиях "1.2" и "1.3" метод контроллера с версией "1.2+" поддерживает обе. Это позволяет методу продолжать работать и в версии 1.3, и в будущих версиях — пока не появится необходимость в новом методе для более высокой версии.

Важная часть версионирования — отправлять клиентам подсказки о депрекейте. Можно настроить обработчик депрекейтов. Встроенный обработчик умеет выставлять заголовки "Deprecation", "Sunset" и "Link" согласно RFC 9745 и RFC 8594.

Поддержка на стороне клиента

Со стороны клиента может понадобиться выполнять запросы с версией API. Для этого есть ApiVersionInserter, который определяет, как один раз вставлять версию в запрос. После его настройки при выполнении запросов нужно указывать только значение версии.

Например, в RestClient и WebClient можно настроить inserter:

RestClient client = RestClient.builder()
		.baseUrl("http://localhost:8080")
		.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"))
		.build();

Для Spring Boot есть аналогичный параметр:

spring.http.client.restclient.apiversion.insert.header=API-Version

И затем выполнять запросы так:

Account account = client.get().uri("/accounts/1")
		.apiVersion(1.1)
		.retrieve()
		.body(Account.class);

HTTP-интерфейсы также поддерживают API Versioning через новый атрибут version у аннотации @HttpExchange и её специализированных форм, например @GetExchange:

@HttpExchange("/accounts")
public interface AccountService {

	@GetExchange(url = "/{id}", version = "1.1")
	Account getAccount(@PathVariable int id);

}

Тестирование

Как и ожидалось, API Versioning важно и для тестирования — например, с RestTestClient (новый в Spring Framework 7.0) и WebTestClient.

Оба — полноценные клиенты, аналогичные RestClient и WebClient, поэтому им также нужно настроить ApiVersionInserter, чтобы корректно формировать запросы. Они также конфигурируют серверную часть, но это зависит от способа поднятия приложения:

  • Standalone — нужно вручную предоставить ApiVersionStrategy.

  • ApplicationContext — контекст уже содержит настройки API Versioning, ничего делать не нужно.

  • Live server — сервер уже запущен и настроен независимо от тестового клиента.

Поддержка API Versioning есть и при тестировании напрямую через MockMvc, без тестового клиента. Можно настроить ApiVersionInserter для инициализации MockHttpServletRequest, а также ApiVersionStrategy для standalone-настройки.

Если хотите увидеть примеры в сценариях Spring Boot-тестов, посмотрите тесты в этом демонстрационном проекте.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Итоги

В этом посте мы кратко прошлись по поддержке API Versioning в Spring Framework 7.

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

Если у вас есть требования по API Versioning, изучите эту возможность и дайте нам знать — здесь или через наш ишью трекер.

Комментарии (0)