Verwendung der typisierten Konfiguration
 
  Vereinfachtes User-Repository
 
  User-Login Test
 
  Ungeordnetes Container-Startup-Event
 
  UI-Validierung mit Bean-Validation Constraints
 
  UI-Controller für manuellen Logout
 
  UI spezifischer IdentityHolder
 
  Typsichere Konfiguration als Partial-Bean
 
  Typisierter Config-Handler mit Remote-Support
 
  TokenExpirationManager für Tests
 
  Tokenerneuerung testen
 
  Test eines invaliden Logins
 
  Simples Login Test-Target
 
  Simple Login
 
  REST-Resource Client als Partial-Bean im UI
 
  REST-Resource Client als Partial-Bean
 
  Remote-Archivierung via Partial-Bean
 
  Nested Partial-Bean
 
  Meecrowave Starter-Klasse
 
  Manuell übertragener Token
 
  JAX-RS Test mit Meecrowave
 
  JAX-RS Resource zur User-Registrierung
 
  JAX-RS Endpunkt zur Archivierung
 
  JAX-RS Endpunkt zum Versenden von E-Mails
 
  Geordnete Container-Startup-Events
 
  Erweiterung der Annotation TypedConfig
 
  Dynamisches Login Test-Target
 
  CDI-Test mit Meecrowave
 
  CDI-Observer für asynchrone Events
 
  CDI 1.1 Container-Startup-Event
 
  Autom. auffindbare JAX-RS Applikation
 
  Asynchrone CDI-Events feuern
 
  Asynchrone Archivierung
 
  Angepasstes IdeaRepository
 
  Anbindung von CategoryService
 
  Aktuelle User-Details laden
 
  Abgelaufene Token testen
 
 

8 CDI Micro

Seit einigen Jahren werden Themen wie Cloud-Computing und kleinere Deploymenteinheiten stark gepushed. Einige PaaS-Plattformen ermöglichten bereits seit Java EE 6 standardkonforme EE-Applikationen unverändert zu deployen. In vielen Fällen steht Java EE selbst allerdings nicht mehr im Zentrum der Architekturüberlegungen, sondern stellt eine mögliche Variante dar. Applikationen werden immer öfters in eigenständige kleine Module unterteilt, welche unabhängig deployed werden können. In diesem Zusammenhang sind sogenannte Micro-Deployments zur gängigen Praxis geworden. Hierbei gibt es verschiedene Ausprägungen, für welche es teilweise unterschiedliche Definitionen, Sichtweisen und Empfehlungen gibt. Oftmals werden die unterschiedlichen Ansätze pauschal mit dem Begriff "Microservices" referenziert. Wir bleiben in diesem Kapitel bei dem Begriff Micro-Deployments, da wir uns exemplarisch die Aufteilung von IdeaFork in kleinere Deploymenteinheiten ansehen.

 

Die Kommunikation zwischen Services bzw. Modulen erfolgt über ein vordefiniertes Protokoll. Inter-Modulkommunikation via REST verbreitete sich in diesem Zusammenhang sehr schnell, da per Definition eine lose Koppelung ermöglicht wird und Module folglich mit unterschiedlichen Technologien umsetzbar sind. Eine Bedingung ist die Verwendung von REST allerdings nicht. Es kann prinzipiell jedes Protokoll verwendet werden, welches von den Technologien aller Module unterstützt wird. Durch die Eigenschaften von HTTP (v1.0 und v1.1) wird in einigen Projekten auf Alternativen wie bspw. "gRPC" zurückgegriffen. Statt JSON als Serialisierungsformat wird in diesem Fall auf Protocol-Buffers gesetzt. Hiermit kann die Kommunikation zwischen Services optimiert und die Latenzzeit mitunter deutlich reduziert werden. Je mehr Services für die Verarbeitung eines einzelnen Client-Requests erforderlich sind, desto wichtiger werden solche Aspekte. Bisher haben wir IdeaFork auf Basis verschiedener Java EE Spezifikationen entwickelt und setzen dies auch in diesem Kapitel fort. Bestandteile von Java EE, wie bspw. JAX-RS, werden auch außerhalb der Plattform zum Aufbau von Applikationen mit Micro-Deployments verwendet. Daher folgen wir unserem bisherigen Weg und verzichten auf proprietäre Ansätze wie bspw. "gRPC".

 

8.1 EE oder nicht EE

Java EE Server galten lange Zeit als behäbig. Nur wenige Applikationsserver unterstützten Micro-Deployments direkt. Heutzutage stellt sich diese Situation anders dar. Es gibt kaum noch langsam startende Server, Server mit großem Ressourcenverbrauch oder Server ohne Embedded-Modus. Die Bestrebung nach aktuellen Laufzeitumgebungen und den damit verbundenen schnelleren Releasezyklen mündete schließlich in einer neuen Community und dem inoffiziellen Micro-Profile. Ursprünglich hat diese Initiative mit CDI, JAX-RS und JSON-P ein Subset von Java EE definiert. Mittlerweile gibt es darüber hinaus auch Spezifikationen, die außerhalb des JCPs umgesetzt wurden. Einige Teile, vor allem die Configuration-Spezifikation, sind aus Open-Source Projekten wie bspw. Apache DeltaSpike extrahiert worden und könnten zukünftig in Java EE einfließen. Andere Teile wie bspw. "Fault-Tolerance", "Health", "Metrics" und "JWT-Auth" sind aus API-Sicht größtenteils Neuentwicklungen und müssen erst den Prüfstand der Zeit bestehen.

 

Bei den Themen Cloud- und Micro-Deployments gibt es abseits von Java EE und dem Micro-Profile viele konkurrierende neue Ansätze. Wie üblich wird in Java EE nicht versucht eigene Wege durchzusetzen, sondern es werden bewährte Technologien spezifiziert. Aus diesem Grund konzentrieren wir uns in diesem Kapitel primär auf die in Java EE enthaltenen Spezifikationen und erweitern diese falls erforderlich.

8.2 IdeaForkMicro

Statt IdeaForkLite zu refactorn, werden wir die Applikation Schritt für Schritt komplett neu aufbauen. Einige Teile können wir unverändert übernehmen, während wir andere Teile noch schlanker gestalten und bei Bedarf um neue Konzepte erweitern werden. Da sich die Funktionalität von IdeaFork selbst nicht verändern wird, werden wir nur auszugsweise einige der Änderungen besprechen. Im UI-Teil wird es am wenigsten Änderungen geben. Unser Ziel ist es die bisherige Implementierung im UI soweit wie möglich zu übernehmen. Grundsätzlich erlaubt zwar auch JSF die Aufteilung in mehrere nahezu unabhängige Module, allerdings könnten wir dann nicht von Funktionalitäten wie bspw. dem Window-Scope profitieren. IdeaForkMicro wird somit aus einem monolitischen UI-Layer bestehen, welcher via REST mit den einzelnen unabhängigen Backend-Modulen kommuniziert. Jedes Modul speichert Daten in einer für das Modul optimierten Datenstruktur. Redundante Daten sind bei diesem Strukturierungskonzept nicht nur eine Begleiterscheinung, sondern ein "erwünschter" Effekt. Sowohl die Backend-Module als auch das UI-Modul werden in einem Git-Repository namens IdeaForkMicro abgelegt, in welchem die wichtigsten Schritte in einzelne Commits aufgeteilt sind.

 

Durch die vielfältigen Möglichkeiten bei Micro-Deployments, können wir uns in diesem Kapitel nur auf Teilaspekte beschränken. Dieser ist primär als Anregung gedacht und veranschaulicht unter anderem wie die Kommunikation zwischen Modulen mit Partial-Beans gekapselt und vereinfacht werden kann. Darüber hinaus werden wir neue Mechanismen von CDI 1.1 bis 2.0 in IdeaForkMicro einfließen lassen. Im Vergleich zu den bisherigen Kapiteln ist es daher nicht das Ziel möglichst viele Aspekte zu betrachten, welche unmittelbar in produktive Projekte übernommen werden können. Vielmehr hilft bspw. der Prototyp zur Inter-Modulkommunikation schnell und einfach mit verschiedenen Modulen zu experimentieren ohne sich vorab in Themen wie Service-Discovery- und Container-Lösungen einarbeiten zu müssen. Sollen in einem Projekt Micro-Deployments tatsächlich eingesetzt werden, dann wird man diese und weitere Themen nicht vermeiden können. Allerdings kann bspw. eine professionelle Service-Discovery-Lösung auf ähnliche Art und Weise via Partial-Beans integriert werden.

 

8.3 Module über Module

Wie genau eine Applikation in Module unterteilt wird ist eine Wissenschaft für sich und sowohl Meinungen als auch Empfehlungen gehen bei diesem vermeintlich einfachen Thema teilweise stark auseinander. Für uns sind zwei Fragen von zentraler Bedeutung. Die Frage nach der Aufteilung des UIs haben wir bereits vorweggenommen. Im Gegensatz zum SCS-Ansatz ("Self-Contained Systems") werden wir das IdeaFork -UI nicht auf einzelne Module aufsplitten. Die zweite Frage dreht sich um die Modulgrenzen der Backend-Module und ist etwas umfangreicher, da wir hier eine Antwort auf den Kommunikationsoverhead finden müssen. Damit wir diesen möglichst minimieren, werden wir die Kommunikation zwischen Modulen auf das absolute Minimum reduzieren. Gleichzeitig müssen wir dafür sorgen, dass der Ausfall eines Moduls nicht die komplette Applikation lahm legt. Dieser Aspekt darf nicht vernachlässigt werden, da die Remote-Kommunikation zwischen Modulen per Definition nicht nur die Latenzzeit erhöht, sondern auch zusätzliche Fehlerquellen mit sich bringt.

 

IdeaFork selbst ist eine recht kleine Applikation, wodurch sich die Modularisierungsmöglichkeiten in Grenzen halten. Ein offensichtlich eigenständiger Teil besteht aus der Benutzerregistrierung und dem Login. Das hierfür nötige Backend-Modul werden wir User-Service nennen. In IdeaForkLite haben wir zwar nur eine E-Mail Notifizierung am Ende des Registrierungsprozesses, dennoch lagern wir diese in ein eigenes Modul aus, welches asynchron aufgerufen wird. Die Verwaltung der Ideen übernimmt ebenfalls ein eigenes Modul namens Idea-Service. Sowohl User- als auch Idea-Service archivieren Änderungen asynchron über ein Modul, welches wir History-Service nennen. Sämtliche Module können Konfigurationen über das Config-Service beziehen. Durch den Kommunikationsoverhead werden wir hier jedoch auf ein zweistufiges Konzept zurückgreifen, welches zusätzlich den möglichen Ausfall des Config-Services kompensieren kann.

