Guide des microservices réactifs utilisant le framework Lagom

1. Vue d'ensemble

Dans cet article, nous allons explorer le framework Lagom et implémenter un exemple d'application à l' aide d'une architecture pilotée par des microservices réactifs.

En termes simples, les applications logicielles réactives reposent sur la communication asynchrone géré par message et sont très réactif , résiliente et élastique dans la nature.

Par architecture axée sur les microservices, nous entendions diviser le système en frontières entre les services collaboratifs pour atteindre les objectifs d' isolement , d' autonomie , de responsabilité unique , de mobilité , etc.

2. Pourquoi Lagom?

Lagom est un framework open source conçu en pensant au passage des monolithes à une architecture d'applications basée sur les microservices. Il résume la complexité de la création, de l'exécution et de la surveillance des applications basées sur des microservices.

Dans les coulisses, le framework Lagom utilise Play Framework, un environnement d'exécution basé sur les messages Akka, Kafka pour les services de découplage, l'approvisionnement d'événements et les modèles CQRS, et le support ConductR pour la surveillance et la mise à l'échelle des microservices dans l'environnement de conteneur.

3. Hello World à Lagom

Nous allons créer une application Lagom pour gérer une demande de bienvenue de l'utilisateur et y répondre avec un message d'accueil ainsi que des statistiques météorologiques pour la journée.

Et nous allons développer deux microservices distincts: accueil et météo.

Le message d'accueil se concentrera sur le traitement d'une demande de message d'accueil, l'interaction avec le service météo pour répondre à l'utilisateur. Le microservice Météo traitera la demande de statistiques météorologiques pour aujourd'hui.

Dans le cas d'un utilisateur existant interagissant avec le microservice de salutation , le message d'accueil différent sera montré à l'utilisateur.

3.1. Conditions préalables

  1. Installez Scala (nous utilisons actuellement la version 2.11.8) à partir d'ici
  2. Installez l' outil de construction sbt (nous utilisons actuellement la version 0.13.11) à partir d'ici

4. Configuration du projet

Voyons maintenant rapidement les étapes de configuration d'un système Lagom fonctionnel.

4.1. Construction SBT

Créez un dossier de projet lagom-hello-world suivi du fichier de construction build.sbt . Un système Lagom est généralement composé d'un ensemble de builds sbt, chaque build correspondant à un groupe de services associés:

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))

Pour commencer, nous avons spécifié les détails de l'organisation, la version scala et désactivé Kafka pour le projet actuel. Lagom suit une convention de deux projets distincts pour chaque microservice : un projet API et un projet d'implémentation.

Le projet API contient l'interface de service dont dépend l'implémentation.

Nous avons ajouté des dépendances aux modules Lagom pertinents tels que lagomJavadslApi , lagomJavadslPersistenceCassandra pour utiliser l'API Java Lagom dans nos microservices et stocker les événements liés à l'entité persistante dans Cassandra, respectivement.

En outre, le projet Greet-Impl dépend du projet weather-api pour récupérer et diffuser des statistiques météorologiques tout en saluant un utilisateur.

La prise en charge du plugin Lagom est ajoutée en créant un dossier de plugin avec le fichier plugins.sbt , ayant une entrée pour le plugin Lagom. Il fournit tout le support nécessaire pour créer, exécuter et déployer notre application.

De plus, le plugin sbteclipse sera pratique si nous utilisons Eclipse IDE pour ce projet. Le code ci-dessous montre le contenu des deux plugins:

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

Créez le fichier project / build.properties et spécifiez la version sbt à utiliser:

sbt.version=0.13.11

4.2. Génération de projet

L'exécution de la commande sbt à partir de la racine du projet générera les modèles de projet suivants:

  1. salutation-api
  2. salutation-impl
  3. météo-api
  4. météo-impl

Avant de commencer à implémenter les microservices, ajoutons les dossiers src / main / java et src / main / java / resources dans chacun des projets, pour suivre la disposition des répertoires de projet de type Maven.

De plus, deux projets dynamiques sont générés dans project-root / target / lagom-dynamic-projects :

  1. lagom-interne-meta-projet-cassandra
  2. localisateur de service de méta-projet interne lagom

Ces projets sont utilisés en interne par Lagom.

5. Interface de service

Dans le projet salutation-api , nous spécifions l'interface suivante:

