Одно точно. Если вам не требуется версия вашего API, не пытайтесь сделать это. Однако иногда приходится. Многие из самых популярных сервисов, таких как Twitter, Facebook, Netflix или PayPal, используют свои REST API. Преимущества и недостатки такого подхода очевидны. С одной стороны, вам не нужно беспокоиться о внесении изменений в ваш API, даже если его используют многие внешние клиенты и приложения. Но с другой стороны, вы должны поддерживать разные версии реализации API в вашем коде, что иногда может быть проблематичным.
В этой статье я покажу вам, как наиболее удобно поддерживать несколько версий API REST в вашем приложении. Мы будем основывать эту статью на примере приложения, написанного поверх платформы Spring Boot и раскрывающего документацию API с использованием библиотек Swagger2 и SpringFox.
Spring Boot не предоставляет никаких специальных решений для API-интерфейсов управления версиями. Ситуация отличается для библиотеки SpringFox Swagger2, которая предоставляет механизм группировки с версии 2.8.0, который идеально подходит для создания документации по версионному REST API.
Я уже представил Swagger2 вместе с приложением Spring Boot в одном из моих предыдущих постов. В статье «
Документация API Microservices с Swagger2» вы можете прочитать, как использовать Swagger2 для создания документации API для всех независимых микросервисов и публикации ее в одном месте — на API-шлюзе.
Различные подходы к API-версиям
Существует несколько различных способов обеспечения контроля версий API в вашем приложении. Самые популярные из них:
- Через путь URI — вы включаете номер версии в путь URL конечной точки, например, / api / v1 / people.
- Через параметры запроса — вы передаете номер версии в качестве параметра запроса с указанным именем, например, / api / Person? Version = 1.
- С помощью пользовательских заголовков HTTP — вы определяете новый заголовок, который содержит номер версии в запросе.
- Посредством согласования контента — номер версии включается в заголовок «Принять» вместе с принятым типом контента. Запрос с cURL будет выглядеть следующим образом:
curl -H "Accept: application/vnd.piomin.v1+json" http://localhost:8080/api/persons
Решение о том, какой из этих подходов реализовать в приложении, остается за вами. Мы обсудим преимущества и недостатки каждого подхода, однако это не является основной целью данной статьи. Основная цель — показать вам, как реализовать управление версиями в приложениях Spring Boot, а затем автоматически публиковать документацию API с помощью Swagger2. Пример исходного кода приложения доступен на GitHub . Я реализовал два подхода, описанных выше — в пунктах 1 и 4.
Включение Swagger для Spring Boot
Swagger2 можно включить в приложении Spring Boot, включив библиотеку SpringFox . Фактически это набор библиотек Java, используемый для автоматизации создания машиночитаемых и читаемых пользователем спецификаций для API-интерфейсов JSON, написанных с использованием Spring Framework. Он поддерживает такие форматы, как Swagger, RAML и JSON API. Чтобы включить его для вашего приложения, включите в проект следующие зависимости Maven:
-
io.springfox:springfox-swagger-ui
-
io.springfox:springfox-swagger2
-
io.springfox:springfox-spring-web
Затем вам нужно будет пометить основной класс @EnableSwagger2
и определить Docker
объект. Docket — это основной механизм конфигурации SpringFox для Swagger 2.0. Мы обсудим подробности об этом в следующем разделе вместе с примером для каждого способа управления версиями API.
Пример API
Наш пример API очень прост. Он раскрывает основные методы CRUD для Person
объекта. Есть три версии API , доступной для внешних клиентов: 1.0
, 1.1
и 1.2
. В версии 1.1
я изменил метод обновления Person
сущности. В версии 1.0
он был доступен по /person
пути, а теперь он доступен по /person/{id}
пути. Это единственная разница между версиями 1.0
и 1.1
. Существует также только одно различие в API между версиями 1.1
и 1.2
. Вместо поля birthDate
возвращается age
как целочисленный параметр. Это изменение влияет на все конечные точки, кроме DELETE /person/{id}
. Теперь перейдем к реализации.
Управление версиями с использованием пути URI
Вот полная реализация контроля версий пути URI в Spring @RestController
.
@RestController
@RequestMapping("/person")
public class PersonController {
@Autowired
PersonMapper mapper;
@Autowired
PersonRepository repository;
@PostMapping({"/v1.0", "/v1.1"})
public PersonOld add(@RequestBody PersonOld person) {
return (PersonOld) repository.add(person);
}
@PostMapping("/v1.2")
public PersonCurrent add(@RequestBody PersonCurrent person) {
return mapper.map((PersonOld) repository.add(person));
}
@PutMapping("/v1.0")
@Deprecated
public PersonOld update(@RequestBody PersonOld person) {
return (PersonOld) repository.update(person);
}
@PutMapping("/v1.1/{id}")
public PersonOld update(@PathVariable("id") Long id, @RequestBody PersonOld person) {
return (PersonOld) repository.update(person);
}
@PutMapping("/v1.2/{id}")
public PersonCurrent update(@PathVariable("id") Long id, @RequestBody PersonCurrent person) {
return mapper.map((PersonOld) repository.update(person));
}
@GetMapping({"/v1.0/{id}", "/v1.1/{id}"})
public PersonOld findByIdOld(@PathVariable("id") Long id) {
return (PersonOld) repository.findById(id);
}
@GetMapping("/v1.2/{id}")
public PersonCurrent findById(@PathVariable("id") Long id) {
return mapper.map((PersonOld) repository.findById(id));
}
@DeleteMapping({"/v1.0/{id}", "/v1.1/{id}", "/v1.2/{id}"})
public void delete(@PathVariable("id") Long id) {
repository.delete(id);
}
}
Если вы хотите, чтобы в одной сгенерированной спецификации API были доступны три разные версии, вы должны объявить три Docket
@Beans
— по одной на одну версию. В этом случае для нас будет полезна концепция группы Swagger, которая уже была представлена SpringFox. Причиной введения этой концепции является необходимость поддержки приложений, требующих более одного списка ресурсов Swagger. Обычно вам требуется более одного списка ресурсов, чтобы предоставить разные версии одного и того же API. Мы можем назначить группу каждому Docket, просто вызвав для этого метод groupName DSL. Поскольку разные версии метода API реализованы в одном контроллере, мы должны различать их, объявляя регулярное выражение пути, соответствующее выбранной версии. Все остальные настройки являются стандартными.
@Bean
public Docket swaggerPersonApi10() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("person-api-1.0")
.select()
.apis(RequestHandlerSelectors.basePackage("pl.piomin.services.versioning.controller"))
.paths(regex("/person/v1.0.*"))
.build()
.apiInfo(new ApiInfoBuilder().version("1.0").title("Person API").description("Documentation Person API v1.0").build());
}
@Bean
public Docket swaggerPersonApi11() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("person-api-1.1")
.select()
.apis(RequestHandlerSelectors.basePackage("pl.piomin.services.versioning.controller"))
.paths(regex("/person/v1.1.*"))
.build()
.apiInfo(new ApiInfoBuilder().version("1.1").title("Person API").description("Documentation Person API v1.1").build());
}
@Bean
public Docket swaggerPersonApi12() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("person-api-1.2")
.select()
.apis(RequestHandlerSelectors.basePackage("pl.piomin.services.versioning.controller"))
.paths(regex("/person/v1.2.*"))
.build()
.apiInfo(new ApiInfoBuilder().version("1.2").title("Person API").description("Documentation Person API v1.2").build());
}
Теперь мы можем отобразить Swagger UI для нашего API, просто вызвав URL в пути веб-браузера /swagger-ui.html
. Вы можете переключаться между всеми доступными версиями API, как показано на рисунке ниже.
Спецификация генерируется точной версией API. Вот документация для версии 1.0
. Поскольку метод PUT /person
аннотирован с @Deprecated
этим, он зачеркнут на сгенерированной странице документации HTML.
Если вы переключитесь на группу, person-api-1
вы увидите все методы, которые содержатся v1.1
в пути. Наряду с ними вы можете узнать текущую версию метода PUT с {id}
полем в пути.
При использовании документации, сгенерированной Swagger, вы можете легко вызывать каждый метод после его расширения. Вот пример вызова метода PUT /person/{id}
из того, что мы реализовали для версии 1.2.
Управление версиями с использованием заголовка Accept
Чтобы получить доступ к реализации контроля версий с заголовком «Accept», вам следует переключиться на заголовок ветви . Вот полная реализация согласования контента с использованием версии заголовка «Accept» внутри Spring @RestController
.
@RestController
@RequestMapping("/person")
public class PersonController {
@Autowired
PersonMapper mapper;
@Autowired
PersonRepository repository;
@PostMapping(produces = {"application/vnd.piomin.app-v1.0+json", "application/vnd.piomin.app-v1.1+json"})
public PersonOld add(@RequestBody PersonOld person) {
return (PersonOld) repository.add(person);
}
@PostMapping(produces = "application/vnd.piomin.app-v1.2+json")
public PersonCurrent add(@RequestBody PersonCurrent person) {
return mapper.map((PersonOld) repository.add(person));
}
@PutMapping(produces = "application/vnd.piomin.app-v1.0+json")
@Deprecated
public PersonOld update(@RequestBody PersonOld person) {
return (PersonOld) repository.update(person);
}
@PutMapping(value = "/{id}", produces = "application/vnd.piomin.app-v1.1+json")
public PersonOld update(@PathVariable("id") Long id, @RequestBody PersonOld person) {
return (PersonOld) repository.update(person);
}
@PutMapping(value = "/{id}", produces = "application/vnd.piomin.app-v1.2+json")
public PersonCurrent update(@PathVariable("id") Long id, @RequestBody PersonCurrent person) {
return mapper.map((PersonOld) repository.update(person));
}
@GetMapping(name = "findByIdOld", value = "/{idOld}", produces = {"application/vnd.piomin.app-v1.0+json", "application/vnd.piomin.app-v1.1+json"})
@Deprecated
public PersonOld findByIdOld(@PathVariable("idOld") Long id) {
return (PersonOld) repository.findById(id);
}
@GetMapping(name = "findById", value = "/{id}", produces = "application/vnd.piomin.app-v1.2+json")
public PersonCurrent findById(@PathVariable("id") Long id) {
return mapper.map((PersonOld) repository.findById(id));
}
@DeleteMapping(value = "/{id}", produces = {"application/vnd.piomin.app-v1.0+json", "application/vnd.piomin.app-v1.1+json", "application/vnd.piomin.app-v1.2+json"})
public void delete(@PathVariable("id") Long id) {
repository.delete(id);
}
}
Нам еще предстоит определить три Docker
@Beans
, но критерии фильтрации немного отличаются. Простая фильтрация по пути здесь не вариант. Мы должны создать Predicate
для RequestHandler
объекта и передать его в apis
метод DSL. Реализация предиката должна фильтровать каждый метод, чтобы найти только те, которые имеют produces
поле с требуемым номером версии. Вот пример Docket
реализации для версии 1.2
.
@Bean
public Docket swaggerPersonApi12() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("person-api-1.2")
.select()
.apis(p -> {
if (p.produces() != null) {
for (MediaType mt : p.produces()) {
if (mt.toString().equals("application/vnd.piomin.app-v1.2+json")) {
return true;
}
}
}
return false;
})
.build()
.produces(Collections.singleton("application/vnd.piomin.app-v1.2+json"))
.apiInfo(new ApiInfoBuilder().version("1.2").title("Person API").description("Documentation Person API v1.2").build());
}
Как видно на рисунке ниже, сгенерированные методы не имеют номера версии в пути.
При вызове метода для выбранной версии API единственное отличие заключается в требуемом типе содержимого ответа.
Резюме
Управление версиями — одна из важнейших концепций проектирования HTTP API. Независимо от того, какой подход к выбору версий вы выберете, вы должны делать все возможное, чтобы хорошо описать свой API. Это кажется особенно важным в эпоху микросервисов, когда ваш интерфейс может вызываться многими другими независимыми приложениями. В этом случае создание документации отдельно от исходного кода может быть проблематичным. Swagger решает все описанные проблемы. Он может быть легко интегрирован с вашим приложением и поддерживает управление версиями. Благодаря проекту SpringFox его также можно легко настроить в приложении Spring Boot для удовлетворения более сложных требований.