8.4 Am Puls der Zeit

Unabhängige Module erlauben die Verwendung verschiedener Technologiestacks. Für die Module von IdeaForkMicro definieren wir CDI und JAX-RS als absolutes Minimum. Das UI-Modul bleibt wie erwähnt nahezu unverändert und setzt somit weiterhin auf Java EE v6 bzw. v7. Allerdings wollen wir einen möglichst schnellen Start ermöglichen und gleichzeitig die Build-Konfiguration vereinfachen. Bisher konnte IdeaFork bzw. IdeaForkLite in TomEE, JBoss AS bzw. WildFly sowie GlassFish deployed werden. Jeder dieser Server ermöglicht die Umsetzung von proprietären Micro-Deployments. Daher könnten wir uns zumindest je Modul für einen Server explizit entscheiden. Der Applikationscode ist weiterhin durch die Java EE APIs portabel, allerdings benötigen wir zusätzlich einen proprietären Starter für den Server. In IdeaForkMicro werden wir hierfür je Modul eine Klasse verwenden, welche im Basisverzeichnis "dev-starter" abgelegt ist. Je Server könnten wir eine eigene Implementierung aus weniger als durchschnittlich 10 Zeilen Konfigurations- und Initialisierungscode umsetzen, der auf dem proprietären Deployment-API des jeweiligen Servers basiert. Wir entscheiden uns allerdings für zwei neue Server. Für das UI-Modul entschieden wir uns für JBoss WildFly-Swarm. Hierbei handelt es sich um einen modularen Server, welcher es ermöglicht nur bestimmte Teile von JBoss WildFly zu nutzen. Für die Backend-Module kommt Apache-Meecrowave zum Einsatz. Der Name Meecrowave ist eine Anspielung auf Micro-Deployments, hat aber mit der Micro-Profile Spezifikation nur wenig gemein. Falls erforderlich kann Meecrowave zu einem Micro-Profile konformen Server erweitert werden. Meecrowave selbst ist ein schlanker Server, welcher primär Apache Tomcat als Servlet-Container, Apache OpenWebBeans als CDI-Implementierung, Apache CXF als JAX-RS-Implementierung und Apache Johnzon für JSON-P (= JSON-Processing) kombiniert. Das Ergebnis ist ein performanter Server, welcher in wenigen Sekunden einsatzbereit ist. Ein weiterer für uns positiver Nebeneffekt ist die Unterstützung von CDI 2.0. Folglich können wir bspw. für asynchrone Events statt EJBs oder Akka direkt asynchrone CDI-Events verwenden.

8.5 Projektaufteilung

Statt einer Buildkonfiguration im Root-Verzeichnis des Projekts befinden sich in unserem Basis-Verzeichnis nur noch drei Ordner. Im Verzeichnis "ui" werden wir den leicht angepassten UI-Layer von IdeaForkLite ablegen, während wir im Verzeichnis "backend" je Service einen Ordner erstellen. Das "config" Verzeichnis ist der dritte Ordner und wird eine generische Erweiterung enthalten, welche einen einfachen Zugriff auf ein zentrales Config-Services ermöglicht.
Den Anfang stellt das User-Service dar. Im Verzeichnis backend/ideafork_user-service legen wir eine Buildkonfiguration an, welche neben den APIs für CDI 2.0, JAX-RS 2.0 und JPA 2.1 auch Dependencies für die entsprechenden Implementierungen und für Demozwecke "H2" als In-Memory Datenbank schrittweise definiert. Außerdem fügen wir Dependencies zu DeltaSpike-Core, -JPA und -Data hinzu. Abgesehen von etwas anderen Versionen entspricht dies einem Subset der bisherigen Dependencies von IdeaForkLite . Komplett neu ist die Dependency zu Meecrowave, welche wir mit org.apache.meecrowave als groupId und meecrowave-core als artifactId und provided als scope definieren. Diese Dependency benötigen wir für die Umsetzung einer Starter-Klasse, mit welcher wir die Applikation in einer Embedded-Instanz des Servers aus der IDE heraus starten können. Hierfür wird kein spezielles IDE-Plugin benötigt. Da wir ein unabhängiges Modul umsetzen, können wir problemlos von Java 7 auf Java 8 wechseln. Sowohl Meecrowave als auch DeltaSpike sind mit Java 8 kompatibel. Gäbe es andere Services mit Dependencies für die dies nicht der Fall ist, könnten wir notfalls für die entsprechenden Module auf eine ältere Java-Version zurückgreifen, bis ein Upgrade möglich ist.

8.6 Jeder Start ist einfach

Meecrowave kann auf verschiedene Arten gestartet werden. Neben einem Maven-Plugin stellt Meecrowave ein einfaches API zur Verfügung, um eine Embedded-Instanz zu starten. Hierfür legen wir im User-Service-Modul die Klasse DevUserServerStarter an. Die einfachste Variante, um den Server via Java-API zu starten ist:

 

 

new Meecrowave().bake().await();

 

 

Viel einfacher ist der Start eines EE-Micro Servers kaum noch möglich. Im Hinblick auf Micro-Deployments liegt hier bereits der erste Fallstrick verborgen. Im Gegensatz zu einem Monolithen möchten wir jedes Modul in einer eigenen Server-Instanz starten. Damit dies lokal möglich ist, muss jede Instanz einen eigenen Port verwenden. Ohne einer Container-Lösung wie bspw. Docker, müssen wir dem Server je Instanz einen entsprechenden Port vergeben. In unserem Fall laden wir den konfigurierten Port mit dem ConfigResolver von DeltaSpike.
public class DevUserServerStarter {
  public static void main(String[] args) {
    Meecrowave.Builder builder = new Meecrowave.Builder();
    String configuredPort =
      ConfigResolver.getPropertyValue("user-service.http.port");
      builder.setHttpPort(Integer.parseInt(configuredPort));
 
      new Meecrowave(builder).bake().await();
  }
}
Listing Meecrowave Starter-Klasse verdeutlicht, dass wir in einer herkömmlichen Main-Methode Meecrowave mit wenigen Zeilen anpassen und schließlich starten können. Den Wert für user-service.http.port können wir wie gewohnt in der Datei META-INF/apache-deltaspike.properties oder einer anderen aktiven Config-Source (von DeltaSpike) hinterlegen. In unserem Fall wollen wir den Port in der Build-Config hinterlegen und benötigen daher einen Platzhalter. Somit sieht die Konfiguration in META-INF/apache-deltaspike.properties wie folgt aus:

 

 

service.name=user-service

 

user-service.http.port=${app.http.port}

 

 

Der Wert von service.name ist die Basis für die Port-Konfiguration des Moduls. Der hier konfiguriere Wert dient als Prefix für die Portkonfiguration. Mit [prefix].http.port wird der Port eines Service-Moduls konfiguriert. Dadurch könnten wir sogar Portkonfigurationen in einer zentralen Config-Source (von DeltaSpike) ablegen. Der Platzhalter ${app.http.port} wird hier durch Maven ersetzt. Folglich fehlt an diese Stelle noch der Properties-Eintrag <app.http.port>8082</app.http.port> in der Build-Konfiguration. service.name spielt für uns an dieser Stelle eine untergeordnete Rolle. Wir werden in einem der nächsten Schritte allerdings eine CDI-Extension namens Remote-Access-Lite hinzufügen, welche genau diese Art der Port-Konfiguration voraussetzt.

8.7 Von Lite zu Micro

Die Basistechnologien und die grundsätzlichen Funktionen von IdeaForkLite ändern sich nicht. Für die Modularisierung sind jedoch kleine Änderungen notwendig, welche wir Schritt für Schritt durchführen. BaseEntity , User und PasswordManager könnnen wir ohne Änderungen übernehmen. Bei EntityManagerProducer ändern wir nur den Namen der Persistence-Unit, damit jedes Service-Modul einen eindeutigen Persistence-Unit Namen verwenden kann. Dies erleichtert die Zuordnung in der IDE ein wenig, falls wir mehrere Module parallel öffnen möchten. Da wir IdeaForkMicro auch gleichzeitig etwas schlanker gestalten wollen, reduzieren wir die verwendeten Interceptoren in UserRepository auf @org.apache.deltaspike.jpa.api.transaction.Transactional und vereinfachen die Umsetzung, welche in Listing Vereinfachtes User-Repository ersichtlich ist.
@Transactional(qualifier = Default.class)
@Repository
public interface UserRepository extends EntityRepository<User, String> {
  @Query(
    value = "select u from User u where u.nickName = ?1",
    singleResult = OPTIONAL)
  User loadByNickName(String nickName);
 
  @Query(
    value = "select u from User u where u.email = ?1",
    singleResult = OPTIONAL)
  User loadByEmail(String email);
}
Die Datei META-INF/beans.xml benötigen wir mit Meecrowave hingegen nicht zwingend. Seit CDI 1.1 ist diese Konfigurationsdatei optional und der Interceptor für @Transactional ist bereits durch DeltaSpike vorkonfiguriert. Ohne die Datei beans.xml werden nur Klassen zu CDI-Beans, wenn diese mit CDI-Annotationen annotiert sind, welche für die Definition von Beans gültig sind. Diese Annotationen werden auch "bean defining annotations" genannt. Hierzu zählen bspw. Scope-, Stereotype-, Interceptor- und Decorator-Annotationen. Aus diesem Grund können wir bei der Klasse BaseEntity auf @Exclude verzichten. Das eben beschriebene Default-Verhalten sorgt implizit dafür, dass diese Klasse und die abgeleiteten IdeaFork -Entities keine CDI-Beans sind.
Um die übernommenen Klassen auch testen zu können fügen wir neben JUnit auch org.apache.meecrowave:meecrowave-junit als Test-Dependency hinzu. Listing CDI-Test mit Meecrowave zeigt einen einfachen Testfall, mit dem wir die übernommene Logik testen können. Die Klasse MonoMeecrowave.Rule verwenden wir im Konstruktor von UserTest , um die definierten Injection-Points der Klasse durch Meecrowave befüllen zu lassen. Im Gegensatz zum bisher verwendeten CdiTestRunner von DeltaSpike muss dieser Schritt manuell durchgeführt werden. Später werden wir diese TestRule auch für Tests der JAX-RS Endpoints verwenden, welche wir im nächsten Schritt hinzufügen. Im Git-Repository von IdeaForkMicro sind neben dem Test aus Listing CDI-Test mit Meecrowave auch weitere Test-Methoden verfügbar.
public class UserTest {
  @ClassRule
  public static final MonoMeecrowave.Rule RULE =
    new MonoMeecrowave.Rule();
 
