Здравствуйте! Сегодня предлагаем вам очередной интересный пост на неисчерпаемую тему микросервисов, на этот раз — для корифеев и неофитов языка Java. Читаем и голосуем!
В большинстве микросервисных архитектур существует масса возможностей для совместного использования кода – соответственно, велик и соблазн этим заняться. В этой статье я поделюсь собственным опытом: расскажу, когда уместно переиспользовать код, и когда этого лучше избегать. Все моменты будут проиллюстрированы на примере специального проекта с использованием Spring Boot, который доступен на Github.
ВВЕДЕНИЕ
Прежде чем поговорить о совместном использовании кода и о том, что за этим стоит, определимся, какие задачи обычно решаются при помощи микросервисных архитектур. Вот основные преимущества внедрения микросервисов:
- Улучшается масштабирование – различные части приложения масштабируются независимо друг от друга
- Эффективное устранение сильной связанности между различными частями системы – это всегда желательно, но лучше всего достигается именно при помощи микросервисов
- Повышается надежность системы – при отказе одного сервиса остальные сохраняют работоспособность.
- Свобода при выборе технологий – каждый сервис можно реализовывать при помощи той технологии, которая лучше всего подходит для данного случая.
- Улучшенная многоразовость компонентов – сервисы (даже те, что уже развернуты) можно совместно использовать в разных проектах
- Существует и множество других достоинств, зависящих от конкретной архитектуры или решаемой проблемы
Естественно, многие такие преимущества позволяют не только выстроить более качественную систему, но и облегчить жизнь разработчика, сделать его труд более благодарным. Разумеется, можно сколько угодно спорить о них, поэтому давайте просто сойдемся на том, что микросервисы полезны (что подтверждается на опыте таких крупных компаний, как Netflix и Nginx). Как и для любой другой архитектуры, для микросервисов характерны свои недостатки и сложности, которые требуется преодолевать. Наиболее важные таковы:
- Повышенная сложность развертывания – процесс развертывания состоит не из одного или нескольких этапов, а из десятков и даже более
- Больше интеграционного кода – зачастую сервисы должны обмениваться информацией друг с другом. О том, как это правильно организовать, стоило бы написать отдельную статью
- Потребует ли работа в данной предметной области активно копировать код в распределенной системе – или, может быть, нет?
ПРОБЛЕМА
Итак, вот мы и подошли к вопросу, с которым сталкиваются большинство команд, приступающих к работе с микросервисами. Учитывая, какова цель работы с микросервисами и рекомендумые приемы их реализации, сталкиваемся с проблемой: «Нам нужны слабо связанные сервисы, между которыми почти не будет общего кода и зависимостей. Таким образом, всякий раз, когда мы потребляем некоторый сервис, нужно писать классы, которые будут обрабатывать отклик. А как же принцип «DRY» (Не повторяться)? Что делать?». В таком случае легко удариться в два антипаттерна:
- Давайте сделаем так, чтобы сервисы зависели друг от друга! Что ж, это означает, что о слабом связывании можно забыть (здесь нам его точно не добиться), и что свобода в выборе технологии также будет утрачена: логика будет рассыпана по всему коду, и предметная область чрезмерно усложнится.
- Давайте просто копипастить код! Это не так плохо, поскольку, как минимум, позволяет сохранить слабое связывание и не допускает перенасыщения домена логикой. Клиент не может зависеть от кода сервиса. Однако, будем честны; никто не хочет повсюду копипастить одни и те же классы и писать массу трафаретного кода всякий раз, когда планируется потреблять этот гнусный пользовательский сервис. Принцип «Суши код» превратился в мантру не просто так!
РЕШЕНИЕ
Если четко сформулировать назначение архитектуры и как следует пояснить проблему, решение словно напрашивается само собой. Если код сервиса должен быть полностью автономен, но нам понадобится потреблять на клиентах довольно сложные отклики, то клиенты должны писать собственные библиотеки для потребления этого сервиса.
Этот подход обладает следующими достоинствами:
- Сервис полностью отделяется от клиента, а конкретные сервисы не зависят друг от друга – библиотека автономна и клиенто-специфична. Она может быть даже заточена под конкретную технологию, если мы работаем сразу с несколькими технологиями.
- Релиз новой версии клиента никак не зависит от клиента; при наличии обратной совместимости клиенты могут даже «не заметить» релиза, поскольку именно клиент обеспечивает поддержку библиотеки
- Теперь клиенты СУХИЕ – никакой избыточный код не копипастится
- Интеграция с сервисом ускоряется, но при этом мы не теряем никаких преимуществ микросервисной архитектуры.
Данное решение не назовешь совершенно новым – именно такой подход описан в книге «Создание микросервисов» Сэма Ньюмена (очень рекомендую). Воплощение этих идей встречается во многих успешных микросервисных архитектурах. Эта статья посвящена в основном переиспользованию кода в предметной области, но аналогичные принципы применимы и к коду, обеспечивающему общую соединяемость и обмен информацией, поскольку это не противоречит изложенным здесь принципам.
Возможен и иной вопрос: стоит ли беспокоиться о связывании объектов предметной области и соединяемости с клиентскими библиотеками. Как и при ответе на наш основной вопрос, важнейшим фактором в данном случае является влияние таких деталей на общую архитектуру. Если мы решим, что производительность повысится, если включить соединительный код в клиентские библиотеки, то нужно гарантировать, что при этом не возникнет сильного связывания между клиентскими сервисами. Учитывая, что соединяемость в таких архитектурах обычно обеспечивается при помощи простых REST-вызовов, либо при помощи очереди сообщений, не рекомендую ставить такой код в клиентскую библиотеку, поскольку он добавляет лишние зависимости, но при этом не слишком выгоден. Если в коде для соединяемости есть нечто особенное или слишком сложное – например, клиентские сертификаты для выполнения SOAP-запросов, до, возможно, будет целесообразно прицепить дополнительную библиотеку. Если вы изберете такой путь, то всегда задавайте использование клиентской библиотеки как опциональное, а не обязательное. Клиентские сервисы не должны полностью владеть кодом (нельзя обязывать поставщик сервиса непременно обновлять соответствующие клиентские библиотеки).
ПРИМЕР СО SPRING BOOT
Итак, я объяснил решение, а теперь продемонстрирую его в коде. Кстати, вот и возможность лишний раз пропиарить мою любимую микросервисную библиотеку — Spring Boot. Весь пример можно скачать из репозитория на Github, созданного специально для этой статьи.
Spring Boot позволяет разрабатывать микросервисы с места в карьер – да, я не преувеличиваю. Если Dropwizard показался вам быстрым, то вы весьма удивитесь, насколько удобнее работать со Spring Boot. В этом примере мы разрабатываем очень простой сервис User
, который будет возвращать смоделированный объект User
JSON. В дальнейшем этот сервис будет использоваться службой уведомления и табличной службой, фактически, выстраивая различные представления данных; однако, в обоих случаях сервису требуется понимать объект User
.
СЕРВИС USER
В UserServiceApplication
будет находиться основной метод. Поскольку это Spring Boot, при запуске он также включает встроенный сервер Tomcat:
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
В самом деле, проще и быть не может! Spring Boot – очень категоричный фреймворк, поэтому, если умолчания нас устраивают, то набирать вручную почти ничего не приходится. Однако, одну штуку поменять все-таки придется: речь о заданном по умолчанию номере порта. Посмотрим, как это делается в файле application.properties
:
server.port = 9001
Просто и красиво. Если вам доводилось писать REST-сервис на Java, то вы, вероятно, знаете, что для этого нужен Controller
. Если делаете это впервые – не волнуйтесь, писать контроллеры в Spring Boot совсем просто:
package com.example;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@RequestMapping("/user")
public User getUser(@RequestParam(value="id", defaultValue="1") int id) {
return new User(id);
}
}
Так мы просто позволим пользователю выполнять запросы к конечной точке /user?id=
, где id
может соответствовать любому пользователю, который нас интересует. Учитывая, насколько просты эти классы – в самом деле, вся логика должна лежать в конкретном классе User
. Этот класс сгенерирует заготовочные данные и будет сериализован при помощи Jackson (библиотека JSON для Java):
package com.example;
import java.util.ArrayList;
import java.util.List;
public class User {
private final long id;
private final String forename;
private final String surname;
private final String organisation;
private final List<String> notifications;
private final long points;
// Друзья признаны нежелательными и использоваться не будут
private final List<String> friends;
public User(int id) {
String[] forenames = {"Alice", "Manjula", "Bartosz", "Mack"};
String[] surnames = {"Smith", "Salvatore", "Jedrzejewski", "Scott"};
String[] organisations = {"ScottLogic", "UNICEF"};
forename = forenames[id%3];
surname = surnames[id%4];
organisation = organisations[id%2];
notifications= new ArrayList<>();
notifications.add("You have been promoted!");
notifications.add("Sorry, disregard the previous notifaction- wrong user");
points = id * 31 % 1000;
// У вас нет друзей
friends = new ArrayList<>();
this.id = id;
}
// Геттеры и сеттеры на все случаи…
}
Вот и весь сервис, необходимый для создания User JSON. Поскольку это первый рассматриваемый нами сервис Spring Boot, не помешает заглянуть и в файл .pom
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>user-service</name>
<description>Demo user-service with Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
<relativePath/> <!-- ищем родительский узел в репозитории -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
При вызове сервиса, id которого равен 10, видим такой вывод JSON:
КЛИЕНТСКАЯ БИБЛИОТЕКА
Допустим, у нас есть два сервиса, использующих этот API – сервис уведомления и личный кабинет. В реалистичном примере объект User мог бы оказаться гораздо сложнее, и клиентов у нас могло быть не два, а больше. Клиентская библиотека – простой проект под названием user-client-libs
, состоит из единственного класса:
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserView {
private long id;
private String forename;
private String surname;
private String organisation;
private List<String> notifications;
private long points;
public UserView(){
}
public long getId() {
return id;
}
public String getForename() {
return forename;
}
public String getSurname() {
return surname;
}
public String getOrganisation() {
return organisation;
}
public List<String> getNotifications() {
return notifications;
}
public long getPoints() {
return points;
}
}
Как видите, этот класс проще – в нем нет деталей, связанных с имитацией пользователей, нет и списка friends, который в исходном классе признан нежелательным. Мы скрываем эти детали от клиентов. В такой облегченной реализации также будут игнорироваться новые поля, которые может возвращать этот API. Разумеется, в реалистичном примере клиентская библиотека могла получиться гораздо сложнее, что сэкономило бы нам время, затраченное на набор трафаретного кода и помогло бы лучше понять взаимосвязи между полями.
КЛИЕНТЫ
В этом примере показана реализация двух отдельных клиентских сервисов. Один нужен для создания «пользовательского личного кабинета», а другой – для «списка уведомлений». Можете считать их специализированными микросервисами для работы с компонентами пользовательского интерфейса.
Вот контроллер сервиса личного кабинета:
import com.example.user.dto.UserView;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class UserDashboardController {
@RequestMapping("/dashboard")
public String getUser(@RequestParam(value="id", defaultValue="1") int id) {
RestTemplate restTemplate = new RestTemplate();
UserView user = restTemplate.getForObject("http://localhost:9001/user?id="+id, UserView.class);
return "USER DASHBOARD <br>" +
"Welcome " + user.getForename() +" "+user.getSurname()+"<br>"+
"You have " +user.getPoints() + " points! Good job!<br>"+
"<br>"+
"<br>"+user.getOrganisation();
}
}
А это контроллер сервиса личных уведомлений:
import com.example.user.dto.UserView;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class UserNotificationController {
@RequestMapping("/notification")
public String getUser(@RequestParam(value="id", defaultValue="1") int id) {
RestTemplate restTemplate = new RestTemplate();
UserView user = restTemplate.getForObject("http://localhost:9001/user?id="+id, UserView.class);
String response = "NOTIFICATIONS";
int number = 1;
for(String notification : user.getNotifications()){
response += "
Notification number "+(number++)+": "+notification;
}
return response;
}
}
Как видите, оба клиента очень просты, и соединение между ними и сервисом также тривиально. Разумеется, при этом мы должны добавить в файлы .pom зависимости для обоих сервисов
<dependency>
<groupId>com.example</groupId>
<artifactId>user-client-libs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
Все, что осталось сделать в этом примере – запустить все три сервиса на портах 9001, 9002 и 9003 посмотреть вывод:
Личный кабинет:
Уведомления:
ЗАКЛЮЧЕНИЕ
Я считаю, что такой подход к проектированию позволяет решить большинство проблем с переиспользованием кода в микросервисной архитектуре. Он понятен, позволяет избежать большинства недостатков, присущих другим подходам и упрощает жизнь разработчика. Более того – это решение, опробованное на реальных проектах и хорошо себя зарекомендовавшее.
В примере со Spring Boot со всей очевидностью продемонстрировано, насколько удобен такой подход; кроме того, оказывается, что микросервисы гораздо проще, чем могут показаться. Если хотите подробнее изучить этот проект – смотрите у меня на Github и попробуйте его развить.
Удачи с разработкой микросервисов!
P.S. — от авторов перевода:
→ Вот книга о Spring Boot.
→ Вот книга о микросервисах в Spring
Хотите какую-нибудь?
Автор: Издательский дом «Питер»