«1. Обзор

В этой статье мы рассмотрим платформу Lagom и реализуем пример приложения с использованием реактивной архитектуры, управляемой микросервисами.

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

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

2. Почему Лагом?

Lagom — это фреймворк с открытым исходным кодом, созданный с учетом перехода от монолитов к архитектуре приложений, управляемой микросервисами. Он абстрагируется от сложности создания, запуска и мониторинга приложений, управляемых микросервисами.

За кулисами платформа Lagom использует Play Framework, среду выполнения Akka, управляемую сообщениями, Kafka для разделения сервисов, шаблонов Event Sourcing и CQRS, а также поддержку ConductR для мониторинга и масштабирования микросервисов в контейнерной среде.

3. Hello World в Lagom

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

И мы будем разрабатывать два отдельных микросервиса: Приветствие и Погода.

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

В случае, если существующий пользователь взаимодействует с микросервисом Greeting, пользователю будет показано другое приветственное сообщение.

3.1. Предварительные требования

  1. Install Scala (we are currently using 2.11.8 version) from here
  2. Install sbt build tool (we are currently using 0.13.11) from here

4. Настройка проекта

Давайте теперь кратко рассмотрим шаги по настройке работающей системы Lagom.

4.1. Сборка SBT

Создайте папку проекта lagom-hello-world, за которой следует файл сборки build.sbt. Система Lagom обычно состоит из набора сборок sbt, каждая из которых соответствует группе связанных сервисов:

organization in ThisBuild := "com.baeldung"

scalaVersion in ThisBuild := "2.11.8"

lagomKafkaEnabled in ThisBuild := false

lazy val greetingApi = project("greeting-api")
  .settings(
    version := "1.0-SNAPSHOT",
    libraryDependencies ++= Seq(
      lagomJavadslApi
    )
  )

lazy val greetingImpl = project("greeting-impl")
  .enablePlugins(LagomJava)
  .settings(
    version := "1.0-SNAPSHOT",
    libraryDependencies ++= Seq(
      lagomJavadslPersistenceCassandra
    )
  )
  .dependsOn(greetingApi, weatherApi)

lazy val weatherApi = project("weather-api")
  .settings(
    version := "1.0-SNAPSHOT",
    libraryDependencies ++= Seq(
      lagomJavadslApi
    )
  )

lazy val weatherImpl = project("weather-impl")
  .enablePlugins(LagomJava)
  .settings(
    version := "1.0-SNAPSHOT"
  )
  .dependsOn(weatherApi)

def project(id: String) = Project(id, base = file(id))

Для начала мы указали детали организации, версию scala и отключили Kafka для текущий проект. Lagom следует соглашению о двух отдельных проектах для каждого микросервиса: проект API и проект реализации.

Проект API содержит интерфейс сервиса, от которого зависит реализация.

Мы добавили зависимости к соответствующим модулям Lagom, таким как lagomJavadslApi, lagomJavadslPersistenceCassandra, для использования Lagom Java API в наших микросервисах и хранения событий, связанных с постоянным объектом в Cassandra, соответственно.

Кроме того, проект Greeting-Imple зависит от проекта Weather-API для получения и обслуживания статистики погоды во время приветствия пользователя.

Поддержка плагина Lagom добавлена ​​путем создания папки плагина с файлом plugins.sbt, имеющим запись для плагина Lagom. Он предоставляет всю необходимую поддержку для создания, запуска и развертывания нашего приложения.

Кроме того, плагин sbteclipse будет полезен, если мы будем использовать Eclipse IDE для этого проекта. В приведенном ниже коде показано содержимое обоих плагинов:

addSbtPlugin("com.lightbend.lagom" % "lagom-sbt-plugin" % "1.3.1")
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "3.0.0")

Создайте файл проекта/build.properties и укажите используемую версию sbt:

sbt.version=0.13.11

4.2. Генерация проекта

Запуск команды sbt из корня проекта сгенерирует следующие шаблоны проекта:

  1. greeting-api
  2. greeting-impl
  3. weather-api
  4. weather-impl

Прежде чем приступить к реализации микросервисов, давайте добавим папки src/main/java и src/main/java/resources внутрь каждого из проекты, чтобы следовать макету каталога проекта, подобному Maven.

Кроме того, внутри проекта-root/target/lagom-dynamic-projects генерируются два динамических проекта:

  1. lagom-internal-meta-project-cassandra
  2. lagom-internal-meta-project-service-locator

Эти проекты используются внутри Lagom.

5. Интерфейс службы

В проекте Greeting-API мы указываем следующий интерфейс:

public interface GreetingService extends Service {

    public ServiceCall<NotUsed, String> handleGreetFrom(String user);

