В прошлых частях мы успешно спроектировали и запустили два микросервиса: сервис BookStore и сервис аутентификации/авторизации.
Теперь мы можем расположить каждый из них на отдельном инстансе (например в AWS EC2), но в таком случае они будут выглядеть не как одно целое для конечного потребителя. Далее при развитии архитектуры количество сервисов будет только увеличиваться, и нам понадобится что-то, что свяжет наши сервисы и будет маршрутизировать запросы пользователя на каждый из них. Для этих целей используют шаблон проектирования API Gateway, который позволяет реализовать единую точку входа в нашу систему, и перенаправляет запросы на нужный микросервис.
Существует множество реализаций Api Gateway — например, Kong, Gravitee, Krakend и т.д. Мы же воспользуемся решением для экосистемы Spring Cloud — Spring Cloud Gateway.
Spring Cloud Gateway представляет собой Spring-Boot приложение, которое позволяет настраивать маршрутизацию в yaml-конфиге или в коде приложения, а также расширять логику путём написания своего кода. Реализуем схему, при которой все запросы на эндпоинт /registration
и /login
будут перенаправляться на сервис авторизации, /help
будет вести на сайт spring, а остальные запросы будут маршрутизироваться в BookStore. Запускать всё вместе будем на одном инстансе на разных портах (аналогично реализуется на нескольких инстансах).
Начнём с генерации gradle-проекта на start.spring.io. После добавления компонента Gateway должен получиться следующий build.gradle:
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2021.0.0")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
Код Main-класса:
@SpringBootApplication
public class SpringDemoApigatewayApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDemoApigatewayApplication.class, args);
}
}
На этом всё, остается сконфигурировать маршруты для наших запросов в соответствии со схемой. Как я говорил, конфигурацию можно писать кодом или в yaml-файле, который и будем использовать вместо application.properties
:
server:
port: 80
spring:
cloud:
gateway:
httpclient:
ssl:
useInsecureTrustManager: true
routes:
- id: bookstore
uri: http://localhost:8080
predicates:
- Path=/books/**
В секции routes
указываются правила для обработки входящий запросов. В данном случае правило предписывает все запросы на эндпоинт /books
перенаправлять на http://localhost:8080
. То есть Gateway, получив такой запрос, сделает вызов своим http-клиентом на http://localhost:8080
и полученный ответ вернёт пользователю.
Добавим теперь правила для сервиса авторизации, продолжив список в yaml-конфигурации (обращая внимание на табуляции, так как лишний tab может привести к ошибке, и приложение не запустится):
- id: registration
uri: http://localhost:8081
predicates:
- Path=/registration
filters:
- RewritePath=/registration, /auth
- id: token
uri: http://localhost:8081
predicates:
- Path=/login
filters:
- RewritePath=/login, /auth/token
Здесь у нас появились фильтры — сущности, позволяющие модифицировать запрос. Полное описание фильтров можно найти в документации. Как можно догадаться из названия, RewritePath модифицирует путь запроса. Приходящие запросы на эндпоинты /registration
и /login
будут перенаправлены на сервис авторизации на эндпоинты /auth
и /auth/token
соответственно.
Добавим ещё одно правило, которое будет возвращать 302 Redirect
на страницу с руководствами Spring:
- id: help
uri: https://spring.io/guides
predicates:
- Path=/help
filters:
- RedirectTo=302, https://spring.io/guides
Отправив запрос на /help
, пользователь будет получать 302, и браузер будет редиректить на страницу https://spring.io/guides
.
Запустим все три приложения (на портах 8080, 8081 и 80) и попытаемся сделать запросы через Api Gateway.
Регистрация
curl --location --request POST 'http://localhost/registration' \
--header 'Content-Type: application/json' \
--data-raw '{
"clientId": "admin",
"clientSecret": "password"
}'
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Date: Thu, 06 Jan 2022 10:15:00 GMT
content-length: 10
Registered
Аутентификация
curl --location --request POST 'http://localhost/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"clientId": "admin",
"clientSecret": "password"
}'
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json
Date: Thu, 06 Jan 2022 10:16:20 GMT
{
"token" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJib29rc3RvcmUiLCJzdWIiOiJhZG1pbiIsImlzcyI6ImF1dGgtc2VydmljZSIsImV4cCI6MTY0MTQ2NDQ4MCwiaWF0IjoxNjQxNDY0MTgwfQ.qTVmUmfG4tro_D9ui7iSrBGZBi0Z0ob653kKV2vjjp0"
}
Получение данных
curl --location --request GET 'http://localhost/books' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJib29rc3RvcmUiLCJzdWIiOiJhZG1pbiIsImlzcyI6ImF1dGgtc2VydmljZSIsImV4cCI6MTY0MTQ2NDQ4MCwiaWF0IjoxNjQxNDY0MTgwfQ.qTVmUmfG4tro_D9ui7iSrBGZBi0Z0ob653kKV2vjjp0'
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json
Date: Thu, 06 Jan 2022 10:17:58 GMT
[ {
"id" : 1,
"author" : "Joshua Bloch",
"title" : "Effective Java",
"price" : 54.99
}, {
"id" : 2,
"author" : "Kathy Sierra",
"title" : "Head First Java",
"price" : 12.66
}, {
"id" : 3,
"author" : "Benjamin J. Evans",
"title" : "Java in a Nutshell: A Desktop Quick Reference",
"price" : 28.14
} ]
Переадресация
curl --location --request GET 'http://localhost/help'
HTTP/1.1 302
Location: https://spring.io/guides
content-length: 0
С помощью фильтров можно также добавлять необходимые заголовки в запросы, менять тело запроса и ответа, добавлять дополнительные параметры в запрос и так далее.
Отдельно хочется выделить фильтры CircuitBreaker и RequestRateLimiter, которые используются в высоконагруженных системах. RequestRateLimiter необходим для ограничания количества запросов, при превышении порога которых, пользователю будет возвращаться 429 Too Many Requests
.
CircuitBreaker используется, чтобы при падении одного из сервисов Gateway не продолжал спамить этот сервис, не давая ему корректно подняться. В этом случае Gateway сразу будет возвращать ошибку и перенаправит поток сообщений только тогда, когда сервис успешно поднимется.
Входящие в состав Spring Cloud Gateway стандартные предикаты и фильтры в большинстве случаев покрывают все потребности. Однако, если вам понадобится реализовать что-то более сложное, то придется самостоятельно написать код. Особенностью фреймворка является его реактивность — он построен на основе Spring WebFlux и Project Reactor. Требуется некоторая подготовка и понимание принципов реактивного программирования.
Стоит отметить, что обычно Api Gateway также используется для терминирования https. То есть защищённое соединение устанавливается между пользователем и Gateway, а дальше трафик на внутренние сервисы идёт уже по протоколу http, что избавляет от необходимости работы с сертификатами на сотнях микросервисах.
Код проекта доступен на GitHub.