Kolumne: EnterpriseTales

Wenn Scopes fremdgehen: Mehr Spaß mit CDI-Scopes

Lars Röwekamp, Arne Limburg

© Sofware & Support Media

Eine klassische Webarchitektur besteht mindestens aus einer Serviceschicht, die in CDI @ApplicationScoped ist, und einer darüberliegenden Controllerschicht, deren Beans je nach Anwendungsfall @RequestScoped, @ConversationScoped oder @SessionScoped sind und die Services injiziert bekommen. So weit, so normal. Dank des CDI-Proxy-Mechanismus ist aber auch der umgekehrte Weg möglich, also die Injection einer @RequestScoped, @ConversationScoped oder @SessionScoped Bean in eine @ApplicationScoped Bean (also z. B. in den Service). Auf diese Weise lässt sich z. B. ein @RequestScoped EntityManager, der gerade angemeldete Benutzer oder der aktuelle Mandant überall injizieren, wo sie benötigt werden – jedenfalls in der klassischen Webanwendung. Sobald man aber asynchron unterwegs ist, ergeben sich einige Schwierigkeiten, die wir in dieser Kolumne näher beleuchten und umschiffen wollen.

In vorangegangenen Kolumnen haben wir bereits die Möglichkeiten erläutert, die sich durch geschickten Einsatz von CDI-Scopes ergeben. Durch das Erzeugen und Injizieren eines @RequestScoped EntityManagers ist es z. B. möglich, dafür zu sorgen, dass der EntityManager auch nach Beendigung der Transaktion noch geöffnet ist. Dadurch wird das Problem des Lazy Loadings erheblich entschärft. Neben der klassischen Dependency Injection von Infrastrukturobjekten bietet CDI durch die Verwendung verschiedener Scopes auch die Möglichkeit, sich fachliche Objekte, wie z. B. den angemeldeten User oder den aktuellen Mandanten, injizieren zu lassen. Und das, dank der CDI-Proxies, funktioniert sogar bei der Injektion in „größere“ Scopes. Es ist also z. B. sogar möglich, sich den aktuellen (@SessionScoped)Mandanten in einen @ApplicationScoped-Service injizieren zu lassen und dennoch bei jedem Zugriff den korrekten Mandanten zu erhalten. Was geschieht aber, wenn ein solcher Zugriff gerade dann passiert, wenn keine Session aktiv ist, wie z. B. beim Container-Start, in einer @Async-Methode, in einem Thread, der über einen ManagedExecutorService gestartet wurde oder in einem Batch? Ohne Weiteres stehen diese Beans dort nicht zur Verfügung. Beim Zugriff auf eine Methode einer solchen Bean gibt es eine ContextNotActiveException. Wie geht man also mit diesem Problem am besten um?

Das Problem

Fangen wir vorne an. Irgendwo gibt es z. B. Code, der ermittelt, welches der aktuelle Mandant ist. Dieser muss dann @SessionScoped zur Verfügung gestellt werden. Das kann z. B. über einen TenantController geschehen (Listing 1).

Wenn jetzt aber ein @ApplicationScoped-Service einmalig (bei der Erstellung) diesen Mandanten injiziert bekommt, wie kann es dann sein, dass er bei jedem Zugriff einen anderen Mandanten erhält (nämlich den in der aktuellen Session gültigen)? Die Antwort lautet: Er bekommt zunächst einen Proxy injiziert. Injiziert man via CDI einen @RequestScoped EntityManager oder einen @SessionScoped Tenant in einen @ApplicationScoped-Service, so wird zunächst jeweils ein Proxy (die so genannte Contextual Reference) in den Service injiziert. Erst wenn man eine Methode des Proxys aufruft, erfolgt das Ermitteln der aktuell gültigen Instanz (Contextual Instance) und ggf. die Erzeugung dieser Instanz (falls im aktuellen Kontext noch keine existiert). Der Methodenaufruf wird dann vom Proxy auf die Instanz delegiert. Das funktioniert so lange, wie zum Zeitpunkt des Zugriffs tatsächlich ein Request bzw. eine Session aktiv ist. Nur dann nämlich kann eine Contextual Instance gefunden bzw. erzeugt werden. Ansonsten (also z. B. in einem Thread aus dem ManagedExecutorService) fliegt besagte ContextNotActiveException.

Kontexte aktivieren

Die Concurrency Utilities for Java EE definieren, dass innerhalb eines Threads aus einer ManagedThreadFactory (die auch vom ManagedExecutorService verwendet wird) nur der Application-Context aktiv sein muss, d. h. es wird nur garantiert, dass @ApplicationScoped und @Dependent Beans tatsächlich funktionieren. In der Praxis ist es auch tatsächlich so, dass alle anderen Kontexte nicht aktiv sind. Leider gibt es im CDI-Standard keine Möglichkeit, diese Kontexte programmatisch zu aktivieren. Die einfachste Möglichkeit, das dennoch zu tun, bietet die Open-Source-Bibliothek Apache DeltaSpike. Sie bietet die Klasse ContextControl an, mit der sich über die Methoden startContext und stopContext z. B. innerhalb der run()-Methode des Jobs die benötigten Kontexte starten und stoppen lassen.

Infrastrukturcode kapseln

