Архитектурата на микроуслугите се състои от няколко независими бекенд услуги, които комуникират помежду си чрез 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 от същия тип по-интуитивно.

Имайте предвид, че backend 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 слой може да донесе на вашата бекенд архитектура.

Защо вашето приложение за пролетно стартиране трябва да има 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 Web по подразбиране. По-конкретно, той демонстрира как да направите заявка за 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, направено с този екземпляр, автоматично ще има Authorization HTTP хедъра, зададен на желаната стойност.

Благодарение на API слоя, интегрирането на външни услуги изисква само няколко реда код. Цялата логика е централизирана в един файл и е изложена чрез функции. така че преминаването от една услуга към друга е лесно. Трябва само да актуализирате кода във всяка функция в съответния <ExternalService>API.java файл. Това ви спестява от основен рефакторинг.

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

Нито един базиран на API SaaS не предлага 100% ъптайм. В допълнение, някои от тези външни услуги може да са податливи на грешки, особено по време на актуализации. Това означава, че услугите на трети страни не са непознати за HTTP 5xx грешки от страна на сървъра, особено разнообразието 503 Service Unavailable.

Когато услуга на трета страна отговори с грешка, вашият бекенд може да се провали в резултат на това. Следователно грешките, върнати от всяка външна услуга, използвана от бекенда, също са ваш проблем. В същото време услугите на трети страни базират своя успех на ефективността и наличността на техните функции. Така че тези грешки трябва да са временни. Ето защо повторното изпробване на HTTP заявки в случай на конкретни грешки е толкова важно.

Благодарение на Spring Retry можете без усилие да интегрирате логиката за повторен опит в приложението Spring Boot. Всичко, което трябва да направите, е да украсите методите на API слоя с анотацията Spring Retry @Retryable. След това тези методи ще бъдат изпълнени до три пъти, със закъснение от една секунда между всеки опит.

Добавяне на API слой към Spring Boot App

Вече знаете какво е 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 Layer за изпълнение на 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()method:

// 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 грешка, бекендът ще се опита до десет пъти да изпълни извикването на API. Обърнете внимание, че повторният опит на 4xx HTTP грешки няма смисъл. Това е така, защото грешките 4xx са грешки на клиента и повторният опит би довел до същата грешка отново и отново.

По-подробно, callAPIWithRetryLogic() прави повторен опит на Http5xxExceptions. Това е персонализирано изключение за грешка, което можете да дефинирате по следния начин:

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

Обърнете внимание, че не всички извиквания на API се нуждаят от повтаряща се логика. Поради тази причина трябва да дефинирате две подобни на _54 функции в 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 repo.

Клонирайте Хранилището на 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!

Заключение

В този урок научихте какво представлява backend API слой, основните му предимства в света на микроуслугите и как да го интегрирате в Spring Boot Web приложение. API слоят не е нищо повече от набор от компоненти, които предоставят пълна функционалност за изпращане и получаване на данни чрез API извиквания. Това ви позволява да консолидирате API логиката в един слой на архитектурата на приложението Spring Boot, което прави цялата кодова база по-лесна за поддръжка и по-надеждна, намалявайки възможността за човешка грешка. Както е показано в пълен пример, внедряването на API слой в Spring Boot е просто и отнема само няколко минути.

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