    @Override
    default Descriptor descriptor() {
        return named("greetingservice")
          .withCalls(restCall(Method.GET, "/api/greeting/:fromUser",
            this::handleGreetFrom))
          .withAutoAcl(true);
    }
}

GreetingService предоставляет функцию handleGreetFrom() для обработки запроса приветствия от пользователя. ServiceCall API используется в качестве возвращаемого типа этих методов. ServiceCall принимает два параметра типа Request и Response.

«Параметр Request — это тип входящего сообщения запроса, а параметр Response — тип исходящего ответного сообщения.

В приведенном выше примере мы не используем полезную нагрузку запроса, тип запроса — NotUsed, а тип ответа — приветственное сообщение String.

GreetingService также указывает сопоставление с фактическим транспортом, используемым во время вызова, предоставляя реализацию метода Service.descriptor() по умолчанию. Возвращается служба с именем GreetingService.

Вызов службы handleGreetFrom() сопоставляется с использованием идентификатора Rest: тип метода GET и идентификатор пути /api/greeting/:fromUser сопоставляются с методом handleGreetFrom(). Перейдите по этой ссылке для получения более подробной информации об идентификаторах служб.

Таким же образом мы определяем интерфейс WeatherService в проекте Weather-API. Метод WeatherStatsForToday() и метод descriptor() говорят сами за себя:

public interface WeatherService extends Service {
    
    public ServiceCall<NotUsed, WeatherStats> weatherStatsForToday();

    @Override
    default Descriptor descriptor() {
        return named("weatherservice")
          .withCalls(
            restCall(Method.GET, "/api/weather",
              this::weatherStatsForToday))
          .withAutoAcl(true);
    }
};

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

public enum WeatherStats {

    STATS_RAINY("Going to Rain, Take Umbrella"), 
    STATS_HUMID("Going to be very humid, Take Water");

    public static WeatherStats forToday() {
        return VALUES.get(RANDOM.nextInt(SIZE));
    }
}

6. Lagom Persistence — Event Sourcing

Проще говоря, в системе, использующей Event Sourcing, мы сможем фиксировать все изменения в виде неизменяемых доменных событий, добавляемых одно за другим. Текущее состояние определяется путем воспроизведения и обработки событий. Эта операция по существу является операцией foldLeft, известной из парадигмы функционального программирования.

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

Давайте теперь посмотрим на нашу персистентную сущность в проекте Greeting-impl, GreetingEntity:

public class GreetingEntity extends 
  PersistentEntity<GreetingCommand, GreetingEvent, GreetingState> {

      @Override
      public Behavior initialBehavior(
        Optional<GreetingState> snapshotState) {
            BehaviorBuilder b 
              = newBehaviorBuilder(new GreetingState("Hello "));
        
            b.setCommandHandler(
              ReceivedGreetingCommand.class,
              (cmd, ctx) -> {
                  String fromUser = cmd.getFromUser();
                  String currentGreeting = state().getMessage();
                  return ctx.thenPersist(
                    new ReceivedGreetingEvent(fromUser),
                    evt -> ctx.reply(
                      currentGreeting + fromUser + "!"));
              });
        
            b.setEventHandler(
              ReceivedGreetingEvent.class,
              evt -> state().withMessage("Hello Again "));

            return b.build();
      }
}

Lagom предоставляет API PersistentEntity\u003cCommand, Entity, Event\u003e для обработки входящих событий типа Command с помощью методов setCommandHandler() и сохранения изменения состояния как события типа Event. Состояние объекта домена обновляется путем применения события к текущему состоянию с помощью метода setEventHandler(). Абстрактный метод initialBehavior() определяет поведение объекта.

В InitialBehavior() мы создаем исходный текст GreetingState «Hello». Затем мы можем определить обработчик команды ReceivedGreetingCommand, который создает событие ReceivedGreetingEvent и сохраняется в журнале событий.

GreetingState пересчитывается в «Hello Again» методом обработчика событий ReceivedGreetingEvent. Как упоминалось ранее, мы не вызываем сеттеры — вместо этого мы создаем новый экземпляр State из текущего обрабатываемого события.

Lagom следует соглашению интерфейсов GreetingCommand и GreetingEvent для объединения всех поддерживаемых команд и событий:

public interface GreetingCommand extends Jsonable {

    @JsonDeserialize
    public class ReceivedGreetingCommand implements 
      GreetingCommand, 
      CompressedJsonable, 
      PersistentEntity.ReplyType<String> {      
          @JsonCreator
          public ReceivedGreetingCommand(String fromUser) {
              this.fromUser = Preconditions.checkNotNull(
                fromUser, "fromUser");
          }
    }
}
public interface GreetingEvent extends Jsonable {
    class ReceivedGreetingEvent implements GreetingEvent {

