Viacheslav Shago
Viacheslav Shago
0
Обложка: Пишем Java веб-приложение на современном стеке. С нуля до микросервисной архитектуры. Часть 3

Пишем Java веб-приложение на современном стеке. С нуля до микросервисной архитектуры. Часть 3

В прошлых частях мы успешно спроектировали и запустили два микросервиса: сервис 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.