  @Inject
  private UserRepository userRepository;
 
  @Inject
  private PasswordManager passwordManager;
 
  public UserTest() {
    RULE.inject(this);
  }
 
  @Before
  public void init() {
    List<User> allUsers = userRepository.findAll();
    for (User user : allUsers) {
      userRepository.attachAndRemove(user);
    }
  }
 
  @Test
  public void createUser() {
    String password = passwordManager.createPasswordHash("xyz");
    User user = new User("gp@test.org", password);
 
    User savedUser = userRepository.save(user);
    Assert.assertEquals(user, savedUser);
 
    User loadedUser = userRepository.loadByEmail("gp@test.org");
 
    assertUser(user, updatedUser);
  }
 
  //...
}
Grundsätzlich geht es bei den Test-Methoden von UserTest darum die CDI-Beans direkt zu testen. Zu diesem Zeitpunkt wissen wir folglich, dass die übernommenen CDI-Beans wie erwartet funktionieren. Starten wir die Applikation allerdings mit unserer Starter-Klasse so stellen wir fest, dass unser neues Modul zwar startet aber nach außen noch keine Funktionalität zur Verfügung stellt und somit auch nicht verwendet werden kann. Um dies zu ändern fügen wir einen JAX-RS Endpunkt hinzu. Wie bei JAX-RS üblich beginnen wir mit einer Subklasse von javax.ws.rs.core.Application . In Listing Autom. auffindbare JAX-RS Applikation annotieren wir die Klasse UserApplication zusätzlich mit @javax.enterprise.context.Dependent . Die Verwendung von @Dependent oder auch @ApplicationScoped sorgt dafür, dass Meecrowave die Klasse findet und intern registriert. In unserem Fall besteht der Einstiegspfad direkt aus der Versionsnummer. Dies ist in IdeaForkMicro eindeutig, da eine Meecrowave-Instanz nur ein Modul in einer Version enthält.
@Dependent
@ApplicationPath("/v1/")
public class UserApplication extends Application {
}
Listing JAX-RS Resource zur User-Registrierung zeigt eine einfache JAX-RS Ressource, mit welcher neue User registriert werden können. In unserem Minimalbeispiel wird ein User mit E-Mail, Nickname und Passwort angelegt. Alle weitere Informationen sind optional und können mit einem Update-Request später übermittelt werden. Hierbei handelt es sich um keinen Best Practice Vorschlag, sondern hilft primär unsere Beispiele minimal zu halten und gleichzeitig verschiedene Konzepte zu illustrieren. Daher verzichten wir auch auf eine zusätzliche Ebene zur Kapselung der Registrierungslogik und setzen die Logik direkt im REST-Endpunkt um. Hätten wir neben dem REST-API noch weitere Technologien für Endpunkte, dann wäre eine solche Kapselung natürlich zu bevorzugen, damit die eigentliche Logik nicht mehrfach umgesetzt werden muss.
@Path("registration")
@ApplicationScoped
public class SimpleRegistrationResource {
  @Inject
  private UserRepository userRepository;
 
  @Inject
  private PasswordManager passwordManager;
 
  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  public Response register(RegistrationRequest registrationRequest,
                           @Context UriInfo uriInfo) {
 
    if (userRepository.loadByEmail(
      registrationRequest.getEmail()) == null) {
        String passwordHash = passwordManager
          .createPasswordHash(registrationRequest.getPassword());

        User userToRegister = new User(
          registrationRequest.getEmail(), passwordHash);
        userToRegister.setNickName(registrationRequest.getNickName());

        User savedUser = userRepository.save(userToRegister);
        User registeredUser = userRepository.findBy(savedUser.getId());
 
        if (registeredUser != null) {
          return Response.created(uriInfo.getBaseUriBuilder().build())
            .entity(new PublicUserResponse(savedUser, true))
            .type(MediaType.APPLICATION_JSON_TYPE).build();
        }
    }
 
    return Response.status(Response.Status.CONFLICT).build();
  }
}
Die POST-Methode aus Listing JAX-RS Resource zur User-Registrierung nimmt RegistrationRequest als ersten Parameter entgegen. Bei dieser Klasse handelt es sich um ein einfaches Java-Bean mit den erforderlichen Properties zur Übermittlung der User-Daten. Meecrowave konvertiert valide JSON-Strings in Instanzen dieser Klasse, sofern dies möglich ist. Für die Antwort an den REST-Client könnten wir direkt die User -Instanz verwenden, da auch diese autom. in einen JSON-String konvertiert wird. Dies hätte im konkreten Beispiel den Nebeneffekt, dass wir den Password-Hash an den Client senden würden. Informationen wie diese können auf verschiedene Arten bei Bedarf gefiltert werden. Wir verwenden hierfür die Klasse PublicUserResponse . Diese definiert die gleichen Properties wie die Klasse User mit Ausnahme der Passwort-Property. Des Weiteren kann explizit gesteuert werden, ob die E-Mail Adresse in den Response aufgenommen werden soll. Dieser Umstand ermöglicht sogar dynamische Limitierungen zur Laufzeit. Möchten wir darüber hinaus in bestimmten Situationen den Response anpassen, so verwenden wir wie in Listing JAX-RS Resource zur User-Registrierung die Klasse javax.ws.rs.core.Response als Rückgabetyp. Ein Beispiel hierfür ist die Änderung des Status-Codes auf Response.Status.CONFLICT , falls es bereits einen registrierten User mit der übermittelten E-Mail Adresse geben sollte.
Starten wir unser User-Service erneut, so werden nicht nur unsere CDI-Beans deployed, sondern auch unsere neue JAX-RS Ressource. Meecrowave gibt alle aktiven URIs während des Startprozesses aus. Somit ist der erste Teil unseres User-Service-Moduls komplett und wir können einen Test für die eben definierte REST-Schnittstelle hinzufügen. Diese Tests sammeln wir in der Klasse UserWorkflowTest . Auch hier hilft uns MonoMeecrowave.Rule . Statt wie bisher CDI-Beans direkt in unsere Test-Klasse zu injizieren, nutzen wir diese Test-Rule in Listing JAX-RS Test mit Meecrowave für den Zugriff auf den aktuellen Port des Containers. In der @Before Callback-Methode greifen wir weiterhin direkt auf unser UserRepository zu, um die gespeicherten User vor jedem Test zu löschen. Hier könnten wir ebenfalls über einen REST-Endpunkt gehen. Darauf verzichten wir in unserem Fall, weil wir, abgesehen von Tests, keine Verwendung für einen solchen Endpunkt hätten. Sämtliche Test-Methoden verwenden jedoch strikt das definierte API unserer REST-Ressource bzw. der Ressourcen, die wir darüber hinaus definieren und testen. Auf die Details der Test-Methoden werden wir nicht genauer eingehen, da hier primär die Standard-Client-API von JAX-RS verwendet wird.
public class UserWorkflowTest {
  @ClassRule
  public static final MonoMeecrowave.Rule RULE =
    new MonoMeecrowave.Rule();
 
  private static WebTarget userRegistrationTarget;
  private static Client client;
 
  @BeforeClass
  public static void createTarget() {
    client = ClientBuilder.newClient();
 
    int testHttpPort = RULE.getConfiguration().getHttpPort();
    createUserRegistrationTarget(testHttpPort);
  }
 
  @AfterClass
  public static void onShutdown() {
    client.close();
  }
 
  private static void createUserRegistrationTarget(int testHttpPort) {
    String applicationPath =
      UserApplication.class.getAnnotation(ApplicationPath.class).value();
    String userRegistrationPath =
      SimpleRegistrationResource.class.getAnnotation(Path.class).value();
    String baseUserUrl = "http://localhost:" + testHttpPort +
      applicationPath + userRegistrationPath;
    URI uri = UriBuilder.fromUri(baseUserUrl).build();
    userRegistrationTarget = client.target(uri);
  }

  @Before
  public void init() {
    UserRepository userRepository =
      BeanProvider.getContextualReference(UserRepository.class);

    List<User> allUsers = userRepository.findAll();
    for (User user : allUsers) {
      userRepository.attachAndRemove(user);
    }
  }
 
  @Test
  public void registerUser() {
    User user = new User();
    user.setEmail("gp@test.org");
    user.setPassword("xyz");
 
    Response response = userRegistrationTarget.request()
      .buildPost(Entity.json(user)).invoke();
 
    Assert.assertNotNull(response);
    Assert.assertEquals(CREATED.getStatusCode(), response.getStatus());
    User createdUser = response.readEntity(User.class);
 
    Assert.assertEquals("gp@test.org", createdUser.getEmail());
  }
}
Die bisher beschriebene Funktionalität zur Registrierung neuer Benutzer soll ohne Authentifizierung zur Verfügung stehen. Im nächsten Schritt implementieren wir eine REST-Ressource, welche von der Login-Seite verwendet werden soll. In den bisherigen Kapiteln haben wir nach erfolgreicher Anmeldung den aktuellen Benutzer im ActiveUserHolder -Bean gespeichert und geschützte Service-Methoden nur aufgerufen, wenn die Methode #isLoggedIn den Wert true zurückgeliefert hat. Dies war möglich, weil Services nicht in eigenständigen Modulen gekapselt waren. Da wir in diesem Kapitel Services in verschiedene Module verschieben, müssen diese anders gesichert werden. Hierfür gibt es verschiedene Möglichkeiten. Ohne auf die Vor- und Nachteile einzugehen verwenden wir in IdeaForkMicro JWT (JSON Web Token). Um die JWT-Integration nicht selbst umsetzen zu müssen, greifen wir auf einen kleinen Aufsatz für JAX-RS namens JWT-Authentication-Lite zurück. Das API dieses Prototypen besteht aus zwei Annotationen und einer Klasse, welche sehr einfach anzuwenden sind. Services die nur mit erfolgreicher Authentifizierung verwendbar sein sollen, werden zusätzlich mit der Annotation @AuthenticationRequired markiert. Außerdem müssen alle Services das gleiche Shared-Secret verwenden, welches wir unter dem Key jwt_secret in einer gültigen Config-Source (von DeltaSpike) hinterlegen. In IdeaForkMicro verwenden wir hierfür die Datei META-INF/apache-deltaspike.properties . Allerdings benutzen wir als Wert nur einen Platzhalter, der durch Maven ersetzt wird. Somit könnten wir bspw. über eine Build-Pipeline das Shared-Secret je Stage automatisiert anpassen. Alternativ könnten wir natürlich die Konfiguration bspw. mit System-Properties durchführen.

 

