При большом количестве микросервисов в проекте приходится сталкиваться с тем, что в некоторых из них нужно дублировать один и тот же код (утилитные классы, настройки конфигураций и прочее), а обнаружив баг в одном месте, искать и исправлять его везде. При этом, если микросервисы поддерживаются разными разработчиками, то каждый будет исправлять баг по-своему и в дальнейшем будет сложнее привести всё к единообразию. Давайте на примере рассмотрим несколько подходов к переиспользованию кода в микросервисах.

Представим, что у нас есть сайт ветеринарной клиники, где можно зарегистрироваться, выбрать доктора и записаться к нему на прием. Проект может содержать множество микросервисов, но мы выделим для рассмотрения следующие:

  • user-service - отвечает за авторизацию, регистрацию и за работу с пользователями (как врачами, так и владельцами питомцев).

  • notification-service - отвечает за email рассылку (это могут быть новости, напоминания о приеме и тд.)

  • gateway - микросервис, который будет агрегировать ответы от внутренних микросервисов (таких, как user-service) и подготавливать ответ для фронтенда.

Категории микросервисов

В нашем примере микросервисы можно разделить на несколько категорий:

  • микросервисы, которые используют базу данных и не предоставляют никакие API методы. В списке выше это notification-service, который делает рассылки по расписанию или использует какой-нибудь брокер сообщений (типа kafka или rabbitMQ).

  • микросервисы, которые не используют базу данных, а общаются только со сторонними API и предоставляют свои API методы для взаимодействия. В нашем примере это gateway, который только обрабатывает ответы от других микросервисов.

  • микросервисы, которые используют и базу данных и предоставляют свои API методы для работы (как, например, user-service).

Что, если мы хотим, чтобы все эти микросервисы следовали каким-то внутренним стандартам? Например, те, которые предоставляют API методы для взаимодействия, должны содержать автодокументацию с описанием всех методов посредством OpenAPI, а логирование во всех микросервисах должно быть настроено так, чтобы все логи отправлялись в splunk. В этом случае нам придется дублировать конфигурации и какие-то утилитные классы для работы с датами, ценами и прочим в каждом микросервисе. Для того, чтобы этого избежать, нам подойдёт своя библиотека, в которой мы будем хранить весь переиспользуемый код. Рассмотрим несколько возможных реализаций данной библиотеки.

Одна библиотека для всех микросервисов

Мы можем создать новый maven проект, в котором мы подключим все зависимости, которые могут быть использованы в микросервисах. В этом случае в каждом микросервисе будет большое количество неиспользуемых зависимостей, что повлечет за собой увеличение размера финального jar файла. Помимо этого придется строго следить за тем, чтобы не создавались новые бины без надобности (зачем нам бин для работы с базой данных, если он не нужна в микросервисе?).

Множество разных общих библиотек

Мы можем разделить нашу библиотеку на множество проектов. Тогда мы избавимся от проблем из предыдущего примера, но столкнемся с новыми. При изменении сразу в нескольких библиотеках нужно будет делать множество пулл реквестов и выпускать множество версий этих библиотек.

Одна общая многомодульная библиотека

В этом случае все изменения в одном пулл реквесте, нет никаких лишних зависимостей и бинов в микросервисах, публиковать можно как отдельные версии подпроектов так и выпускать одну версию. Давайте подробнее рассмотрим, как же реализовать подобную библиотеку с использованием maven.

Для примера, определимся с подмодулями, которые нам могут потребоваться, и что в них будет находиться:

  • core - в нем мы будем хранить что-то базовое для всех микросервисов. Например, какие-то наши базовые исключения типа PlatformException или ApplicationException. Также там можно хранить какие-то базовые структуры данных, типа Pair, Timer и все подобное, что может использоваться в любом нашем микросервисе вне зависимости от его назначения.

  • utils - в этом модуле будем хранить утилитные классы общего назначения. Например, классы для работы с датами, ценами, json и тп.

  • database подмодуль будет содержать какие-то полезные вещи для работы с базой данных. Это могут быть кастомные сериалайзеры / десериалайзеры полей базы данных и многое другое. В нашем примере этот подмодуль могут использовать user-service, notification-service.

  • logging - в нем мы сможем описать конфигурацию для направления всех наших логов в splunk.

  • client - в этом подмодуле мы сможем хранить всё, что может потребоваться в микросервисах, которые взаимодействуют с другими API. Его может использовать gateway.

  • api - здесь мы будем хранить все для сервисов, которые будут предоставлять методы API. Например, конфигурацию для автогенерации документации и тому подобное. Его можно подключить в user-service и gateway.

  • любые другие подмодули, который вам могут понадобиться.

Описание многомодульного проекта

Давайте начнем с описания базового pom файла. В качестве родителя будем использовать 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>com.example</groupId>
		<artifactId>base-pom</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>
	<groupId>com.example.common</groupId>
	<artifactId>common-library-parent</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>pom</packaging>

	<name>common-library-example-parent</name>
	<description>Demo project</description>

	<modules>
		<module>core</module>
        <module>utils</module>
		<module>database</module>
        <module>logging</module>
        <module>client</module>
        <module>api</module>
	</modules>
