В моем блоге много статей о микросервисах с Spring Boot и Spring Cloud ( https://piotrminkowski.wordpress.com/?s=microservices ). Основная цель этой статьи — предоставить краткое описание наиболее важных компонентов, предоставляемых этими платформами, которые помогут вам в создании микросервисов. Темы, рассматриваемые в этой статье:
- Использование Spring Boot 2.0 в облачной разработке
- Обеспечение обнаружения сервисов для всех микросервисов с Spring Cloud Netflix Eureka
- Распределенная конфигурация с Spring Cloud Config
- Шаблон API Gateway с использованием нового проекта в Spring Cloud: Spring Cloud Gateway
- Корреляция журналов с помощью Spring Cloud Sleuth
Прежде чем мы перейдем к исходному коду, давайте взглянем на следующую диаграмму. Это иллюстрирует архитектуру нашей системы образцов. У нас есть три независимых микросервиса, которые регистрируют себя в сервисе обнаружения, выбирают свойства из сервиса конфигурации и общаются друг с другом. Вся система скрыта за шлюзом API.
В настоящее время самая новая версия Spring Cloud Finchley.M9
. Эта версия spring-cloud-dependencies
должна быть объявлена как спецификация для управления зависимостями.
<?xml version="1.0" encoding="UTF-8"?>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.M9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Теперь давайте рассмотрим дальнейшие шаги, которые необходимо предпринять для создания работающей системы на основе микросервисов с использованием Spring Cloud. Мы начнем с сервера конфигурации .
Исходный код примеров приложений, представленных в этой статье, доступен в этом репозитории GitHub .
Шаг 1. Создание сервера конфигурации с помощью Spring Cloud Config
Чтобы включить функцию Spring Cloud Config для приложения, сначала включите spring-cloud-config-server
в свой проект зависимости.
<?xml version="1.0" encoding="UTF-8"?>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
Затем включите запуск встроенного сервера конфигурации во время загрузки приложения с помощью @EnableConfigServer
аннотации.
@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigApplication.class).run(args);
}
}
По умолчанию Spring Cloud Config Server хранит данные конфигурации в репозитории Git. Это очень хороший выбор в рабочем режиме, но для примера серверной файловой системы этого будет достаточно. Это действительно легко начать с сервера конфигурации, потому что мы можем поместить все свойства в classpath. Spring Cloud Config по умолчанию поиск источников собственности внутри следующих местах: classpath:/, classpath:/config, file:./, file:./config
.
Мы размещаем все источники собственности внутри src/main/resources/config
. Имя файла YAML будет таким же, как имя службы. Например, файл YAML для открытия-службы будет располагаться здесь: src/main/resources/config/discovery-service.yml
.
Две последние важные вещи. Если вы хотите запустить сервер конфигурации с бэкэндом файловой системы, вам нужно активировать собственный профиль Spring Boot. Это может быть достигнуто установкой параметра --spring.profiles.active=native
во время загрузки приложения. Я также изменил порт сервера конфигурации по умолчанию (8888) на 8061 , установив свойство server.port
в bootstrap.yml
файле.
Шаг 2. Создание службы обнаружения с помощью Spring Cloud Netflix Eureka
Более подробно о настройке сервера. Теперь всем другим приложениям, включая discovery-service, необходимо добавить spring-cloud-starter-config
зависимость, чтобы включить клиент конфигурации. Мы также должны добавить зависимость spring-cloud-starter-netflix-eureka-server
.
<?xml version="1.0" encoding="UTF-8"?>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
Затем вы должны включить запуск встроенного сервера обнаружения во время загрузки приложения, установив @EnableEurekaServer
аннотацию для основного класса.
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(DiscoveryApplication.class).run(args);
}
}
Приложение должно получить источник свойства с сервера конфигурации. Минимальная конфигурация, необходимая на стороне клиента, — это имя приложения и параметры подключения сервера конфигурации.
spring:
application:
name: discovery-service
cloud:
config:
uri: http://localhost:8088
Как я уже упоминал, файл конфигурации discovery-service.yml
должен быть размещен внутри config-service
модуля. Тем не менее, я должен сказать несколько слов о конфигурации, видимой ниже. Мы изменили рабочий порт Eureka со значения по умолчанию (8761) на 8061 . Для автономного экземпляра Eureka мы должны отключить регистрацию и загрузку реестра.
server:
port: 8061
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
Теперь, когда вы запускаете приложение со встроенным сервером Eureka, вы должны увидеть следующие журналы.
После успешного запуска приложения вы можете посетить панель инструментов Eureka, доступную по адресу http: // localhost: 8061 / .
Шаг 3. Построение микросервиса с использованием Spring Boot и Spring Cloud
Наш микросервис должен выполнить некоторые операции во время загрузки. Необходимо получить конфигурацию config-service
, зарегистрироваться в discovery-service, предоставить HTTP API и автоматически сгенерировать документацию API. Чтобы включить все эти механизмы, нам нужно включить некоторые зависимости в pom.xml
. Чтобы включить клиент конфигурации, мы должны включить стартер spring-cloud-starter-config
. Клиент обнаружения будет включен для микросервиса после включения spring-cloud-starter-netflix-eureka-client
и аннотирования основного класса с помощью @EnableDiscoveryClient
. Чтобы заставить приложение Spring Boot генерировать документацию по API, мы должны включить springfox-swagger2
зависимость и добавить аннотацию @EnableSwagger2
.
Вот полный список зависимостей, определенных для моего примера микросервиса:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
А вот основной класс приложения, которое включает Discovery Client и Swagger2 для микросервиса:
@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
public class EmployeeApplication {
public static void main(String[] args) {
SpringApplication.run(EmployeeApplication.class, args);
}
@Bean
public Docket swaggerApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("pl.piomin.services.employee.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(new ApiInfoBuilder().version("1.0").title("Employee API").description("Documentation Employee API v1.0").build());
}
...
}
Прикладная программа должна получать конфигурацию с удаленного сервера, поэтому мы должны предоставить только bootstrap.yml
файл с именем службы и URL-адрес сервера. Фактически, это пример подхода Config First Bootstrap , когда приложение сначала подключается к серверу конфигурации и получает адрес сервера обнаружения из удаленного источника свойств. Существует также Discovery First Bootstrap , где адрес сервера конфигурации выбирается с сервера обнаружения.
spring:
application:
name: employee-service
cloud:
config:
uri: http://localhost:8088
Там не так много настроек конфигурации. Вот файл конфигурации приложения, хранящийся на удаленном сервере. Он хранит только HTTP-порт и Eureka URL. Однако я также разместил файл employee-service-instance2.yml
на удаленном сервере конфигурации. Он устанавливает другой порт HTTP для приложения, поэтому вы можете легко запустить два экземпляра одной и той же службы локально на основе удаленных свойств. Теперь вы можете запустить второй экземпляр employee-service
на порту 9090 после передачи аргумента spring.profiles.active=instance2
во время запуска приложения. С настройками по умолчанию вы запустите микросервис на порту 8090 .
server:
port: 9090
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8061/eureka/
Вот код с реализацией класса контроллера REST. Он обеспечивает реализацию для добавления новых сотрудников и поиска сотрудников с использованием различных фильтров.
@RestController
public class EmployeeController {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeController.class);
@Autowired
EmployeeRepository repository;
@PostMapping
public Employee add(@RequestBody Employee employee) {
LOGGER.info("Employee add: {}", employee);
return repository.add(employee);
}
@GetMapping("/{id}")
public Employee findById(@PathVariable("id") Long id) {
LOGGER.info("Employee find: id={}", id);
return repository.findById(id);
}
@GetMapping
public List findAll() {
LOGGER.info("Employee find");
return repository.findAll();
}
@GetMapping("/department/{departmentId}")
public List findByDepartment(@PathVariable("departmentId") Long departmentId) {
LOGGER.info("Employee find: departmentId={}", departmentId);
return repository.findByDepartment(departmentId);
}
@GetMapping("/organization/{organizationId}")
public List findByOrganization(@PathVariable("organizationId") Long organizationId) {
LOGGER.info("Employee find: organizationId={}", organizationId);
return repository.findByOrganization(organizationId);
}
}
Шаг 4. Связь между микросервисами с помощью Spring Cloud Open Feign
Наш первый микросервис был создан и запущен. Теперь мы добавим другие микросервисы, которые общаются друг с другом. Следующая диаграмма иллюстрирует поток связи между три образцом microservices: organization-service
, department-service
и employee-service
. Микросервис organization-service
собирает список отделов с ( GET /organization/{organizationId}/with-employees)
или без сотрудников ( GET /organization/{organizationId}
) от department-service
и список сотрудников, не разделяя их на разные отделы напрямую employee-service
. Микросервис department-service
может собирать список сотрудников, назначенных конкретному отделу.
В описанном выше сценарии оба organization-service
и department-service
должны локализовать другие микросервисы и общаться с ними. Вот почему нам нужно включить дополнительную зависимость для этих модулей: spring-cloud-starter-openfeign. Spring Cloud Open Feign — это декларативный клиент REST, который использует балансировщик нагрузки на стороне клиента для связи с другими микросервисами.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Альтернативным решением для Open Feign является Spring RestTemplate
with @LoadBalanced
. Однако Feign предлагает более элегантный способ определения клиентов, поэтому я предпочитаю его вместо RestTemplate
. После включения требуемой зависимости мы также должны включить клиентов Feign, используя @EnableFeignClients
аннотацию.
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableSwagger2
public class OrganizationApplication {
public static void main(String[] args) {
SpringApplication.run(OrganizationApplication.class, args);
}
...
}
Теперь нам нужно определить интерфейсы клиента. Поскольку organization-service
связывается с двумя другими микросервисами, мы должны создать два интерфейса, по одному на каждый микросервис. Интерфейс каждого клиента должен быть аннотирован @FeignClient
. Одно поле в аннотации обязательно для заполнения name
. Это имя должно совпадать с именем целевой службы, зарегистрированной при обнаружении службы. Вот интерфейс клиента, который вызывает конечную точку, GET /organization/{organizationId}
предоставляемую employee-service
.
@FeignClient(name = "employee-service")
public interface EmployeeClient {
@GetMapping("/organization/{organizationId}")
List findByOrganization(@PathVariable("organizationId") Long organizationId);
}
Интерфейс второго клиента, доступный внутри, organization-service
вызывает две конечные точки из department-service
. Первый из них GET /organization/{organizationId}
возвращает организацию только со списком доступных отделов, а второй GET /organization/{organizationId}/with-employees
возвращает тот же набор данных, включая список сотрудников, назначенных каждому отделу.
@FeignClient(name = "department-service")
public interface DepartmentClient {
@GetMapping("/organization/{organizationId}")
public List findByOrganization(@PathVariable("organizationId") Long organizationId);
@GetMapping("/organization/{organizationId}/with-employees")
public List findByOrganizationWithEmployees(@PathVariable("organizationId") Long organizationId);
}
Наконец, мы должны ввести bean-компоненты клиента Feign в контроллер REST. Теперь мы можем вызывать методы, определенные внутри DepartmentClient
и EmployeeClient
, что эквивалентно вызову конечных точек REST.
@RestController
public class OrganizationController {
private static final Logger LOGGER = LoggerFactory.getLogger(OrganizationController.class);
@Autowired
OrganizationRepository repository;
@Autowired
DepartmentClient departmentClient;
@Autowired
EmployeeClient employeeClient;
...
@GetMapping("/{id}")
public Organization findById(@PathVariable("id") Long id) {
LOGGER.info("Organization find: id={}", id);
return repository.findById(id);
}
@GetMapping("/{id}/with-departments")
public Organization findByIdWithDepartments(@PathVariable("id") Long id) {
LOGGER.info("Organization find: id={}", id);
Organization organization = repository.findById(id);
organization.setDepartments(departmentClient.findByOrganization(organization.getId()));
return organization;
}
@GetMapping("/{id}/with-departments-and-employees")
public Organization findByIdWithDepartmentsAndEmployees(@PathVariable("id") Long id) {
LOGGER.info("Organization find: id={}", id);
Organization organization = repository.findById(id);
organization.setDepartments(departmentClient.findByOrganizationWithEmployees(organization.getId()));
return organization;
}
@GetMapping("/{id}/with-employees")
public Organization findByIdWithEmployees(@PathVariable("id") Long id) {
LOGGER.info("Organization find: id={}", id);
Organization organization = repository.findById(id);
organization.setEmployees(employeeClient.findByOrganization(organization.getId()));
return organization;
}
}
Шаг 5. Создание API-шлюза с использованием Spring Cloud Gateway
Spring Cloud Gateway — это относительно новый проект Spring Cloud. Он построен на основе Spring Framework 5, Project Reactor и Spring Boot 2.0. Для этого требуется среда выполнения Netty, предоставляемая Spring Boot и Spring Webflux. Это действительно хорошая альтернатива Spring Cloud Netflix Zuul, который до сих пор был единственным проектом Spring Cloud, предоставляющим шлюзы API для микросервисов.
Шлюз API реализован внутри модуля gateway-service
. Во-первых, мы должны включить стартер spring-cloud-starter-gateway
в зависимости проекта.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
Нам также нужно включить клиент обнаружения, поскольку он gateway-service
интегрируется с Eureka, чтобы иметь возможность выполнять маршрутизацию к последующим сервисам. Шлюз также будет предоставлять спецификации API всех конечных точек, предоставляемых нашими примерами микросервисов. Вот почему мы также включили Swagger2 на шлюзе.
@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
Spring Cloud Gateway предоставляет три основных компонента, используемых для настройки: маршруты, предикаты и фильтры. Маршрут является основным строительным блоком шлюза. Он содержит целевой URI и список определенных предикатов и фильтров. Предикат несет ответственность за соответствие на что — либо из запроса входящего HTTP, такие как заголовки или параметры. Фильтр может изменить запрос и ответ до и после отправки его вниз по течению услуг. Все эти компоненты могут быть установлены с использованием свойств конфигурации. Мы создадим и поместим его в файл сервера конфигурации gateway-service.yml с маршрутами, определенными для наших примеров микросервисов.
Но сначала мы должны включить интеграцию с сервером обнаружения для маршрутов, установив для свойства spring.cloud.gateway.discovery.locator.enabled
значение true. Затем мы можем приступить к определению правил маршрута. Мы используем Factory Predicate Path Route для сопоставления входящих запросов и RewritePath GatewayFilter Factory для изменения запрошенного пути, чтобы адаптировать его к формату, предоставляемому последующими сервисами. Параметр URI указывает имя целевой службы, зарегистрированной на сервере обнаружения. Давайте посмотрим на следующее определение маршрутов. Например, чтобы сделать organization-service
доступным на шлюзе под путем /organization/**
, мы должны определить предикат Path=/organization/**
, а затем удалить префикс /organization
из пути, потому что целевая служба предоставляется по пути/**
, Адрес целевой службы выбирается для Eureka на основе значения URI lb://organization-service
.
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: employee-service
uri: lb://employee-service
predicates:
- Path=/employee/**
filters:
- RewritePath=/employee/(?<path>.*), /$\{path}
- id: department-service
uri: lb://department-service
predicates:
- Path=/department/**
filters:
- RewritePath=/department/(?<path>.*), /$\{path}
- id: organization-service
uri: lb://organization-service
predicates:
- Path=/organization/**
filters:
- RewritePath=/organization/(?<path>.*), /$\{path}
Шаг 6. Включение спецификации API на шлюзе с помощью Swagger2
Каждый микросервис Spring Boot, на котором есть примечание, @EnableSwagger2
предоставляет документацию Swagger API по пути /v2/api-docs
. Однако нам бы хотелось, чтобы эта документация находилась в одном месте — на шлюзе API. Чтобы достичь этого, нам нужно предоставить компонент, реализующий SwaggerResourcesProvider
интерфейс внутри gateway-service
модуля. Этот компонент отвечает за определение местоположения списка ресурсов Swagger, которые должны отображаться приложением. Вот реализация, SwaggerResourcesProvider
которая берет необходимые местоположения из обнаружения службы на основе свойств конфигурации Spring Cloud Gateway.
К сожалению, SpringFox Swagger по-прежнему не обеспечивает поддержку Spring WebFlux. Это означает, что если вы включите в проект зависимости SpringFox Swagger, приложение не запустится … Я надеюсь, что поддержка WebFlux будет доступна в ближайшее время, но теперь мы должны использовать Spring Cloud Netflix Zuul в качестве шлюза, если мы хотим запустить встроенный Swagger2 на нем.
Я создал модуль, proxy-service
который является альтернативным API-шлюзом на основе Netflix Zuul на gateway-service
основе Spring Cloud Gateway. Вот бин с реализацией SwaggerResourcesProvider, доступной внутри proxy-service
. Он использует ZuulProperties
компонент для динамической загрузки определений маршрутов в компонент.
@Configuration
public class ProxyApi {
@Autowired
ZuulProperties properties;
@Primary
@Bean
public SwaggerResourcesProvider swaggerResourcesProvider() {
return () -> {
List resources = new ArrayList();
properties.getRoutes().values().stream()
.forEach(route -> resources.add(createResource(route.getServiceId(), route.getId(), "2.0")));
return resources;
};
}
private SwaggerResource createResource(String name, String location, String version) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation("/" + location + "/v2/api-docs");
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
Вот пользовательский интерфейс Swagger для нашего примера системы микросервисов, доступный по адресу http: // localhost: 8060 / swagger-ui.html .
Шаг 7. Запуск приложений
Давайте посмотрим на архитектуру нашей системы, показанную на следующей диаграмме. Мы обсудим это с organization-service
точки зрения. После запуска organization-service
подключается к config-service
доступному по адресу localhost: 8088 (1) . Основываясь на настройках удаленной конфигурации, он может зарегистрироваться в Eureka (2) . Когда конечная точка organization-service
вызывается внешним клиентом через шлюз (3), доступный по адресу localhost: 8060, запрос перенаправляется в экземпляр на organization-service
основе записей из обнаружения службы (4) . Затем organization-service
ищем адрес department-service
в Eureka (5) и вызываем его конечную точку (6) . В заключение, department-service
вызывает конечную точку из employee-service
. Запрос сбалансирован между двумя доступными экземплярами с employee-service
помощью ленты (7) .
Давайте посмотрим на панель инструментов Eureka, доступную по адресу http: // localhost: 8061 . Есть четыре экземпляра microservices зарегистрированного там: один экземпляр organization-service
и department-service
, и два экземпляра employee-service
.
Теперь давайте назовем конечную точку http: // localhost: 8060 / organization / 1 / with-департаменты и сотрудники .
Шаг 8. Корреляция логов между независимыми микросервисами с использованием Spring Cloud Sleuth
Корреляция журналов между различными микроуслугами с помощью Spring Cloud Sleuth очень проста. Фактически, единственное, что вам нужно сделать, это добавить стартер spring-cloud-starter-sleuth
в зависимости каждого микросервиса и шлюза.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
Для уточнения, мы будем менять по умолчанию формат журнала немного: %d{yyyy-MM-dd HH:mm:ss} ${LOG_LEVEL_PATTERN:-%5p} %m%n
. Вот журналы, сгенерированные нашими тремя примерами микросервисов. Внутри фигурных скобок, []
созданных Spring Cloud Stream, есть четыре записи . Наиболее важной для нас является вторая запись, которая указывает на то traceId
, что устанавливается один раз для каждого входящего HTTP-запроса на границе системы.