Die Login -Ressource stellt einen Spezialfall dar, weil hier der JWT-Token im Hintergrund erzeugt werden muss. Dies können wir mit Hilfe der zweiten Annotation namens @LoginEntryPoint veranlassen. Listing Simple Login zeigt eine einfach Umsetzung der Login -Ressource.
@Path("user-action")
@ApplicationScoped
public class SimpleLoginResource {
  @Inject
  private UserRepository userRepository;
 
  @Inject
  private PasswordManager passwordManager;
 
  @Inject
  private IdentityHolder identityHolder;
 
  @LoginEntryPoint
  @POST
  @Path("/login")
  @Consumes(MediaType.APPLICATION_JSON)
  public Response login(LoginRequest loginRequest,
                        @Context UriInfo uriInfo) {
 
    if (loginRequest.getEmail() == null ||
        loginRequest.getPassword() == null) {
      return Response.status(
        Response.Status.BAD_REQUEST.getStatusCode()).build();
    }
 
    User loadedUser = userRepository.loadByEmail(loginRequest.getEmail());
 
    if (loadedUser == null) {
      return Response.status(
        Response.Status.BAD_REQUEST.getStatusCode()).build();
    }
 
    String passwordHash = passwordManager
      .createPasswordHash(loginRequest.getPassword());
 
    if (passwordHash.equals(loadedUser.getPassword())) {
      try {
        identityHolder.setAuthenticatedEMail(loginRequest.getEmail());
 
        return Response.ok().build();
      } catch (Exception e) {
        return Response.status(
          Response.Status.INTERNAL_SERVER_ERROR).build();
      }
    }
    return Response.status(Response.Status.UNAUTHORIZED).build();
  }
}
 
public class LoginRequest {
    private String email;
    private String password;
 
    //+ getters and setters
}
Die Beans UserRepository und PasswordManager kennen wir bereits aus den vergangenen Kapiteln. Beide wurden ohne Änderung aus IdeaForkLite übernommen. IdentityHolder ist hingegen ein Request-Scoped Bean, welches in JWT-Authentication-Lite enthalten ist. Wird für die übermittelte E-Mail Adresse ein gespeicherter User gefunden und stimmt der Passwort-Hash ebenfalls überein, dann können wir die E-Mail Adresse an IdentityHolder weitergeben. Im Hintergrund wird mit dieser E-Mail Adresse ein JWT-Token erzeugt, welcher in den HTTP-Header übernommen wird. Bei jedem Folgerequest an JAX-RS Ressourcen, die durch @AuthenticationRequired geschützt sind, muss dieser Token erneut übermittelt werden. JWT-Authentication-Lite verifiziert bei jedem Request den JWT-Token mit Hilfe des Shared-Secrets. Als Metadaten enthält der Token die E-Mail Adresse und einen Timestamp für die zeitliche Begrenzung der Gültigkeit. Nur wenn ein Token erfolgreich verifiziert wurde und darüber hinaus noch gültig ist, wird die E-Mail Adresse für diesen Request in das IdentityHolder -Bean übernommen. Ähnlich wie bei einer HTTP-Session wird die Gültigkeit verlängert, wenn innerhalb eines gewissen Zeitfensters ein Token verwendet wurde. Hierbei ist allerdings zu beachten, dass autom. ein neuer Token mit einer neuen Gültigkeitszeit ausgestellt wird. Für Clients macht dies jedoch keinen Unterschied, da sie im Normalfall immer den übermittelten Token verwenden sollten und der genaue Inhalt nicht relevant ist, da der Token unverändert beim nächsten Request erneut mitgesendet werden muss. Im Falle von IdeaForkMicro werden Token nur bei der Kommunikation zwischen verschiedenen Services übermittelt. Hätten wir bspw. eine JavaScript Applikation, welche im Browser ausgeführt wird und direkt mit den REST-Ressourcen kommunizieren würde, dann wäre eine zusätzliche Absicherung wie bspw. HTTPS statt HTTP ratsam. Da wir für das IdeaFork -UI bei JSF bleiben, ist dies nicht zwingend für die ganze Applikation erforderlich. Vor allem wenn nach außen nur der Server auf dem das IdeaFork -UI deployed wird erreichbar ist. Nach diesem kurzen JWT-Exkurs sieht es danach aus als wäre IdeaForkMicro komplexer als IdeaForkLite , da wir uns in IdeaForkLite bspw. um keine Token kümmern mussten. Dies ist eine der Konsequenzen bei der Aufteilung in modulare Services und hat unmittelbar nichts mit den Eigenschaften von CDI oder JAX-RS zu tun. Abgesehen von der möglichen Skalierbarkeit je Service werden wir im Laufe des Kapitels auch noch weitere Vorteile einer solchen Modularisierung sehen. Am Ende des Kapitels werden wir die eben vorgestellten Endpunkte im UI-Modul einbinden. Vorerst fügen wir Tests für unsere Login-Logik hinzu. Listing Simples Login Test-Target zeigt die Methode zum Erzeugen des Login-Targets, welche wir zur Klasse UserWorkflowTest hinzufügen und in der Methode #createTarget aufrufen.
private static void createLoginTarget(int testHttpPort) {
  String applicationPath =
    UserApplication.class.getAnnotation(ApplicationPath.class).value();
  String loginPath =
    SimpleLoginResource.class.getAnnotation(Path.class).value();
 
  String baseUserUrl = "http://localhost:" + testHttpPort +
    applicationPath + loginPath + "/login";
  URI uri = UriBuilder.fromUri(baseUserUrl).build();
  loginTarget = client.target(uri);
}
Die grundlegende Logik dieser Methode haben wir bereits für #createUserRegistrationTarget verwendet. Der Hauptunterschied ist, dass wir den letzten Teil des Pfades fix vorgeben. Würden wir in SimpleLoginResource diesen Teil des Pfades später ändern, dann müssten wir die Änderung in jedem Test nachziehen. Listing Dynamisches Login Test-Target zeigt, wie sich dies mit ein paar zusätzlichen Zeilen verhindern lässt, indem dynamisch nach @LoginEntryPoint gesucht wird.
private static void createLoginTarget(int testHttpPort) {
  String applicationPath =
    UserApplication.class.getAnnotation(ApplicationPath.class).value();
  String loginPath =
    SimpleLoginResource.class.getAnnotation(Path.class).value();
 
  for (Method method : SimpleLoginResource.class.getDeclaredMethods()) {
    if (method.isAnnotationPresent(LoginEntryPoint.class)) {
      Path path = method.getAnnotation(Path.class);
      if (path != null) {
        loginPath += path.value();
        break;
      }
    }
  }
 
  String baseUserUrl = "http://localhost:" + testHttpPort +
    applicationPath + loginPath;
  URI uri = UriBuilder.fromUri(baseUserUrl).build();
  loginTarget = client.target(uri);
}
Mithilfe von loginTarget können wir in unseren Tests nach erfolgreicher Registrierung einen Request an die Login-Ressource absetzen. In Listing User-Login Test wird im letzten Schritt überprüft, ob ein Token ausgestellt wurde. Auch hierfür verwenden wir die standardmäßige Client-API von JAX-RS.
@Test
public void loginUser() {
  registerUser();
  User user = new User("gp@test.org", "xyz");
 
  Response response =
    loginTarget.request().buildPost(Entity.json(user)).invoke();
  String token = response.getHeaderString(HttpHeaders.AUTHORIZATION);
  Assert.assertNotNull(token);
}
 
private Response registerUser() {
  User user = new User();
  user.setEmail("gp@test.org");
  user.setPassword("xyz");
 
  Response response = userRegistrationTarget.request()
    .buildPost(Entity.json(user)).invoke();
 
  Assert.assertNotNull(response);
  Assert.assertEquals(CREATED.getStatusCode(), response.getStatus());
  User createdUser = response.readEntity(User.class);
 
  Assert.assertEquals("gp@test.org", createdUser.getEmail());
  return response;
}
Listing Test eines invaliden Logins testet schließlich noch einen Login-Request mit falschem Passwort. In diesem Fall wird HTTP-401 (Unauthorized) von unserer JAX-RS Ressource zurückgesendet.
  @Test
  public void failedLogin() {
    registerUser();
 
    User user = new User("gp@test.org", "wrong");
 
    Response response = loginTarget.request()
      .buildPost(Entity.json(user)).invoke();
    Assert.assertNotNull(response);
    Assert.assertEquals(
      UNAUTHORIZED.getStatusCode(), response.getStatus());
}

8.8 Asynchrone CDI-Events

Bis zu diesem Punkt hätten wir die gesamte Funktionalität mit jedem beliebigen EE v6 bzw. v7 Server umsetzen können. Wie eingangs erwähnt ist das Ziel von Initiativen wie dem Micro-Profile und Meecrowave nicht nur eine möglichst effiziente Laufzeitumgebung zur Verfügung zu stellen, sondern auch eine möglichst aktuelle. Im Falle von CDI wurde Version 2.0 mehrere Monate vor Java EE 8 finalisiert und produktiv verwendbare Implementierungen standen innerhalb weniger Wochen zur Verfügung. Bis jedoch sämtliche EE-Server alle Spezifikationen einer neuen EE-Version integriert haben, vergeht üblicherweise nochmal deutlich mehr Zeit. EE 8 verspricht diesen oft kritisierten Aspekt etwas zu entkräften, da der Umfang bewusst kleiner als bspw. bei EE 6 gehalten wurde. Mit Servern wie Meecrowave haben wir allerdings den Vorteil am Puls der Zeit zu bleiben und neue Features bereits kurz nach der Fertigstellung der Spezifikation verwenden zu können. In IdeaForkMicro nutzen wir diesen Umstand, um asynchrone Events mit den neuen Boardmitteln von CDI 2.0 zu implementieren. Hierfür verwenden wir in Listing Asynchrone CDI-Events feuern wie gewohnt einen Injection-Point vom Typ javax.enterprise.event.Event . Statt #fire rufen wir jedoch die neue Methode #fireAsync auf. Etwas später werden wir sehen, warum wir in Listing Asynchrone CDI-Events feuern den aktuellen Token an die UserChangedEvent -Instanz übergeben müssen.
@ApplicationScoped
public class UserChangeBroadcaster {
  @Inject
  private AuthenticationManager authenticationManager;
 
