Одной из основных причин разработки микросервисов является то, что они обеспечивают четкие границы модулей

Однако минусы микросервисов настолько велики, что это все равно, что отрубить себе правую руку, чтобы научиться писать левой. Есть более управляемые (и менее болезненные!) способы достижения того же результата.

Даже с тех пор, как началось повальное увлечение микросервисами, возобладали некоторые более хладнокровные. В частности, Оливер Дротбом, разработчик Spring framework, долгое время был сторонником альтернативы moduliths. Идея заключается в том, чтобы сохранить монолит, но спроектировать его на основе модулей.

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

Зачем нужна модульность?

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

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

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

Модульность работает аналогично: она устанавливает границы вокруг частей кода. Таким образом, эффект от изменения ограничивается этой частью и не распространяется за ее границы.

В Java такие части известны как пакеты. Параллель с кораблями на этом заканчивается, потому что пакеты должны работать вместе для достижения желаемых результатов. Пакеты не могут быть «водонепроницаемыми». Язык Java предоставляет модификаторы видимости для работы через границы пакетов. Интересно, что самый известный из них public позволяет полностью пересекать границы пакетов.

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

Нам нужен более совершенный способ обеспечения соблюдения границ.

Модули, модули повсюду

На протяжении долгой истории Java «модули» были решением для обеспечения соблюдения границ. Дело в том, что даже сегодня существует множество определений того, что такое модуль.

Проект разработки технологии OSGI, запущенный в 2000 году, был нацелен на предоставление версий компонентов, которые можно было бы безопасно развертывать и удалять во время выполнения. Она сохранила единицу развертывания JAR, но добавила метаданные в свой манифест. OSGi была мощной, но разработка OSGi bundle (название модуля) была сложной. Разработчики платили более высокую стоимость за разработку, в то время как команда эксплуатации пользовалась преимуществами при развёртывании. DevOps еще только предстояло родиться, он не сделал OSGi таким популярным, каким он мог бы быть.

Параллельно с этим архитекторы Java искали свой путь к модуляризации JDK. Этот подход намного проще по сравнению с OSGI, поскольку он позволяет избежать проблем с развертыванием и управлением версиями. Модули Java, представленные в Java 9, ограничиваются следующими данными: именем, публичным API и зависимостями от других модулей.

Модули Java хорошо работают для JDK, но гораздо хуже для приложений из-за проблемы «курицы и яйца». Чтобы быть полезными для приложений, разработчики должны разбивать библиотеки на модули, не полагаясь на автоматические модули. Но разработчики библиотек будут делать это только в том случае, если достаточное количество разработчиков приложений будут этим пользоваться. В последний раз, когда я проверял, только половина из 20 общих библиотек были модульными.

Что касается сборки, я должен упомянуть модули Maven. Они позволяют разделить код на несколько проектов.

Существуют и другие системы модулей для JVM, но эти три наиболее известны.

Ориентировочный подход к обеспечению соблюдения границ

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

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

Точно так же, когда человек нарушает правило, он должен это исправить. ArchUnit — это инструмент для создания и применения правил. Он позволяет настраивать правила и проверять их как тесты. К сожалению, настройка занимает много времени и должна постоянно поддерживаться, чтобы обеспечить ценность. Вот фрагмент примера приложения, следующего принципу гексагональной архитектуры:

HexagonalArchitecture.boundedContext("io.reflectoring.buckpal.account")
                     .withDomainLayer("domain")
                     .withAdaptersLayer("adapter")
                     .incoming("in.web")
                     .outgoing("out.persistence")
                     .and()
                         .withApplicationLayer("application")
                         .services("service")
                         .incomingPorts("port.in")
                         .outgoingPorts("port.out")
                     .and()
                         .withConfiguration("configuration")
                         .check(new ClassFileImporter()
                         .importPackages("io.reflectoring.buckpal.."));

Обратите внимание, что класс HexagonalArchitecture представляет собой пользовательский DSL-фасад поверх ArchUnit API.

В целом, ArchUnit лучше, чем ничего, но лишь незначительно. Его основным преимуществом является автоматизация с помощью тестов. Он значительно улучшился бы, если бы архитектурные правила можно было выводить автоматически. Именно эта идея лежит в основе проекта Spring Modulith.

Spring Modulith

Spring Modulith является преемником проекта Moduliths Оливера Дротбома (с буквой S в конце). Он использует как ArchUnit, так и jMolecules. На момент написания этой статьи он был экспериментальным.

Spring Modulith позволяет:

  • Документировать отношения между пакетами проекта.

  • Ограничивать определенные отношения.

  • Проверять ограничения во время тестирования.

Он требует, чтобы приложение использовало Spring Framework: он использует способность их взаимопонимания, получаемую при сборке с помощью DI (инверсии зависимостей).

По умолчанию модуль Modulith представляет собой пакет, расположенный на том же уровне, что и класс с аннотацией SpringBootApplication.

  1. Класс приложения

  2. Modulith модуль

  3. Не модуль

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

Spring Modulith предоставляет сервис формирования диаграмм на основе текста с помощью PlantUML со скинами UML или C4 (по умолчанию). Генерация диаграмм проста как пирог:

var modules = ApplicationModules.of(DummyApplication.class);
new Documenter(modules).writeModulesAsPlantUml();

Чтобы прервать сборку, если модуль обращается к обычному пакету, вызовите в тесте метод verify().

var modules = ApplicationModules.of(DummyApplication.class).verify();