</project>

В каждый подмодуль нужно добавить свой pom файл. В нем можно добавить все зависимости, необходимые в конкретном подмодуле, включая зависимости на другие подмодули. Для примера возьмем pom файл из подмодуля client, который требует зависимость на feign (подробнее о том, как его использовать я писал в своей статье) и исключения из подмодуля core. Таким образом 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>com.example</groupId>
		<artifactId>common-library-parent</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>
	<artifactId>client</artifactId>
	<version>${project.parent.version}</version>
	<name>client</name>
	<description>Demo project</description>

	<dependencies>
		<dependency>
			<groupId>${project.groupId}</groupId>
			<artifactId>core</artifactId>
			<version>${project.version}</version>
		</dependency>
        <dependency>
        	<groupId>io.github.openfeign</groupId>
        	<artifactId>feign-spring4</artifactId>
        </dependency>
	</dependencies>
</project>

В подмодуле мы можем добавить классы для расширения возможностей feign клиента. Например, свою реализация логирования запросов / ответов от API и многое другое.

Финальная структура проекта будет выглядеть следующим образом:

Структура проекта
Структура проекта

Подключение многомодульной библиотеки в микросервисы

Для того, чтобы использовать данную библиотеку, как и в случае с parent pom, ее сначала нужно опубликовать в локальный репозиторий (или же в другом хранилище, например, nexus) командой mvn install Вы увидите следующее:

Далее вы уже сможете подключить необходимые подмодули в микросервисы. Подключим подмодуль client в наш микросервис gateway для того, чтобы легко добавить в нем интеграцию с user-service:

<properties>
	<common-lib.version>0.0.1-SNAPSHOT</common-lib.version>
</properties>

<dependencies>
  <dependency>
		<groupId>com.example</groupId>
		<artifactId>client</artifactId>
		<version>${common-lib.version}</version>
	</dependency>
</dependencies>

Заключение

Конечно, количество подмодулей может быть любым. Все зависит от того, что именно требуется в вашем проекте.

Полезные ссылки

Комментарии (6)


  1. js605451
    30.10.2023 18:29
    +4

    Когда говорят слово "микросервисы", часто забывают уточнить о каких именно аспектах микросервисности идёт речь. Независимые билды/деплойменты? Изоляция разных команд друг от друга? Использование разных ЯП для разных сервисов?

    Если всё гарантированно пишется на джаве, лежит в одном репозитории, билдится и деплоится как монолит - можно просто сразу делать монолитную кодовую базу, в которой взаимодействие между модулями, в зависимости от профиля сборки, либо "напрямую" (обычные вызовы внутри ЯП), либо "через транспорт".

    Такой подход упрощает разработку, потому что программируется фактически монолит - легко переиспользовать код, легко рефакторить, легко заставить работать локально, легко отлаживать, легко писать интеграционные тесты, а "микросервисность" появляется только при деплойменте.


    1. nApoBo3
      30.10.2023 18:29
      +3

      Так вы любую внешнюю зависимость под монолит подведёте.

      Собрать набор общих для многих проектов классов в мавен не делает проект монолитом.


  1. aleksandy
    30.10.2023 18:29
    +7

    Если изменения в одном сервисе требуют каскадных изменений в других, то это не микросервисы, а распределённый монолит.
    А всякие переиспользование кода между именно микросервисами через общие зависимости де-факто не отменяет того, что придётся править в нескольких местах, т.к. как минимум надо поднимать версии в зависимостях и вносить правки в код, использующий эти самые общие библиотеки.


    1. ermadmi78
      30.10.2023 18:29

      ИМХО - это слишком радикальный критерий. Например изменение API одного из микросервисов с потерей обратной совместимости гарантированно приведёт к каскадным изменениям в других микросервисах вне зависимости от того, используют они общие библиотеки или нет. Понятно, что обратную совместимость нужно всеми силами стараться сохранить. Но, жизнь штука сложная, и далеко не всегда получается это сделать.


  1. RomTec
    30.10.2023 18:29

    При большом количестве микросервисов в проекте приходится сталкиваться с тем, что в некоторых из них нужно дублировать один и тот же код, а обнаружив баг в одном месте, искать и исправлять его везде. При этом, если микросервисы поддерживаются разными разработчиками, то каждый будет исправлять баг по-своему и в дальнейшем будет сложнее привести всё к единообразию

    Исходные предположения мне показались не совсем верными, поэтому и логика всей статьи нарушена.
    Микросервис это прежде всего функциональная независимость и самостоятельность, причём и функциональная, и кодовая. И именно "дублировать один и тот же код", "исправлять его везде" и приводить "всё к единообразию" как раз не нужно !
    А при переиспользовании общего кода исправление найденного в нём бага уж точно будет не по-своему, а сообща и согласованно всеми разными разработчиками ).

    Но сама тема общей кодовой база в микросервисах кажется интересной и требует дополнительного раскрытия.


  1. werevolff
    30.10.2023 18:29
    +1

    По-моему, я уже видел на Хабре похожую статью про попытки сделать переиспользуемый код микросервисов, ушедшую в минусы. А, да, это же ваша статья про управления зависимости в микросервисах!