  @Inject
  private Event<UserChangedEvent> userChangedEvent;
 
  @Inject
  private IdentityHolder identityHolder;
 
  public void onUserChange(User user) {
    try {
      if (user.getVersion() == 0) {
        String tmpToken =
          authenticationManager.createNewToken(user.getEmail());
        userChangedEvent.fireAsync(
          new UserRegistrationEvent(user, tmpToken));
      } else {
        userChangedEvent.fireAsync(
          new UserChangedEvent(user, identityHolder.getCurrentToken()));
      }
    } catch (Exception e) {
      throw ExceptionUtils.throwAsRuntimeException(e);
    }
  }
}
Der zweite Unterschied ist auf der Observer-Seite zu finden. Wie in Listing CDI-Observer für asynchrone Events zu sehen ist, muss bei einem Observer für asynchrone CDI-Events @ObservesAsync statt @Observes eingesetzt werden. Somit können Event-Klassen sowohl für synchrone als auch asynchrone Events verwendet werden. Erst durch die entsprechende fire -Methode und durch die auf der Observer-Seite dazu passende Annotation wird die Unterscheidung getroffen. Wird zu einem späteren Zeitpunkt bspw. ein synchrones Event auf ein asynchrones umgestellt, so genügt es eben nicht nur den Methodenaufruf auf #fireAsync umzustellen. Wird die Umstellung auf @ObservesAsync ausgelassen, so werden Events nicht mehr zugestellt, weil es dann keine Methoden mit @ObservesAsync gibt. Würden wir sowohl #fireAsync als auch #fire nacheinander aufrufen, können wir auf der Observer-Seite wahlweise @ObservesAsync oder @Observes verwenden ohne auf die verwendete fire -Methode Rücksicht nehmen zu müssen.
@ApplicationScoped
public class UserActivityObserver {
  @Inject
  private UserActionRepository userActionRepository;

  public void onUserActionEvent(
    @ObservesAsync UserActionEvent userActionEvent) {
      userActionRepository.save(userActionEvent.getUserAction());
  }
}
In den nachfolgenden Abschnitten wird es noch weitere Hinweise zu CDI 2.0 geben. Asynchrone Events und die etwas später verwendete Priorisierung von Events stellen für IdeaForkMicro die interessanteste Neuheit in CDI 2.0 dar.

8.9 Entfernt und doch so nahe

Bisher haben wir alle beschriebenen Schritte im User-Service-Modul umgesetzt. Wie eingangs erwähnt wird es in IdeaForkMicro mehrere Module geben. Die nächsten Service-Module sind Notification-Service zum Versenden von E-Mail Benachrichtigungen und das Archive-Service, über welches wir Änderungen an Entitäten separat archivieren. Beide Service-Module sind zu diesem Zeitpunkt noch nicht umgesetzt. Die Schnittstelle der Module ist allerdings schon absehbar. Bisher haben wir RegistrationRequest bereits im Endpunkt SimpleRegistrationResource verwendet. Dieses Java-Bean definiert die Properties email , nickName , firstName , lastName und password . Nur letztere ist in der Klasse RegistrationRequest selbst definiert. Die restlichen Properties sind in einer Basisklasse namens UserRequest enthalten. UserRequest kann somit auch für die Nachricht an das Notification-Service wiederverwendet werden. Listing REST-Resource Client als Partial-Bean zeigt wie wir das noch zu erstellende Notification-Service ansprechen wollen.
@ApplicationScoped
@ResourceClient(name = "notifications", version = "v1")
public interface NotificationResource {
  @POST
  @Path("/welcome")
  void sendWelcomeMessage(UserRequest notificationRequest);
}
Die Annotation @ResourceClient aus Listing REST-Resource Client als Partial-Bean wird durch die CDI-Extension Remote-Access-Lite zur Verfügung gestellt und basiert auf dem Partial-Bean Konzept von DeltaSpike. Das Interface NotificationResource können wir wie ein herkömmliches CDI-Bean injizieren und verwenden. Im konkreten Beispiel wird bei einem Aufruf von NotificationResource#sendWelcomeMessage das Service mit dem Namen "notifications" in Version "v1" gesucht und an "/welcome" ein POST-Request gesendet. Bei @POST und @Path handelt es sind um die bereits bekannten JAX-RS Annotationen. Folglich ist nur @ResourceClient eine eigene Annotation, welche einen einfacheren Zugriff auf Remote-Services ermöglicht. Im Hintergrund wird die Adresse des Services festgestellt, mit den Pfadangaben kombiniert und der Request über das JAX-RS Client-API veranlasst. Damit dies möglich ist, registriert sich beim Applikationsstart jedes Modul in einer dezentral verteilten Datenstruktur. Für jeden REST-Endpunkt wird ein Eintrag angelegt, welcher automatisch an alle teilnehmenden Module repliziert und in regelmäßigen Abständen aktualisiert wird. Obwohl wir keine Containerlösung wie bspw. Docker verwenden, müssen wir Informationen anderer Module, wie bspw. Ports, auf diese Weise nicht explizit kennen, weil diese Informationen im Hintergrund ebenfalls automatisch registriert und zwischen Service-Instanzen repliziert werden. Mit diesem Wissen ausgestattet können wir in Listing Remote-Archivierung via Partial-Bean unverzüglich mit der Integration des Archive-Moduls fortsetzen. In diesem Fall ist EntityChangeRequest ein Java-Bean mit den Properties entityAsJson , id , version und einer Readonly Property creationTimestamp , welches nur für Nachrichten an das Archive-Service verwendet wird.
@ApplicationScoped
@ResourceClient(name = "archive", version = "v1")
public interface ArchiveResource {
  @POST
  void recordChange(EntityChangeRequest entityChangeRequest);
}
Ist ein Service zur Laufzeit nicht verfügbar, dann bricht die CDI-Extension die Verarbeitung ab und es wird eine Warnung gelogged. Ein umfangreicheres Error-Handling ist durchaus möglich, aber nicht Teil der Remote-Access-Lite Extension, da es sich hier um einen Prototypen handelt der primär dazu dienen soll, dass wir keine zusätzliche Container- oder Service-Discovery-Lösung für IdeaForkMicro benötigen. Darüber hinaus zeigt diese CDI-Extension erneut die vielfältigen Einsatzmöglichkeiten von Partial-Beans und anderer Mechanismen von Apache DeltaSpike und kann als Inspiration für weitere CDI-Extensions dienen. Durch die eben gezeigten Partial-Beans NotificationResource und ArchiveResource können wir die Anbindung an diese Module bereits im User-Service-Modul umsetzen als hätten wir die beiden anderen Module bereits zur Verfügung. Listing Manuell übertragener Token zeigt den Aufruf von NotificationResource#sendWelcomeMessage und Listing Asynchrone Archivierung die Verwendung von ArchiveResource#recordChange . In beiden Listings erfolgt der Aufruf in einer asynchronen Observer-Methode. In der Methode onUserRegisteredEvent muss der aktuelle Token manuell gesetzt werden, weil Token nach der Überprüfung in einem Thread nur in diesem automatisch übernommen werden. Asynchrone Observer-Methoden werden hingegen durch einen anderen Thread ausgeführt und daher greift der Automatismus an dieser Stelle nicht. Der so gesetzte Token wird anschließend wieder automatisch verarbeitet. Konkret wird er für den Aufruf von NotificationResource#sendWelcomeMessage verwendet. Somit ist dies nur erforderlich, wenn wir in einem neuen Thread ein anderes Service-Modul ansprechen möchten.
@ApplicationScoped
public class RegistrationNotificationObserver {
  @Inject
  private NotificationResource notificationResource;
 
  @Inject
  private IdentityHolder identityHolder;
 
  public void onUserRegisteredEvent(
    @ObservesAsync UserRegistrationEvent userRegistrationEvent) {
      identityHolder.setCurrentToken(userRegistrationEvent.getToken());
      String userSpecificText =
        Optional.ofNullable(userRegistrationEvent.getUser().getNickName())
          .orElse(userRegistrationEvent.getUser().getEmail());
      UserRequest userRequest = new UserRequest();
      userRequest.setNickName(userSpecificText);
      this.notificationResource.sendWelcomeMessage(userRequest);
  }
}
Listing Asynchrone Archivierung zeigt eine ähnliche Konstellation. Allerdings wird hier manuell der ObjectMapper von Jackson verwendet, um den aktuellen Zustand der User -Instanz in JSON zu serialisieren. Folglich muss die checked Exception JsonProcessingException explizit behandelt werden. Auch hier genügt es in unserem vereinfachten Fall, dass wir die checked Exception in eine unchecked Exception umwandeln und weiterwerfen.
@ApplicationScoped
public class UserChangeObserver {
  @Inject
  private IdentityHolder identityHolder;
 
  @Inject
  private ArchiveResource archiveResource;
 