Пример для экспериментов

Я создал пример приложения для экспериментов: оно эмулирует главную страницу интернет-магазина. Главная страница генерируется на стороне сервера с помощью Thymeleaf и отображает элементы каталога и ленту новостей. Последняя также доступна через HTTP API для вызовов на стороне клиента (кодировать который мне было лень). Товары отображаются с указанием цены, поэтому требуется служба ценообразования.

Каждая функция — страница, каталог, лента новостей и ценообразование — находится в пакете, который рассматривается как модуль Spring. Функция документирования Spring Modulith генерирует следующее:

Давайте проверим дизайн функции ценообразования:

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

  • PricingRepository доступен за пределами модуля

  • PricingService раскрывает сущность PricingJPA

Мы исправим дизайн, путем инкапсуляции типов, которые не должны быть открыты. Мы перенесем типы Pricing и PricingRepositoryinternalpricing во внутреннюю подпапку модуля ценообразования:

Module 'home' depends on non-exposed type ch.frankel.blog.pricing.internal.Pricing within module 'pricing'!

Устраним нарушения следующими изменениями:

Заключение

Поработав с примером приложения, я оценил Spring Modulith.

Я вижу два основных сценария использования: документирование существующего приложения и сохранение «чистоты» дизайна. Последнее позволяет избежать эффекта «загнивания» приложений с течением времени. Таким образом, мы можем сохранить дизайн таким, каким он был задуман, и избежать эффекта «спагетти».

Вишенка на торте: это замечательно, когда нам нужно нарезать одну или несколько функций на их единицы развертывания. Это будет очень простой шаг, без потери времени на распутывание зависимостей. Spring Modulith дает огромное преимущество: откладывать каждое важное архитектурное решение до последнего возможного момента.

Благодарю Оливеру Дротбому за отзыв.

Полный исходный код этого поста можно найти на Github.

 Что дальше

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


  1. panzerfaust
    13.01.2023 14:46

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


    1. val6852 Автор
      13.01.2023 15:06

      На Хабре было сравнение модулей и микросервисов:

      Модули вместо микросервисов


    1. val6852 Автор
      13.01.2023 15:09

      1. panzerfaust
        13.01.2023 15:23

        Там нет ответа на мой вопрос. Суть одна: иметь не монолит, а N приложений с независимыми жизненными циклами. Все остальное - детали реализации.
        На моем текущем проекте 30 микросервисов на спринг буте, которые коммуницируют через RMQ и REST. Это с тем же успехом могли быть 30 OSGI-приложений на Apache ServiceMix, коммуницирующих через ActiveMQ и SOAP. Я так и не понимаю, в чем автор видит разницу.


        1. val6852 Автор
          13.01.2023 16:43

          Автор оригинала: Nicolas Fränkel. Я переводил эту статью.

          Его идея, что можно сделать монолит модульным.

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

          Да, я видел статьи об использовании OSGI для реализации микросервисов. Но не видел сравнений разработки и скорости в продакшн.


        1. sshikov
          13.01.2023 18:57

          Как человек делавший такое приложение на OSGI, я бы сказал, что нет особой разницы. Вы точно так же делаете бандлы, публикуете их в контейнер, они публикуют сервисы, ими кто-то пользуется. Технически разница есть, а практически — на уровне изучения принципов «как тут строят приложения».

          >OSGi была мощной, но разработка OSGi bundle (название модуля) была сложной.
          Ну и кстати, нифига. Как опять же делавший бандлы массово, скажу что эта сложность сильно преувеличена. Кто умеет в спринг — умеет в блюпринт уже.


          1. PrinceKorwin
            13.01.2023 19:18

            Я щупал OSGi до того как появился блюпринт. И да. Делать модули тогда было очень больно. Это как J2EE первой версии. Не хочу туда снова :)


            1. sshikov
              13.01.2023 19:20

              Это сколькож лет назад было? То что я делал на J2EE первой версии не похоже вообще. На сегодня делание бандла от делания jar отличается только одним — нужно бы определиться с тем, что вы реально импортируете и экспортируете. А это всегда делать полезно.

              P.S. У нас три человека сейчас их делают. Никакого обучения вообще не проводили. Ноль проблем.


            1. sshikov
              13.01.2023 19:32

              Для ясности — J2EE 1 версии я тоже видел.


    1. ris58h
      13.01.2023 17:25

      Я с OSGI не работал, но всегда думал что бандлы эти можно в рамках одной JVM подгружать, тем самым, например, решать проблему перекрытия классов для разных версий одной и той же библиотеки (а не мучиться с шейдингом в maven). Поправьте, если не прав.

      Думаю, не надо объяснять в чем разница между несколько модулей в одной JVM на одной машине и несколько микросервисов, скорее всего, на разных.


      1. sshikov
        13.01.2023 19:01
        +2

        >несколько модулей в одной JVM на одной машине
        Ну вы почти правы, но не совсем. Современный karaf, на котором строится большинство коммерческих OSGI решений типа Fuse, умеет в кластер много лет. Это ничуть не сложнее микросервисов, а с точки зрения мониторинга к примеру, одна JVM конечно же даст много очков форы куче микросервисов на разных технологиях.

        >не мучиться с шейдингом в maven
        Таки иногда приходится. Пользующиеся механизмом services классы запускать в OSGI сложно. Но вы можете замаскировать внутри бандла некоторые тонкости. Но большинство проблем такого сорта OSGI конечно решает с полпинка.