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

Выполнение HTTP-запроса для вызова API занимает всего несколько строк кода. Однако вам, вероятно, придется вызывать один и тот же API в разных местах. Это подразумевает повторение одной и той же логики снова и снова. Такой подход громоздок и приводит к дублированию кода. Просто подумайте, когда изменится конечная точка API. В этом распространенном сценарии вам придется обновлять код во всех точках, где вызывается API.

Теперь представьте, что в архитектуре вашего внутреннего программного обеспечения есть слой, содержащий все необходимое для вызова всех API. Централизация всего кода для выполнения вызовов API в одном месте даст несколько преимуществ и поможет смягчить негативные аспекты, упомянутые ранее. Вот что такое уровень API!

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

Что такое уровень API?

Уровень API — это часть серверного приложения, которая инкапсулирует всю логику программирования, необходимую для получения и отправки данных через интерфейс (API). Другими словами, все внешние API-вызовы серверного приложения зависят от прохождения через этот архитектурный уровень. Имейте в виду, что в этом нет ничего нового, и вы можете воспользоваться преимуществами уровня API также во внешнем приложении. Узнайте больше о том, как создать слой API в React.

В приложении Spring Boot вся логика, необходимая для реализации слоя API, должна быть помещена в пакет api. Затем вы можете использовать следующий формат для именования файлов классов Java, содержащихся в этом пакете:

<ExternalService>API.java

Этот файл Java должен отображать все вызовы API, связанные с конкретной внешней микрослужбой, в виде функций. Таким образом, пакет API будет состоять из нескольких классов. В каждом из этих файлов будут собраны все API, связанные со службой, указанной с помощью квалификатора <ExternalService>. Это также сделает поиск API одного типа более интуитивным.

Имейте в виду, что серверный уровень API обычно также требует некоторых служебных классов. Эти файлы не соответствуют представленному выше соглашению об именах. Итак, вот как может выглядеть слой API в приложении Spring Boot:

api
├── utils
│    └── APIHandler.java    
│
├── APILayer.java
.
.
.
├── AuthorizationAPI.java
.
.
.
├── CmsAPI.java
.
.
.
└── SemaphoreAPI.java

Как вы можете видеть на файловой диаграмме выше, уровень API объединяет все API внешних сервисов, которые использует ваше серверное приложение.

В частности, давайте сосредоточимся на файле APILayer.java. Это специальный файл, который инкапсулирует все файлы <ExternalService>API.java следующим образом:

package com.apilayer.service.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class APILayer {
    private final AuthorizationAPI authorizationAPI;
    private final CmsAPI cmsAPI;
    private final SemaphoreAPI semaphoreAPI;
    // other <ExternalService>API ...
@Component
    APILayer(
        @Autowired AuthorizationAPI authorizationAPI,
        @Autowired CmsAPI cmsAPI,
        @Autowired SemaphoreAPI semaphoreAPI
        // ...
    ) {
        this.authorizationAPI = authorizationAPI;
        this.cmsAPI = authorizationAPI;
        this.semaphoreAPI = semaphoreAPI;
        // ...
    }
    public AuthorizationAPI getAuthorizationAPI() {
        return authorizationAPI;
    }
    public SemaphoreAPI getSemaphoreAPI() {
        return semaphoreAPI;
    }
    public CmsAPI getCmsAPI() {
        return cmsAPI;
    }
    // other getters...
}

APILayer имеет аннотацию @Component, потому что обычно содержится на сервисном уровне. Причина этого в том, что получение и запись данных через API является частью бизнес-логики, которая должна быть определена на сервисном уровне.

Итак, если ваш бэкэнд Spring Boot следует многоуровневой архитектуре с пакетом для каждого уровня, пакет api должен быть помещен внутри уровня service:

com.your-app
├── controller
│    └── ...    
├── dao
│    └── ...
├── data
│    └── ...
├── dto
│    └── ...
├── model
│    └── ...
.
.
.
└── service
     ├── api
     │    ├── utils
     │    └── ...
     └── ...