  public void onUserChange(
    @ObservesAsync UserChangedEvent userChangedEvent) {
      this.identityHolder.setCurrentToken(userChangedEvent.getToken());
 
      ObjectMapper objectMapper = new ObjectMapper();
 
      try {
        User user = userChangedEvent.getUser();
        EntityChangeRequest entityChangeRequest =
          new EntityChangeRequest();
        entityChangeRequest.setId(user.getId());
        entityChangeRequest.setEntityAsJson(
          objectMapper.writeValueAsString(user));
        entityChangeRequest.setVersion(user.getVersion());
 
        archiveResource.recordChange(entityChangeRequest);
      } catch (JsonProcessingException e) {
        throw ExceptionUtils.throwAsRuntimeException(e);
      }
  }
}
Damit Änderungen, die mit dem ArchiveResource -Partial-Bean verschickt werden auch ankommen und verarbeitet werden, erstellen wir ein Modul namens History-Service. In diesem Modul archivieren und verwalten wir Änderungen an Entitäten. Grundsätzlich ist dieses Modul äquivalent zum User-Service Modul aufgebaut. Sowohl Konfiguration, Starterklasse als auch ein JAX-RS Endpunkt werden hier nach dem zuvor beschriebenen Vorgehen umgesetzt. Listing JAX-RS Endpunkt zur Archivierung zeigt, dass auch hier die JAX-RS Ressource denkbar einfach umzusetzen ist. Die mit @POST annotierte Methode #archiveEntity konvertiert die übermittelte Instanz vom Typ EntityChangeRequest in ein JPA-Entity vom Typ EntityChange und speichert diese mit Hilfe des injizierten EntityChangeRepository -Instanz.
@AuthenticationRequired
@Path("archive")
 
@ApplicationScoped
public class EntityArchiveResource {
  @Inject
  private EntityChangeRepository entityChangeRepository;
 
  @POST
  public void archiveEntity(EntityChangeRequest entityChangeRequest) {
    EntityChange entityChange = new EntityChange(
      entityChangeRequest.id,
      entityChangeRequest.version,
      entityChangeRequest.entityAsJson,
      entityChangeRequest.creationTimestamp);
 
      entityChangeRepository.save(entityChange);
  }

  //...
}
Allerdings zeigt sich bei der Modularisierung, dass sich bereits jetzt gewisse Codeduplizierungen einschleichen. Da Module eigenständig sind und auch eigene Konzepte verfolgen können, wird oft empfohlen keinen geteilten Code zu verwenden. In unserem Fall führt dies dazu, dass jedes Modul in welchem wir JPA verwenden eine Kopie der Klasse BaseEntity enthält. Generische Libraries könnte man wie andere Dependencies zwischen Modulen teilen. In IdeaForkMicro verzichten wir in diesem Fall jedoch auf die Wiederverwendbarkeit dieser einen Klasse.

8.10 Diversität je Modul

Neben dem ArchiveResource -Partial-Bean haben wir auch ein Partial-Bean namens NotificationResource bereits eingebunden. Aktuell fehlt nur noch das dazugehörige Notification -Modul. Dieses legen wir im nachfolgenden Schritt an. Auch hier erstellen wir eine Klasse für den einfachen manuellen Start und nennen diese DevNotificationServerStarter . Wie bereits erwähnt sind die Starter-Klassen für Meecrowave gleich aufgebaut. Der einzige Unterschied liegt in der Konfiguration des Servicenamens und des Ports. Eine Besonderheit im Notification-Service Modul gibt es dennoch. Wir übernehmen aus IdeaForkLite die Integration von Spring-Mail, welche mit Hilfe unserer CDI-Spring-Bridge umgesetzt wurde. Die Funktionalität des Moduls stellen wir, wie auch beim User-Service Modul, mit einem JAX-RS Endpunkt für andere Teile der Applikation zur Verfügung. Listing JAX-RS Endpunkt zum Versenden von E-Mails zeigt die Injizierung und Verwendung von MailService , welches wie gehabt das Spring-Bean SpringMailSender verwendet.
@AuthenticationRequired
@Path("notifications")
 
@ApplicationScoped
public class NotificationResource {
  @Inject
  private IdentityHolder identityHolder;
 
  @Inject
  private MailService mailService;
 
  @POST
  @Path("/welcome")
  public void onNotification(RecipientDetails recipientDetails) {
    mailService.sendWelcomeMessage(recipientDetails.nickName);
  }
}
Da wir Spring-Mail bzw. Spring nur in diesem Modul verwenden, können wir die damit verbundene Build-Konfiguration und Funktionalität auf dieses Modul beschränken. Des Weiteren mussten wir bisher SpringMailSender mit @Exclude explizit für den CDI-Container entfernen, da zur Laufzeit Spring dieses Bean verwalten soll. Seit CDI 1.1 könnten wir hierfür auch @javax.enterprise.inject.Vetoed verwenden. Alternativ können wir die Datei META-INF/beans.xml weglassen oder in dieser Datei den Tag beans um das Attribut bean-discovery-mode erweitern und statt "all" den Wert "annotated" verwenden. In beiden Fällen werden, wie am Anfang des Kapitels erklärt, nur Klassen mit den sogenannten "bean defining annotations" berücksichtigt. Ebenfalls in der Datei beans.xml steht eine weitere Alternative zur Verfügung. Durch die Tags scan und exclude können Teile des BDAs via Konfiguration ausgelassen werden. CDI 2.0 geht sogar noch einen Schritt weiter und führt den <trim/> -Tag als einfachen Marker-Tag ein. Mit diesem werden Beans für den CDI-Container erst exkludiert, falls sie nach dem Startprozess weder durch eine explizite (CDI-)Annotation noch dynamisch durch eine CDI-Extension zu einem CDI-Bean gemacht wurden.

 

An diesem Punkt haben wir bereits drei Module, wobei das User-Service Modul mit den beiden anderen Modulen kommuniziert. Über den IdentityHolder haben wir auf den aktuellen Token zugegriffen bzw. nach einem erfolgreichen Login die authentifiziert E-Mail Adresse für den aktuellen Thread gesetzt. Diese E-Mail Adresse wird auch bei nachfolgenden Requests mit JWT-Token durch den IdentityHolder zur Verfügung gestellt sobald der mitgesendete Token erfolgreich verifiziert wurde. Listing Aktuelle User-Details laden zeigt dies anhand der UserActionResource , welche im User-Service Modul hinzugefügt wird.
@AuthenticationRequired
@Path("user-action")
 
@ApplicationScoped
public class UserActionResource {
  @Inject
  private UserRepository userRepository;
 
  @Inject
  private UserActionRepository userActionRepository;
 
  @Inject
  private IdentityHolder identityHolder;
 
  @GET
  public UserActionResponse loadCurrentUserDetails() {
    User user = userRepository.loadByEmail(
      identityHolder.getAuthenticatedEMail());
 
    List<UserAction> result = Optional
      .ofNullable(userActionRepository.loadLatestActivities(user, 10))
      .orElse(emptyList());
    List<UserActionEntry> userActionEntryList =
      result.stream().map(UserActionEntry::new).collect(toList());
    return new UserActionResponse(user, userActionEntryList);
  }
}
In der Methode #loadCurrentUserDetails verwenden wir den Wert, der von IdentityHolder#getAuthenticatedEMail zurückgegeben wird, um den entsprechenden User zu laden und im nächsten Schritt können wir zusätzlich die User-Aktionen für diesen User laden. Sämtliche dieser User-Details werden am Ende des Kapitels im User-Profil Bereich von IdeaForkLite angezeigt. User-Aktionen beinhalten bis zu diesem Punkt nur User-Logins. Das Git-Repository von IdeaForkMicro enthält im User-Service Modul außerdem eine ähnlich aufgebaute SimpleLogoutResource Klasse, welche zusätzlich User-Logouts via REST-Schnittstelle entgegennimmt und intern ein asynchrones UserActionEvent weiterleitet, welches schließlich dazu führt, dass der UserActivityObserver auch diese Events speichert. Das User-Service Modul selbst ist zustandslos, wodurch ein expliziter Logout keine weiteren Auswirkungen hat. Im UI-Teil der Applikation, zu dem wir etwas später kommen werden, sieht dies anders aus. Hier muss bei einem Logout zumindest der JWT-Token verworfen werden. Damit wir Logout-Events im User-Profil zusätzlich anzeigen können, muss der zuvor erwähnte Logout-Request an das User-Modul explizit durchgeführt werden. Die Zustellung dieses Events führt in unserem Fall zu einem zusätzlichen User-Action Eintrag, hat aber darüber hinaus im UI-Modul keine weiteren Auswirkungen.

 

Wird kein oder ein abgelaufener Token an das User-Service-Modul übermittelt, so werden Requests nur vom Login- und Register-Entry-Point akzeptiert. Soll das Verhalten mit einem abgelaufenen Token explizit getestet werden, so müssen wir den TokenExpirationManager der CDI-Extension anpassen. In Listing TokenExpirationManager für Tests verwenden wir hierfür die Klasse TestTokenExpirationManager , welche von TokenExpirationManager ableitet, mit @Specializes annotiert ist und nur im Test-Classpath verfügbar ist.
@Specializes
public class TestTokenExpirationManager extends TokenExpirationManager {
  @Override
  public long getExpirationTimeInMilliSeconds() {
    expirationTimeInMilliSeconds = globalExpirationTimeInMilliSeconds;
    return super.getExpirationTimeInMilliSeconds();
  }
 
  public static int replaceExpirationTimeInMilliSeconds(
    int expirationTimeInMilliSeconds) {
      int oldValue = globalExpirationTimeInMilliSeconds;
      globalExpirationTimeInMilliSeconds = expirationTimeInMilliSeconds;
      initTokenRenewTimeframe();
      return oldValue;
  }
}
Über die Methode #replaceExpirationTimeInMilliSeconds wird in Listing Abgelaufene Token testen die Gültigkeit des Tokens künstlich reduziert, damit in der Methode #forcedPause statt mehreren Minuten nur wenige Millisekunden gewartet werden muss, bevor mit einem bewusst abgelaufenen Token weitergetestet werden kann. Im konkreten Fall soll der Status UNAUTHORIZED beim Aufruf eines JAX-RS Endpunkts an den Client zurückgesendet werden.
@Test
public void updateUserDetailsAfterTokenExpiration() {
  int previousExpirationTime = TestTokenExpirationManager
    .replaceExpirationTimeInMilliSeconds(1);
 
  try {
    registerUser();
    String token = loginUser();
 
    forcedPause(10L);
 
    Response response = userTarget.request()
      .header(HttpHeaders.AUTHORIZATION, token)
      .buildPost(Entity.json(createTestUser())).invoke();

    Assert.assertNotNull(response);
    Assert.assertEquals(
      UNAUTHORIZED.getStatusCode(), response.getStatus());
  } finally {
    TestTokenExpirationManager
      .replaceExpirationTimeInMilliSeconds(previousExpirationTime);
  }
}
Soll zusätzlich die autom. Tokenerneuerung getestet werden, so müssen wir die Gültigkeitsdauer etwas erhöhen und wie in Listing Tokenerneuerung testen nach dem ersten Request etwas kürzer als die Gültigkeitsdauer warten, bevor wir den zweiten Request absetzen. Von diesem bekommen wir einen neuen Token, mit dem ein dritter Request abgesetzt wird. Warten wir zwischen dem zweiten und dritten Request wieder bis kurz vor Ablauf des Tokens, dann sind wir klar über die Gültigkeitsdauer des ersten Tokens, aber der neu ausgestellte Token ist noch gültig und somit muss auch dieser dritte Request erfolgreich verlaufen.
@Test
public void renewToken() {
  int expirationTime = 3000;
  int previousExpirationTime = TestTokenExpirationManager
    .replaceExpirationTimeInMilliSeconds(expirationTime);
 
  try {
    registerUser();
    String token = loginUser();
 
    forcedPause(expirationTime - 1000L);
    String newToken = updateUserWithTokenUpdate(token);
    forcedPause(expirationTime - 1000L);
    Assert.assertNotEquals(token, newToken);
 
    User loadedUser = updateUser(newToken);
 
    Assert.assertNotNull(loadedUser);
  } finally {
    TestTokenExpirationManager
      .replaceExpirationTimeInMilliSeconds(previousExpirationTime);
  }
}

