Guida ai microservizi reattivi utilizzando Lagom Framework

1. Panoramica

In questo articolo, esploreremo il framework Lagom e implementeremo un'applicazione di esempio usando un'architettura basata su microservizi reattivi.

In poche parole, le applicazioni software reattivi si basano su comunicazione asincrona message-driven e sono altamente reattivo , resiliente e elastico in natura.

Per architettura basata sui microservizi, intendevamo suddividere il sistema in confini tra servizi collaborativi per il raggiungimento degli obiettivi di isolamento , autonomia , responsabilità singola , mobilità , ecc. Per ulteriori letture su questi due concetti, fare riferimento a The Reactive Manifesto e Reactive Microservices Architecture.

2. Perché Lagom?

Lagom è un framework open source creato pensando al passaggio dai monoliti all'architettura dell'applicazione basata su microservizi. Estrae la complessità della creazione, esecuzione e monitoraggio di applicazioni basate su microservizi.

Dietro le quinte, il framework Lagom utilizza Play Framework, un runtime basato sui messaggi Akka, Kafka per i servizi di disaccoppiamento, Event Sourcing e modelli CQRS e il supporto di ConductR per il monitoraggio e il ridimensionamento dei microservizi nell'ambiente del contenitore.

3. Hello World in Lagom

Creeremo un'applicazione Lagom per gestire una richiesta di saluto da parte dell'utente e rispondere con un messaggio di saluto insieme alle statistiche meteorologiche per il giorno.

E svilupperemo due microservizi separati: Greeting e Weather.

Il saluto si concentrerà sulla gestione di una richiesta di saluto, interagendo con il servizio meteo per rispondere all'utente. Il microservizio Meteo servirà la richiesta di statistiche meteo per oggi.

Nel caso di un utente esistente che interagisce con il microservizio Saluto , all'utente verrà mostrato il diverso messaggio di saluto.

3.1. Prerequisiti

  1. Installa Scala (attualmente stiamo usando la versione 2.11.8) da qui
  2. Installa lo strumento di compilazione sbt (attualmente stiamo usando 0.13.11) da qui

4. Configurazione del progetto

Diamo ora una rapida occhiata ai passaggi per impostare un sistema Lagom funzionante.

4.1. Build SBT

Crea una cartella di progetto lagom-hello-world seguita dal file build build.sbt . Un sistema Lagom è tipicamente costituito da una serie di build sbt con ciascuna build corrispondente a un gruppo di servizi correlati:

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

Per cominciare, abbiamo specificato i dettagli dell'organizzazione, la versione scala e Kafka disabilitato per il progetto corrente. Lagom segue una convenzione di due progetti separati per ogni microservizio : un progetto API e un progetto di implementazione.

Il progetto API contiene l'interfaccia del servizio da cui dipende l'implementazione.

Abbiamo aggiunto dipendenze ai moduli Lagom rilevanti come lagomJavadslApi , lagomJavadslPersistenceCassandra per l'utilizzo dell'API Java Lagom nei nostri microservizi e l'archiviazione di eventi relativi all'entità persistente in Cassandra, rispettivamente.

Inoltre, il progetto greeting-impl dipende dal progetto weather-api per recuperare e fornire statistiche meteorologiche mentre saluta un utente.

Il supporto per il plugin Lagom viene aggiunto creando una cartella del plugin con il file plugins.sbt , con una voce per il plugin Lagom. Fornisce tutto il supporto necessario per la creazione, l'esecuzione e la distribuzione della nostra applicazione.

Inoltre, il plugin sbteclipse sarà utile se usiamo Eclipse IDE per questo progetto. Il codice seguente mostra il contenuto di entrambi i plugin:

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

Crea il file project / build.properties e specifica la versione sbt da usare:

sbt.version=0.13.11

4.2. Generazione del progetto

L'esecuzione del comando sbt dalla radice del progetto genererà i seguenti modelli di progetto:

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

Prima di iniziare a implementare i microservizi, aggiungiamo le cartelle src / main / java e src / main / java / resources all'interno di ciascuno dei progetti, per seguire il layout della directory del progetto simile a Maven.

Inoltre, due progetti dinamici vengono generati all'interno di project-root / target / lagom-dynamic-projects :

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

Questi progetti sono utilizzati internamente da Lagom.

5. Interfaccia di servizio

Nel progetto greeting-api , specifichiamo la seguente interfaccia:

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 espone handleGreetFrom () per gestire la richiesta di benvenuto dell'utente. Come tipo di ritorno di questi metodi viene utilizzata un'API ServiceCall . ServiceCall accetta due parametri di tipo Request e Response .

Il parametro Request è il tipo di messaggio di richiesta in arrivo e il parametro Response è il tipo di messaggio di risposta in uscita.

Nell'esempio sopra, non stiamo usando il payload della richiesta, il tipo di richiesta è NotUsed e il tipo di risposta è un messaggio di saluto 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'esecuzione della stessa richiesta di ricciolo per un utente esistente cambierà il messaggio di saluto:

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

9. Conclusione

In questo articolo, abbiamo spiegato come utilizzare il framework Lagom per creare due micro servizi che interagiscono in modo asincrono.

Il codice sorgente completo e tutti i frammenti di codice per questo articolo sono disponibili nel progetto GitHub.