Обратите внимание на папку api в каталоге service. Уровень API можно рассматривать как специализированный подуровень сервисного уровня.

Благодаря APILayer для импорта всего слоя API требуется только одна переменная @Autowired. В частности, теперь вы можете вызывать API в классе @Service следующим образом:

package com.apilayer.service;
import com.apilayer.service.api.APILayer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
// ...
@Service
public class ArticleService {
    private final APILayer apiLayer;
  
    // registering the APILayer bean
    // via constructor dependency injection
    public ArticleService(@Autowired APILayer apiLayer) {
        this.apiLayer = apiLayer;
    }
    public List<Article> getAllArticles(
    )  {
        // retrieving some data from an external CMS 
        // through an API call
        List<Article> articles = apiLayer.getCmsAPI().getAllArticles();
       // remaining business logic...
      
        return articles;
    }
  
    // ...
}

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

Давайте теперь углубимся в преимущества, которые уровень API может принести вашей серверной архитектуре.

Почему ваше приложение Spring Boot должно иметь слой API

Слой API предлагает несколько преимуществ для архитектуры приложения Spring Boot. Давайте рассмотрим три наиболее важные причины, по которым вам следует его использовать.

1. Избегайте дублирования кода

В архитектуре микросервисов серверные приложения используют API для связи друг с другом. Таким образом, ваш бэкенд, скорее всего, будет делать много запросов к API. В приложении Spring Boot с традиционной архитектурой это означает повторение строк кода, показанных ниже, во всей базе исходного кода:

// verifying if an authorization token
// is valid by calling an API
// exposed by the authentication microservice
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.AUTHORIZATION, token);
boolean isValid = return restTemplate.exchange(
       "https://your-auth-backend.com/api/v1/verifyToken", 
        HttpMethod.GET, 
        new HttpEntity<>(null, httpHeaders), 
        Boolean.class
);

В приведенном выше примере показано, как вызывать API в Spring Boot с помощью RestTemplate, веб-клиента HTTP Spring Boot по умолчанию. В частности, он демонстрирует, как сделать запрос API аутентификации. Поскольку все аутентифицированные API в вашем бэкенде будут полагаться на этот внешний вызов API, вам придется повторять эту логику несколько раз. Это приведет к дублированию, что увеличит количество строк кода в кодовой базе и, следовательно, возможность внесения ошибок и багов. В частности, даже один неверный вызов API может привести к сбою приложения, а этого следует избегать. Кроме того, количество API-интерфейсов, от которых зависит серверная часть, обычно очень ограничено. Таким образом, дублирование некоторой логики для вызова одних и тех же API кажется неизбежным при таком подходе.

Теперь рассмотрите возможность централизации всей логики ваших вызовов API в функциях, предоставляемых уровнем API. Выполнение вызова API потребует одной строки кода — вызова функции. Кроме того, при изменении конечной точки API вам потребуется обновить только одну функцию. Без слоя API вам пришлось бы находить и изменять каждый вызов API, сделанный в коде. Огромная разница!

Таким образом, уровень API делает серверную часть Spring Boot более современной, простой в обслуживании и чистой, без повторения одних и тех же строк кода снова и снова.

2. Упрощение интеграции сторонних сервисов

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

Вы можете применить ту же логику к серверному приложению, особенно учитывая, что большинство услуг SaaS (программное обеспечение как услуга) теперь основаны на API. Это означает, что вы можете получить доступ к их данным и функциям через API.

С уровнем API у вас уже есть инфраструктура для обработки вызовов API. В частности, интеграция новой сторонней службы в ваш бэкэнд просто включает определение нового файла <ExternalService>API.java. Чтобы сделать все еще проще, вы также можете настроить собственный экземпляр RestTemplate следующим образом:

// building a RestTemplate instance
// with custom default headers
RestTemplate semaphoreRestTemplate = new RestTemplateBuilder()
            .defaultHeader("Authorization", "Token: <YOUR_SEMPAHORE_API_KEY>")
            .build();

Например, вы можете использовать этот экземпляр RestTemplate в вашем файле SemaphoreAPI.java. Каждый вызов API, сделанный с этим экземпляром, автоматически будет иметь HTTP-заголовок Authorization, установленный на желаемое значение.