8.11 Alles zu seiner Zeit

Als nächstes ist das Config-Service Modul an der Reihe. Auch hier gibt es kaum einen Unterschied. Die bisherige Funktionalität wird aus IdeaForkLite wieder übernommen und mit einer JAX-RS Resource für andere Teile der Applikation verfügbar gemacht. Allerdings ist die Initialisierung beim Start des Services etwas anders. Zum einen haben wir in IdeaForkLite bereits eine eigene Config-Source dynamisch hinzugefügt, um die Datenbank als zusätzliche Konfigurationsquelle zu verwenden und wir haben im Falle vom Project-Stage Development einen Konfigurationseintrag manuell erstellt. Bisher haben wir uns hierfür ein JSF Add-on zu Nutze gemacht. Unsere Service-Module können diesen Trick allerdings nicht mehr verwenden. Glücklicherweise schafft hier CDI seit Version 1.1 selbst Abhilfe. Listing CDI 1.1 Container-Startup-Event zeigt wie in der Klasse IdeaForkConfigServiceStartupObserver eine Observer-Methode mit dem vordefinierten Qualifier @Initialized der Start des Application-Scopes überwacht werden kann. Wie bei Observer-Methoden üblich sind zusätzliche Parameter optionale Injection-Points. In unserem Fall lassen wir uns DataBaseAwareConfigSource injizieren und registrieren wie bisher diese zusätzliche Config-Source via ConfigResolver#addConfigSources .
@ApplicationScoped
public class IdeaForkConfigServiceStartupObserver {
  protected void onStartup(@Observes @Initialized(ApplicationScoped.class)
                           Object ideaForkStartedEvent,
                           DataBaseAwareConfigSource configSource) {
 
    ConfigResolver
      .addConfigSources(Arrays.<ConfigSource>asList(configSource));
  }
}
Natürlich kann es für dieses Event mehrere Observer-Methoden geben. So fügen wir in Listing Ungeordnetes Container-Startup-Event einen weiteren Observer hinzu, um einen Konfigurationswert abhängig vom Project-Stage dynamisch zu setzen. Hierfür gibt es verschiedene Möglichkeiten. Wir greifen auf das bereits vorgestellte @Exclude zurück und lassen uns zusätzlich ConfigRepository als Parameter der Observer-Methode injizieren. In der Observer-Methode selbst speichern wir folglich nur noch den Konfigurationseintrag mit Hilfe von ConfigRepository in der Datenbank.
@Exclude(exceptIfProjectStage = ProjectStage.Development.class)
@ApplicationScoped
public class DevIdeaForkConfigServiceStartupObserver {
  protected void onStartup(@Observes @Initialized(ApplicationScoped.class)
                           Object ideaForkStartedEvent,
                           ConfigRepository configRepository) {
 
    configRepository.save(
      new ConfigEntry("maxNumberOfHighestRatedCategories", "10"));
  }
}
Haben wir mehrere Observer für das gleiche Event, wie es bei den eben gezeigten der Fall ist, so kann es sein, dass die Reihenfolge wichtig ist. Wollen wir sicherstellen, dass die neue Config-Source hinzugefügt wird bevor in unserem Beispiel der Konfigurationswert gespeichert wird, können wir eine neue Funktionalität von CDI 2.0 verwenden. Mit der zusätzlichen Verwendung von @javax.annotation.Priority lässt sich die Aufrufreihenfolge von Observer-Methoden steuern. Listing Geordnete Container-Startup-Events zeigt dies für unsere beiden Observer-Methoden.
protected void onStartup(
  @Observes @Initialized(ApplicationScoped.class) @Priority(1)
  Object ideaForkStartedEvent,
  DataBaseAwareConfigSource configSource) {
    //...
}

protected void onStartup(
  @Observes @Initialized(ApplicationScoped.class) @Priority(2)
  Object ideaForkStartedEvent,
  ConfigRepository configRepository) {
    //...
}
Bevor wir das Config-Service verwenden können übernehmen wir den Partial-Bean Ansatz aus IdeaForkLite , mit welchem wir typsichere Konfigurationen via @TypedConfig umsetzen können. Zusätzlich erweitern wir in Listing Erweiterung der Annotation TypedConfig die Annotation um das Annotation-Attribut remote , um zwischen lokaler und zentraler Konfiguration unterscheiden zu können.
@PartialBeanBinding
@Retention(RUNTIME)
@Target(TYPE)
public @interface TypedConfig {
  boolean remote() default false;
}
In der Klasse TypedConfigHandler delegieren wir wie gehabt an den ConfigResolver von DeltaSpike und nur an den in Listing gezeigten Config-Service Client, wenn der Wert von remote explizit auf true gesetzt wird.
@ResourceClient(name = "configs", version = "v1")
public interface ConfigService {
  @GET
  @Path("/{key}")
  String loadForKey(@PathParam("key") String key);
}
Wie der Ausschnitt von Listing Typisierter Config-Handler mit Remote-Support darstellt, wird das ConfigService -Bean direkt injiziert. Geladene Werte werden nach wie vor zeitlich begrenzt gecached. Die entsprechende Funktionalität wurde ebenfalls aus IdeaForkLite übernommen. Da sich dieses Modul selten bis gar nicht ändern würde, könnten wir es auch außerhalb von IdeaForkLite halten und in IdeaForkLite nur als herkömmliche Dependency hinzufügen. Damit wir diese Funktionalität trotzdem im gleichen Git-Repository ohne zusätzlichem Build-Schritt ablegen können, importieren wir den gesamten typsicheren Konfigurationsmechanismus ausnahmsweise in ein eigenes Maven-Modul, welches von allen anderen Modulen verwendet werden darf.
@TypedConfig
@ConfigScoped
@SuppressWarnings("unused")
public class TypedConfigHandler implements InvocationHandler {
  @Inject
  private ConfigService configService;
 
  private Map<String, Object> loadedValues =
    new ConcurrentHashMap<String, Object>();
 
  public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
      String key = method.getName();
      Object result = loadedValues.get(key);
 
      if (result != null) {
        return result;
      }
 
      String loadedValue = null;
 
      TypedConfig typedConfig =
        proxy.getClass().getAnnotation(TypedConfig.class);
      if (typedConfig != null && typedConfig.remote()) {
        loadedValue = configService.loadForKey(key);
      }
 
      if (loadedValue == null) {
        loadedValue = ConfigResolver
          .getProjectStageAwarePropertyValue(key);
      }
      final Class<?> configType = method.getReturnType();
      result = parseValue(loadedValue, configType);
 
      loadedValues.put(key, result);
      return result;
  }
 
  //...
}
Das letzte zu erstellende Backend-Modul ist das Idea-Modul. Wie gehabt importieren wir die Funktionalität von IdeaForkLite und können große Teile unverändert übernehmen. Bei der typsicheren Konfiguration gibt es die erste Anpassung. Wir fügen die typsichere Konfiguration namens IdeaConfig aus Listing Typsichere Konfiguration als Partial-Bean hinzu und legen das Attribute remote mit true fest.
@TypedConfig(remote = true)
public interface IdeaConfig {
  Integer maxNumberOfHighestRatedCategories();
}
Listing Verwendung der typisierten Konfiguration zeigt, dass es bei der Verwendung keinen Unterschied gibt. IdeaConfig wird injiziert und durch den Aufruf von IdeaConfig#maxNumberOfHighestRatedCategories wird der Konfigurationswert für den Key maxNumberOfHighestRatedCategories geladen. Wie bereits beschrieben wird in diesem Beispiel im Hintergrund zuerst der JAX-RS Endpunkt des Config-Service Moduls abgefragt und erst wenn dieser keinen Wert liefert, werden die anderen Config-Sources befragt. Wir könnten das Partial-Bean ConfigService auch direkt via Config-Source einbinden. Allerdings würde dann jeder Config-Lookup eine Remote-Abfrage auslösen. Aus diesem Grund bleiben wir bei dem zuvor beschriebenen zweistufigen Vorgehen in TypedConfigHandler .
@AuthenticationRequired
@Path("categories")
 
@ApplicationScoped
public class CategoryResource {
  @Inject
  private IdeaRepository ideaRepository;
 
  @Inject
  private IdeaConfig ideaConfig;
 
  @GET
  @Path("top")
  public List<CategoryView> getHighestRatedCategories() {
    List<CategoryView> result = ideaRepository.getHighestRatedCategories(
      ideaConfig.maxNumberOfHighestRatedCategories());
    return result;
  }
}
Listing Angepasstes IdeaRepository zeigt die dazugehörigen Anpassungen und Vereinfachungen in der Klasse IdeaRepository . Hier wird auch deutlich, warum in IdeaForkMicro der konfigurierte Wert nicht in IdeaRepository selbst geladen wird.
@Transactional(qualifier = Default.class)
@Repository
public interface IdeaRepository extends EntityRepository<Idea, String> {
  @Query("select i from Idea i where i.authorEmail = ?1")
  List<Idea> loadAllOfAuthor(String email);

  @Query("select new at.irian.cdiatwork.ideafork.idea.domain.CategoryView(
    i.category, count(i.category)) from Idea i group by i.category
    order by count(i.category) desc")
  List<CategoryView> getHighestRatedCategories(
    @MaxResults int maxNumberOfHighestRatedCategories);