public interface GreetingService extends Service { public ServiceCall handleGreetFrom(String user); @Override default Descriptor descriptor() { return named("greetingservice") .withCalls(restCall(Method.GET, "/api/greeting/:fromUser", this::handleGreetFrom)) .withAutoAcl(true); } }

GreetingService expose handleGreetFrom () pour gérer la demande de bienvenue de l'utilisateur. Une API ServiceCall est utilisée comme type de retour de ces méthodes. ServiceCall prend deux paramètres de type Request et Response .

Le paramètre Request est le type du message de demande entrant et le paramètre Response est le type du message de réponse sortant.

Dans l'exemple ci-dessus, nous n'utilisons pas la charge utile de la demande, le type de demande est NotUsed et le type de réponse est un message d'accueil String .

GreetingService also specifies a mapping to the actual transport used during the invocation, by providing a default implementation of the Service.descriptor() method. A service named greetingservice is returned.

handleGreetFrom() service call is mapped using a Rest identifier: GET method type and path identifier /api/greeting/:fromUser mapped to handleGreetFrom() method. Check this link out for more details on service identifiers.

On the same lines, we define WeatherService interface in the weather-api project. weatherStatsForToday() method and descriptor() method are pretty much self explanatory:

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

WeatherStats is defined as an enum with sample values for different weather and random lookup to return weather forecast for the day:

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

Simply put, in a system making use of Event Sourcing, we'll be able to capture all changes as immutable domain events appended one after the other. The current state is derived by replaying and processing events. This operation is essentially a foldLeft operation known from the Functional Programming paradigm.

Event sourcing helps to achieve high write performance by appending the events and avoiding updates and deletes of existing events.

Let's now look at our persistent entity in the greeting-impl project, GreetingEntity:

public class GreetingEntity extends PersistentEntity { @Override public Behavior initialBehavior( Optional 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 provides PersistentEntity API for processing incoming events of type Command via setCommandHandler() methods and persist state changes as events of type Event. The domain object state is updated by applying the event to the current state using the setEventHandler() method. The initialBehavior() abstract method defines the Behavior of the entity.

In initialBehavior(), we build original GreetingState “Hello” text. Then we can define a ReceivedGreetingCommand command handler – which produces a ReceivedGreetingEvent Event and gets persisted in the event log.

GreetingState is recalculated to “Hello Again” by the ReceivedGreetingEvent event handler method. As mentioned earlier, we're not invoking setters – instead, we are creating a new instance of State from the current event being processed.

Lagom follows the convention of GreetingCommand and GreetingEvent interfaces for holding together all the supported commands and events:

public interface GreetingCommand extends Jsonable { @JsonDeserialize public class ReceivedGreetingCommand implements GreetingCommand, CompressedJsonable, PersistentEntity.ReplyType { @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. Service Implementation

7.1. Greeting Service

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 handleGreetFrom(String user) { return request -> { PersistentEntityRef ref = persistentEntityRegistry.refFor( GreetingEntity.class, user); CompletableFuture greetingResponse = ref.ask(new ReceivedGreetingCommand(user)) .toCompletableFuture(); CompletableFuture todaysWeatherInfo = (CompletableFuture) 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 weatherStatsForToday() { return req -> CompletableFuture.completedFuture(WeatherStats.forToday()); } }

We follow the same steps as we did above for greeting module to register the weather module with Lagom:

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

Also, register the weather module to Play's framework list of enabled modules:

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

8. Running the Project

Lagom allows running any number of services together with a single command.

We can start our project by hitting the below command:

sbt lagom:runAll

This will start the embedded Service Locator, embedded Cassandra and then start microservices in parallel. The same command also reloads our individual microservice when the code changes so that we don’t have to restart them manually.

We can be focused on our logic and Lagom handle the compilation and reloading. Once started successfully, we will see the following output:

................ [info] Cassandra server running at 127.0.0.1:4000 [info] Service locator is running at //localhost:8000 [info] Service gateway is running at //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...)

Once started successfully we can make a curl request for greeting:

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

We will see following output on the console:

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

L'exécution de la même requête curl pour un utilisateur existant modifiera le message d'accueil:

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

9. Conclusion

Dans cet article, nous avons expliqué comment utiliser le framework Lagom pour créer deux micro services qui interagissent de manière asynchrone.

Le code source complet et tous les extraits de code de cet article sont disponibles dans le projet GitHub.