Благодаря уровню API для интеграции внешних сервисов требуется всего несколько строк кода. Вся логика централизована в одном файле и представлена ​​через функции. так что переключаться с одного сервиса на другой просто. Вам нужно только обновить код внутри каждой функции в соответствующем файле <ExternalService>API.java. Это избавит вас от серьезного рефакторинга.

3. Упрощение добавления логики повторных попыток при сбое API

Ни один SaaS на основе API не обеспечивает 100% безотказной работы. Кроме того, некоторые из этих внешних служб могут быть подвержены ошибкам, особенно во время обновлений. Это означает, что сторонние сервисы не чужды ошибкам HTTP 5xx на стороне сервера, особенно разновидности 503 Service Unavailable.

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

Благодаря Spring Retry вы можете легко интегрировать логику повторных попыток в свое приложение Spring Boot. Все, что вам нужно сделать, это украсить методы уровня API аннотацией Spring Retry @Retryable. Затем эти методы будут выполняться до трех раз с задержкой в ​​одну секунду между каждой попыткой.

Добавление уровня API в приложение Spring Boot

Теперь вы знаете, что такое уровень API и какие преимущества он может принести приложению Spring Boot. Пришло время научиться его реализовывать!

Предпосылки

Во-первых, вам нужно веб-приложение Spring Boot на Java. Вы можете создать его с помощью Spring Initializr следующим образом:

Вы можете выбрать Gradle или Maven в качестве инструмента сборки, но вы должны выбрать Java в качестве языка программирования и добавить Spring Web в качестве зависимости. Таким образом, Spring Initializer автоматически добавит spring-boot-starter-web к зависимостям вашего проекта. Обратите внимание, что Spring Web включает RestTemplate– HTTP-клиент, используемый уровнем Spring Boot API для выполнения HTTP-запросов.

Реализация уровня API

Давайте посмотрим, как вы можете добавить уровень API в веб-приложение Spring Boot. Первое, что вам нужно сделать, это создать служебный класс, чтобы упростить вызов API.

В пакете utils вы можете определить класс APIHandler.java, как показано ниже:

package com.apilayer.service.api.utils;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
@Component
public class APIHandler {
    private final RestTemplate restTemplate;
    public APIHandler(RestTemplateBuilder restTemplateBuilder) {
        // initializing a RestTemplate instance
        // to perform API calls
        this.restTemplate = restTemplateBuilder.build();
    }
    // defining a utility function
    // to perform API calls
    public <T, R> ResponseEntity<R> callAPI(
            String apiEndpoint,
            HttpMethod httpMethod,
            HttpHeaders httpHeaders,
            T requestBody,
            Class<R> classToConvertBodyTo
    ) {
        // setting the HTTP headers and HTTP body (if present)
        HttpEntity<T> requestEntity = new HttpEntity<>(requestBody, httpHeaders);
        try {
            // performing the HTTP request
            return restTemplate.exchange(
                    apiEndpoint,
                    httpMethod,
                    requestEntity,
                    classToConvertBodyTo
            );
        } catch (RestClientException e) {
            // logging errors when the HTTP request fails
            // or there is an error in decoding the response
            e.printStackTrace();
            // other default behavior...
            // (e.g. register the error in an APM service, ...)
            throw e;
        }
    }
}

Как видите, этот класс инициализирует экземпляр RestTemplate в конструкторе с помощью RestTemplateBuilder. Затем он использует этот экземпляр в callAPI() для отправки HTTP-запросов через метод RestTemplate exchange().

Преимущество централизации логики вызовов API в служебном классе APIHandler заключается в том, что вы можете определить собственное поведение для всех ваших вызовов прямо здесь. Например, вы можете регистрировать ошибки в callAPI(), когда метод exchange() вызывает ошибку RestClientException. Это особенно полезно для целей отладки.

Точно так же вы можете добавить пользовательские глобальные конфигурации в экземпляр RestTemplate при его инициализации в конструкторе. Например, вы можете добавить собственный HTTP-заголовок к запросу, выполненному с этим экземпляром RestTemplate, используя метод defaultHeader():

