Статьи

Управление версиями REST API с помощью Spring Boot и Swagger

Одно точно. Если вам не требуется версия вашего 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 в вашем приложении. Самые популярные из них:

  1. Через  путь URI  — вы включаете номер версии в путь URL конечной точки, например, / api / v1 / people.
  2. Через  параметры запроса  — вы передаете номер версии в качестве параметра запроса с указанным именем, например, / api / Person? Version = 1.
  3. С помощью  пользовательских заголовков HTTP  — вы определяете новый заголовок, который содержит номер версии в запросе.
  4. Посредством  согласования контента  — номер версии включается в заголовок «Принять» вместе с принятым типом контента. Запрос с 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.01.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 для удовлетворения более сложных требований.