  @Query("select i from Idea i where i.topic like CONCAT('', ?1, '') or
    i.category like CONCAT('', ?1, '')")
  List<Idea> search(String searchText);
}

8.12 Zusammenführung

Im UI-Modul von IdeaForkMicro sind nur wenige Änderungen zwingend erforderlich. An diesem Punkt sind im Git-Repository sämtliche REST-Endpunkte implementiert. Hierzu zählt auch CategoryResource . Die Implementierungsdetails sind an dieser Stelle nicht relevant. Sobald wir wissen, dass wir Kategorien über den Pfad "/categories/v1/top" abfragen können, ist die Anbindung dieses Remote-Services, wie in Listing REST-Resource Client als Partial-Bean im UI dargestellt, äquivalent zu den bisherigen Umsetzungen. Die Konvertierung in die typisierte Collection wird im Hintergrund automatisch durchgeführt, wodurch neben @ResourceClient und den Standardannotationen von JAX-RS keine zusätzlichen Annotationen erforderlich sind.
@ResourceClient(name = "categories", version = "v1")
public interface CategoryService {
  @GET
  @Path("/top")
  List<Category> getHighestRatedCategories();
}
Listing Anbindung von CategoryService veranschaulicht die Verwendung von CategoryService in IndexViewCtrl . Ebenfalls neu in Listing Anbindung von CategoryService ist die Verwendung von JsfIdentityHolder , welcher den bisherigen ActiveUserHolder ersetzt. Hierbei handelt es sich um eine spezialisierte Variante von IdentityHolder , welche im Session-Scope abgelegt ist und durch @Named auch für EL-Expressions verfügbar ist.
@ViewController
public class IndexViewCtrl implements Serializable {
  @Inject
  private IdeaPromotionService ideaPromotionService;
 
  @Inject
  private CategoryService categoryService;
 
  @Inject
  private JsfIdentityHolder identityHolder;
 
  private List<Category> categories;
  private int categoryCount;
 
  private List<Idea> promotedIdeas;
  private int promotedIdeaCount;
 
  @PreRenderView
  public void onPreRenderView() {
    if (identityHolder.isAuthenticated()) {
      promotedIdeas = Optional
        .ofNullable(ideaPromotionService.loadRecentlyPromotedIdeas())
        .orElse(emptyList());
      categories = Optional
        .ofNullable(categoryService.loadHighestRatedCategories())
        .orElse(emptyList());

      categoryCount = categories.size();
      promotedIdeaCount = promotedIdeas.size();
    }
  }
 
  //...
}
Listing UI spezifischer IdentityHolder verdeutlicht neben der Umsetzung von JsfIdentityHolder auch die Signalisierung eines User-Logouts, der automatisch durch ein Session-Timeout oder manuell ausgelöst wird. Ein manueller Logout führt zum Reset des Tokens. In IdeaForkMicro ist JsfIdentityHolder die einzige Session-Scoped Instanz, welche bei einem manuellen Logout bis zum Session-Timeout weiter existiert, jedoch vollkommen leer ist und somit kaum Speicher beansprucht.
@Named("identityHolder")
@Specializes
@SessionScoped
public class JsfIdentityHolder extends IdentityHolder
  implements Serializable {
    private boolean logoutSent = false;
 
    public boolean isAuthenticated() {
      return getCurrentToken() != null;
    }
 
    @Inject
    private UserActionService.LogoutService logoutService;
 
    @PreDestroy
    protected void onTimeout() {
      onLogout(false);
    }
 
    @Override
    public void setCurrentToken(String currentToken) {
      super.setCurrentToken(currentToken);
      this.logoutSent = false;
    }
 
    public void onLogout(boolean manualLogout) {
      try {
        if (logoutSent) {
          return;
        }
 
        if (manualLogout) {
          logoutService.logout("LOGOUT");
        } else {
          logoutService.logout("AUTO_LOGOUT");
        }
      } finally {
        logoutSent = true;
        reset();
      }
    }
}
In Listing Nested Partial-Bean ist ersichtlich, dass es sich auch bei UserActionService.LogoutService#logout um eine Partial-Bean Methode handelt, welche von der Remote-Access-Lite Erweiterung an den entsprechenden JAX-RS Endpunkt des User-Service Moduls weitergeleitet wird.
public interface UserActionService {
  @ResourceClient(name = "user-action", version = "v1")
  interface LoginService {
    @POST
    @Path("/login")
    void login(User user);
  }

  @ResourceClient(name = "user-action", version = "v1")
  interface LogoutService {
    @POST
    @Path("/logout")
    void logout(@QueryParam("type") String logoutType);
  }

  @ResourceClient(name = "user-action", version = "v1")
  interface UserStatsService {
    @GET
    ProfileActivity loadLatestActivities();
  }
}
Ein manueller Logout wird in IdeaForkMicro durch den MenuController angestoßen. Listing UI-Controller für manuellen Logout veranschaulicht den entsprechenden Ausschnitt. Nachdem der User-Logout an das Backend signalisiert wurde, werden sämtliche serverseitigen Fenster in der betreffenden Session geschlossen. Bei einem Session-Timeout geschieht dies automatisch und muss daher nicht explizit berücksichtigt werden.
@Named("menuBean")
@Model
public class MenuController {
  @Inject
  private JsfIdentityHolder identityHolder;
 
  @Inject
  private WindowContext windowContext;
 
  public Class<? extends ViewConfig> logout() {
    try {
      identityHolder.onLogout(true);
    } finally {
      resetWindowContext();
    }
    return Pages.User.Login.class;
  }
 
  private void resetWindowContext() {
    String currentWindowId = windowContext.getCurrentWindowId();
    windowContext.closeWindow(currentWindowId);
    windowContext.activateWindow(currentWindowId);
  }

  //...
}
Die verbleibenden Änderungen im UI-Modul sind in einem Commit im Git-Repository von IdeaForkMicro zusammengefasst und größtenteils äquivalent zu der eben vorgestellten Integration der Backend-Module. Sämtliche Änderungen sind so ausgelegt, dass sich die XHTML-Seiten nahezu nicht geändert haben und die View-Controller ebenfalls nahezu ident sind. Primär haben sich in manchen Fällen Properties geändert, falls Properties im JSON-Response anders benannt sind. Außerdem wurde, wie bereits erwähnt, activeUserHolder auf den neuen identityHolder umgestellt.

8.13 Kein Vorteil ohne Nachteil

Durch die Aufteilung in unabhängige Module hat sich allerdings noch ein kleiner Nachteil eingeschlichen. Die Verwendung von Bean-Validation Constraints über Layergrenzen hinweg ist ohne geteiltem Code nicht mehr möglich. Möchten wir bspw. eine neue Instanz von Idea validieren, so müssen wir dies im UI-Modul umsetzen. Listing UI-Validierung mit Bean-Validation Constraints zeigt stellvertretend einen Ausschnitt aus der Idea -Klasse des UI-Moduls.
public class Idea {
  private String id;
 
  @NotNull
  @Size(min = 1, max = 64)
  private String topic;
 
  @NotNull
  @Size(min = 1, max = 64)
  private String category;
 
  private String description;
  private String baseIdeaId;

  //...
}
Instanzen von Idea werden wie gehabt durch JSF autom. validiert. Im Idea-Service Modul sollten wir aber zumindest eine minimale Validierung wiederholen. Statt dem manuellen IdeaValidator möchten wir auch hier Bean-Validation Constraints verwenden. Daher erweitern wir das JPA-Entity Idea , nicht zu verwechseln mit der gleichnamigen Klasse im UI-Modul, um die entsprechenden Constraints. Außerdem müssen wir in der Datei persistence.xml des Service-Moduls den validation-mode namens CALLBACK aktivieren. Dies stellt sicher, dass der JPA-Provider vor der Persistierung Entities durch den Bean-Validation-Provider validieren lässt und erst speichert, wenn keine Constraint-Violations gefunden wurden. Hiermit ist bspw. zusätzlich sichergestellt, dass in unserem Fall auch importierte Idea -Instanzen einer rudimentären Überprüfung unterzogen werden.

 

Ein weiterer Nachteil ergibt sich beim @UniqueUser -Constraint. Der bisher verwendete Constraint-Validator führt für die UI-Validierung eine Datenbankabfrage aus. In IdeaForkMicro würde dies bedeuten, dass wir für die UI-Validierung einen Remote-Aufruf an das User-Service Modul benötigen würden. Aus diesem Grund verzichten wir in IdeaForkMicro auf dieses Constraint. In der aktuellen Version im Git-Repository von IdeaForkMicro meldet in einem solchen Fall die REST-Ressource den Statuscode HTTP 409 ("Conflict") an das UI-Modul zurück. Darüber hinaus beschränken wir uns auf die Ausgabe einer einfachen Fehlermeldung, welche auch bei anderen Registrierungsfehlern angezeigt wird. Soll hingegen eine detaillierte Fehlermeldung ausgegeben werden, so müssten wir unterschiedliche Error-Codes verwenden oder das Idea-Service Modul müsste die Fehlerbeschreibung als validen Response zurückliefern. Auch hier zeigt sich, dass sich die Komplexität von IdeaForkMicro im Vergleich zu IdeaForkLite etwas erhöht hat.

8.14 Der Weg ist das Ziel

In diesem Kapitel haben wir gesehen wie einfach CDI basierte Applikationen in unabhängige Services aufgeteilt werden können. Selbst Themen wie "Service Discovery", die aktuell weder Bestandteil von CDI noch Java EE sind, können mit Hilfe einer CDI-Extension einfach nachgerüstet werden. Die zusätzliche Komplexität, die wir im Laufe des Kapitels gesehen haben und auch weiterführende Themen wie die Nutzung von Containerlösungen (wie bspw. Docker), die Umsetzung von weiteren Infrastrukturbestandteilen (wie bspw. Build-Pipelines), spezielles Monitoring (bspw. in Verbindung mit einem Circuit-Breaker Mechanismus) und einiger anderer Themen sind unabhängig von CDI und werden aktuell hauptsächlich durch proprietäre Projekte zur Verfügung gestellt. Der Weg zu einer Applikation, die all diese Aspekte abdeckt, ist somit noch etwas länger und das Ziel mit Java EE derzeit nur mit zusätzlichen Erweiterungen erreichbar.