// add a custom HTTP header to the RestTemplate instance
RestTemplate scrapingRestTemplate = new RestTemplateBuilder()
            .defaultHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36")
            .build();

Поэтому, поскольку у разных сторонних сервисов обычно разные требования к заголовкам или процедурам для вызова их API, вам может потребоваться создать множество файлов APIHandlers. У каждого будут специальные служебные функции, которые зависят от специально настроенного экземпляра RestTemplate instance.

Теперь давайте посмотрим, как использовать APIHandler для создания файла слоя <ExternalService>API.java API:

package com.apilayer.service.api;
import com.apilayer.data.pokeapi.NamedApiResourceList;
import com.apilayer.service.api.utils.APIHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import com.apilayer.data.User;
@Component
public class AuthorizationAPI {
    private final APIHandler apiHandler;
    private final String BASE_URL = "https://your-auth-backend.com/api/v1/users";
    AuthorizationAPI(@Autowired APIHandler apiHandler) {
        this.apiHandler = apiHandler;
    }
    public User getUserDataByToken(String token) {
        // retrieving a single User by token
        ResponseEntity<User> response = apiHandler.callAPI(
                String.format(BASE_URL + "?token=%s", token),
                HttpMethod.GET,
                null,
                null,
                User.class
        );
        
        return response.getBody();
    }
    public bool isAuthenticated(String token) {
        // inserting the token string in the
        // HTTP "Authorization" header
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(HttpHeaders.AUTHORIZATION, token);
        // verifying whether a user is authenticated 
        // by checking his authorization token 
        ResponseEntity<Boolean> response = apiHandler.callAPI(
                BASE_URL,
                HttpMethod.GET,
                httpHeaders,
                null,
                Boolean.class
        );
        
        return response.getBody();
    }
    // other authorization API functions...
}

Ваш бэкенд может полагаться на другие API из вашего бэкенда аутентификации. Каждый из этих API будет соответствовать функции уровня API, которая содержит всю логику для вызова API, инкапсулированную в несколько строк кода. Не забывайте, что это всего лишь один файл <ExternalService>API.java, и вашему приложению Spring Boot может потребоваться гораздо больше файлов уровня API.

Интеграция логики повтора

Как объяснялось ранее, теперь вам нужен Spring Retry. Поскольку для аннотаций Spring Retry требуется зависимость Spring AOP, вам необходимо установить два пакета. Давайте добавим spring-retry и spring-boot-starter-aop в зависимости вашего проекта.

Если вы являетесь пользователем Maven, добавьте следующие строки в тег <dependencies> вашего файла pom.xml:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.0</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>3.0.2</version>
</dependency>

В противном случае, если вы являетесь пользователем Gradle, добавьте две строки ниже в dependenciesobject:

implementation "org.springframework.retry:spring-retry:2.0.0"
implementation "org.springframework.boot:spring-boot-starter-aop:3.0.2"

Включите Spring Retry, украсив свой класс @SpringBootApplication или один из ваших классов @Configuration аннотацией @EnableRetry:

You can now use @Retryable to implement retry logic in the APIHandler callAPI() method as shown below:
package com.apilayer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableRetry
public class DemoApplication {
   public static void main(String[] args) {
      SpringApplication.run(DemoApplication.class, args);
   }
}

Теперь вы можете использовать @Retryable для реализации логики повтора в методе APIHandlercallAPI(), как показано ниже:

@Retryable(
        // attempting up to 10 times
        maxAttempts = 10,
        // with a delay of 2 seconds between attempts
        backoff = @Backoff(delay = 2000),
        // when the Http5xxException is raised by the method
        retryFor = Http5xxException.class
)
public <T, R> ResponseEntity<R> callAPIWithRetryLogic(
        String apiEndpoint,
        HttpMethod httpMethod,
        HttpHeaders headers,
        T body,
        Class<R> classToConvertBodyTo
) {
    // setting the HTTP headers and HTTP body (if present)
    HttpEntity<T> requestEntity = new HttpEntity<>(body, headers);
    try {
        // trying to perform the request
        return restTemplate.exchange(
                    apiEndpoint,
                    httpMethod,
                    requestEntity,
                    classToConvertBodyTo
        );
    } catch (RestClientResponseException e) {
        // in case of a 5xx error, throwing a custom
        // exception that triggers the retry logic
        if (e.getStatusCode().is5xxServerError()) {
            throw new Http5xxException(e.getMessage());
        }
       
        // some default behavior...
        e.printStackTrace();
        
        throw e;
    } catch (RestClientException e) {
        // logging errors when the HTTP request fails
        // or there is an error in decoding the response
        e.printStackTrace();

        // other default behavior...
        // (e.g. register the error in an APM service, ...)
        throw e;
    }
}

Теперь, когда сервер возвращает 5xx HTTP error, бэкэнд будет пытаться выполнить вызов API до десяти раз. Обратите внимание, что повторная попытка 4xx HTTP-ошибок не имеет смысла. Это связано с тем, что ошибки 4xx являются ошибками клиента, и повторная попытка приведет к одной и той же ошибке снова и снова.

В частности, callAPIWithRetryLogic() повторяет попытку Http5xxExceptions. Это пользовательское исключение ошибки, которое можно определить следующим образом:

package com.apilayer.exceptions;
public class Http5xxException extends RuntimeException {
    public Http5xxException(String message) {
        super(message);
    }
}

Обратите внимание, что не все вызовы API нуждаются в логике повторения. По этой причине вы должны определить две callAPI()-подобные функции в APIHandler и аннотировать только одну с помощью @Retryable. Вы можете назвать эти две функции callAPIOnce() и callAPIWithRetry(). Чтобы сделать APIHandler более элегантным и избежать повторения кода, вы должны определить приватную функцию callAPI(), содержащую логику для вызова API, и заставить две функции использовать ее, как это сделано в демо-кодовой базе.

Кроме того, вы должны определить две callAPI()-подобные функции в APIHandler и аннотировать только одну с помощью @Retryable.

Отличная работа! Вы только что узнали, как реализовать уровень API в Spring Boot с помощью RestTemplate и Spring Retry!

Слой API в действии

Давайте теперь посмотрим на уровень API в действии в демонстрационном приложении Spring Boot. В качестве стороннего демонстрационного сервиса приложение использует бесплатный сервис PokeAPI. Кроме того, он основан на моделях Java API из репозитория pokeapi-reactor.

Клонируйте репозиторий GitHub, поддерживающий эту статью и запустите демонстрационное приложение локально, выполнив следующие команды:

git clone https://github.com/Tonel/api-layer-spring-boot
cd api-layer-spring-boot
./gradlew build
./gradlew bootRun

Теперь вы можете вызвать конечную точку /v1/getPokemons с помощью команды curl ниже:

curl -H "Accept: application/json" -H "Content-Type: application/json" -X GET "http://localhost:8080/api/v1/pokemon/getBlastoise"

Вы также можете посетить http://localhost:8080/api/v1/pokemon/getBlastoise в своем браузере.

В обоих случаях вы должны получить следующий результат:

{
  "id": 9,
  "name": "blastoise",
  "image": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/9.png",
  "height": "160 cm",
  "weight": "85 kg"
}

Это не что иное, как преобразованная версия некоторых данных, полученных из вызова API PokeAPI!

Заключение

Из этого руководства вы узнали, что такое серверный уровень API, его основные преимущества в мире микросервисов и способы его интеграции в веб-приложение Spring Boot. Слой API — это не что иное, как набор компонентов, обеспечивающих полную функциональность для отправки и получения данных через вызовы API. Это позволяет консолидировать логику API в один слой архитектуры приложения Spring Boot, делая всю кодовую базу проще в обслуживании и надежнее, уменьшая вероятность человеческой ошибки. Как показано в полном примере, реализация слоя API в Spring Boot проста и занимает всего несколько минут.

Первоначально опубликовано на https://semaphoreci.com 1 марта 2023 г.