Natürlich ist es so, dass jede Klasse, die Runnable implementiert und über einen ManagedExecutorService gestartet wird, eine (fachliche) Aufgabe zu erledigen hat. Da ist es unschön, dass sie zusätzlich Infrastrukturcode enthält, der die Kontexte startet und stoppt. Außerdem muss derselbe Code in jedem solchen Job wiederholt werden. Da bietet sich eine Kapselung geradezu an. In diesem Fall ist die Verwendung eines @Decorators sinnvoll. Im Gegensatz zu einem Interceptor wird er, wenn er über die beans.xml aktiviert ist, automatisch um einen bestimmten Methodenaufruf gelegt, und das bei jeder Bean, die das entsprechende Interface implementiert. In unserem Fall bietet es sich an, einen Decorator für das Runnable-Interface zu schreiben und die Kontexte um die Methode run() herum zu starten und zu stoppen (Listing 2).

Jede CDI-Bean, die das Runnable-Interface implementiert, bekommt so automatisch den Decorator hinzugefügt. Innerhalb jeder run()Methode ist dann der Session-Kontext aktiv, und es gibt keine ContextNotActiveException mehr.

Daten vererben

So weit, so gut, allerdings ist es wahrscheinlich gewünscht, dass der Tenant, der aktiv war, als der Job gestartet wurde, auch innerhalb des Jobs verfügbar ist. Der Tenant muss aus dem Thread, der den Job startet, in den Thread, der den Job ausführt, vererbt werden. Auch dieses kann unser Decorator leisten, wenn der Job selbst @Dependent ist und direkt vor dem Start auch erst erzeugt wird. Dann nämlich wird der Konstruktor des Decorators im startenden Thread ausgeführt und kann den aktuellen Tenant zwischenspeichern. Hier ist es wichtig, dass nicht die Contextual Refenrence (also der Proxy) gespeichert wird, sondern die Contextual Instance. Daher kann man sich hier nicht den Tenant direkt injizieren lassen, sondern muss den TenantController verwenden und sich den Tenant manuell holen. Dann bekommt man die tatsächliche Instanz (Contextual Instance) anstatt des Proxys (Contextual Reference).

Im Gegensatz zum Konstruktor wird die Methode run() in dem Thread ausgeführt, der den Job laufen lässt. Hier kann (nach Aktivierung des Session-Kontexts) der Tenant wieder gesetzt werden (Listing 3). Durch diesen Trick wird der Tenant von einem Thread in den anderen vererbt. Wie geschrieben, ist dies aber nur möglich, weil die Job-Klasse (und damit auch der Decorator) @Dependent sind. Wäre z. B. der Job @ApplicationScoped, würde der Konstruktor des Decorators nur einmal pro Application aufgerufen, und jeder Job würde denselben Tenant bekommen.

Listing 1:
@SessionScoped
public class TenantController implements Serializable {

  private Tenant tenant;

  @Produces
  @SessionScoped
  public Tenant getTenant() {
    return tenant;
  }

  public void setTenant(Tenant currentTenant) {
    tenant = currentTenant;
  }
}
Listing 2
@Decorator
public class JobDecorator implements Runnable {

  @Inject
  private ContextControl contextControl;
  @Inject @Delegate
  private Runnable job;

  public void run() {
    ...
    contextControl.startContext(SessionScoped.class);
    try {
      job.run();
    } finally {
      contextControl.stopContext(SessionScoped.class);
    }
  }
}
Listing 3
@Decorator
public class JobDecorator implements Runnable {

  ...

  private TenantController tenantController;
  private Tenant tenant;

  @Inject
  public JobDecorator(TenantController controller) {
    tenantController = controller;
    tenant = controller.getTenant();
  }
  private ContextControl contextControl;

  public void run() {
    controller.setTenant(tenant);
    ...
  }
}

Fazit

Möchte man in CDI Objekte kontextabhängig (@RequestScoped, @ConversationScoped oder @SessionScoped) zur Verfügung stellen, muss man sich immer auch Gedanken darüber machen, was geschehen soll, wenn gerade keine Session oder kein Request aktiv sind. Das ist z. B. beim Start-up der Fall oder bei Threads, die über einen ManagedExecutorService gestartet wurden. Die Bibliothek DeltaSpike bietet eine einfache Möglichkeit, im Code in solchen Threads auch diese Scopes zu aktivieren. Will man außerdem Daten (wie z. B. den aktuellen Tenant) von einem Thread in den anderen übernehmen, muss man zusätzlich einen kleinen Trick anwenden, der in dieser Kolumne gezeigt wurde.

Es wurde bereits mehrfach diskutiert, das programmatische Aktivieren und Deaktivieren von Kontexten, wie DeltaSpike es anbietet, in den CDI-Standard aufzunehmen. Bisher wurde es aber immer verschoben. Vielleicht schafft es dieses API ja in CDI 2.0. In diesem Sinne: Stay tuned.

Geschrieben von
Lars Röwekamp
Lars Röwekamp
Lars Röwekamp ist Gründer des IT-Beratungs- und Entwicklungsunternehmens open knowledge GmbH, beschäftigt sich im Rahmen seiner Tätigkeit als „CIO New Technologies“ mit der eingehenden Analyse und Bewertung neuer Software- und Technologietrends. Ein besonderer Schwerpunkt seiner Arbeit liegt derzeit in den Bereichen Enterprise und Mobile Computing, wobei neben Design- und Architekturfragen insbesondere die Real-Life-Aspekte im Fokus seiner Betrachtung stehen. Lars Röwekamp, Autor mehrerer Fachartikel und -bücher, beschäftigt sich seit der Geburtsstunde von Java mit dieser Programmiersprache, wobei er einen Großteil seiner praktischen Erfahrungen im Rahmen großer internationaler Projekte sammeln konnte.
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.