        @JsonCreator
        public ReceivedGreetingEvent(String fromUser) {
            this.fromUser = fromUser;
        }
    }
}

7. Реализация службы

public class GreetingServiceImpl implements GreetingService {

    @Inject
    public GreetingServiceImpl(
      PersistentEntityRegistry persistentEntityRegistry, 
      WeatherService weatherService) {
          this.persistentEntityRegistry = persistentEntityRegistry;
          this.weatherService = weatherService;
          persistentEntityRegistry.register(GreetingEntity.class);
      }

    @Override
    public ServiceCall<NotUsed, String> handleGreetFrom(String user) {
        return request -> {
            PersistentEntityRef<GreetingCommand> ref
              = persistentEntityRegistry.refFor(
                GreetingEntity.class, user);
            CompletableFuture<String> greetingResponse 
              = ref.ask(new ReceivedGreetingCommand(user))
                .toCompletableFuture();
            CompletableFuture<WeatherStats> todaysWeatherInfo
              = (CompletableFuture<WeatherStats>) weatherService
                .weatherStatsForToday().invoke();
            
            try {
                return CompletableFuture.completedFuture(
                  greetingResponse.get() + " Today's weather stats: "
                    + todaysWeatherInfo.get().getMessage());
            } catch (InterruptedException | ExecutionException e) {
                return CompletableFuture.completedFuture(
                  "Sorry Some Error at our end, working on it");
            }
        };
    }
}

Simply put, we inject the PersistentEntityRegistry and WeatherService dependencies using @Inject (provided by Guice framework), and we register the persistent GreetingEntity.

The handleGreetFrom() implementation is sending ReceivedGreetingCommand to the GreetingEntity to process and return greeting string asynchronously using CompletableFuture implementation of CompletionStage API.

Similarly, we make an async call to Weather microservice to fetch weather stats for today.

Finally, we concatenate both outputs and return the final result to the user.

To register an implementation of the service descriptor interface GreetingService with Lagom, let’s create GreetingServiceModule class which extends AbstractModule and implements ServiceGuiceSupport:

public class GreetingServiceModule extends AbstractModule 
  implements ServiceGuiceSupport {
 
      @Override
      protected void configure() {
          bindServices(
            serviceBinding(GreetingService.class, GreetingServiceImpl.class));
          bindClient(WeatherService.class);
    }
}

Also, Lagom internally uses the Play Framework. And so, we can add our module to Play’s list of enabled modules in src/main/resources/application.conf file:

play.modules.enabled
  += com.baeldung.lagom.helloworld.greeting.impl.GreetingServiceModule

7.2. Weather Service

After looking at the GreetingServiceImpl, WeatherServiceImpl is pretty much straightforward and self-explanatory:

public class WeatherServiceImpl implements WeatherService {
 
    @Override
    public ServiceCall<NotUsed, WeatherStats> weatherStatsForToday() {
        return req -> 
          CompletableFuture.completedFuture(WeatherStats.forToday());
    }
}

7.1. Служба приветствия

public class WeatherServiceModule 
  extends AbstractModule 
  implements ServiceGuiceSupport {
 
      @Override
      protected void configure() {
          bindServices(serviceBinding(
            WeatherService.class, 
            WeatherServiceImpl.class));
      }
}

Мы выполняем те же шаги, что и выше, для модуля приветствия, чтобы зарегистрировать модуль погоды в Lagom:

play.modules.enabled
  += com.baeldung.lagom.helloworld.weather.impl.WeatherServiceModule

Кроме того, зарегистрируйте модуль погоды в списке включенных модулей платформы Play: ~~ ~

8. Запуск проекта

Lagom позволяет запускать любое количество сервисов одной командой.

sbt lagom:runAll

Мы можем начать наш проект, выполнив следующую команду:

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

................
[info] Cassandra server running at 127.0.0.1:4000
[info] Service locator is running at http://localhost:8000
[info] Service gateway is running at http://localhost:9000
[info] Service weather-impl listening for HTTP on 0:0:0:0:0:0:0:0:56231 and how the services interact via
[info] Service greeting-impl listening for HTTP on 0:0:0:0:0:0:0:0:49356
[info] (Services started, press enter to stop and go back to the console...)

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

curl http://localhost:9000/api/greeting/Amit

После успешного запуска мы можем сделать запрос curl для приветствия:

Hello Amit! Today's weather stats: Going to Rain, Take Umbrella

Мы увидим следующий вывод на консоли:

Hello Again Amit! Today's weather stats: Going to Rain, Take Umbrella

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

9. Заключение

В этой статье мы рассмотрели, как использовать платформу Lagom для создания двух микросервисов, взаимодействующих асинхронно.