Die Väter der EJB Spec haben etwas außergewöhnliches geschafft. Sie haben in einer Umgebung mit potenziell massiver Nebenläufigkeit die zentralen Bausteine der Geschäftslogik Thread-sicher designed. Das hat seinen Preis: unter anderem sind einheitliche, von allen EJBs erreichbare, RAM-basierte Resourcen nicht Spec konform. Mit anderen Worten: es gibt keine Spec konforme Implementierung für Singletons in einem EJB-Container!
Resourcen mit Singleton-Charakter die nicht in derselben VM laufen lassen sich auch in einer EJB-Umgebung bereitstellen:
- DataSource: eine Datenbank ist immer eine synchronisierte Resource, aus Sicht der EJB handelt es sich hier um ein Singleton.
- ResourceAdapter zu einer synchronisierten Resource: der Zugriff auf solche Resourcen wird über Connection-Implementierungen geregelt, die unter der Verwaltung des Containers stehen. Eine DataSource ist nur ein Spezialfall davon.
- Daneben gibt es eine eingeschränkte Möglichkeit für solche Resourcen in der Container-VM: statische Felder mit read-only Semantik. Es ist dann beispielsweise möglich, in einer EJB eine (nicht synchronisierte) Map auf ein statisches Feld zu legen und diese Map innerhalb des statischen Initializers zu befüllen. Nach der Klasseninitialisierung darf dann der Inhalt der Map nicht mehr geändert werden. Das steht im Widerspruch zu einer der EJB-Restriktionen (...enterprise beans should not: ...read or write nonfinal static fields), aber es funktioniert und ich denke dass an dieser Stelle die Restriktionen nicht genügend differenzieren. Ein prominentes Beispiel hierfür liefert das JDK selbst: die ResourceBundle Implementierung folgt dem Singleton Pattern und besitzt eine read-only Semantik. Da die Caching-Strategie des ResourceBundle sogar den aktuellen ClassLoader einbezieht, kann und soll das ResourceBundle API in JavaEE Umgebungen zum Einsatz kommen.
Eine andere, hoch riskante Variante für die Bereitstellung von Singletons in einem EJB Container bietet sich für Stateless Session Beans und Message Driven Beans: die Poolsize für diese Beans setzt man auf Eins. Riskant deshalb, weil man zum einen die Portabilität der Anwendung verliert (es gibt Server, die die Poolsize nicht festlegen lassen!), zum anderen ist Pooling das effektivste Mittel des Servers, um gutes Skalierverhalten für diese Beanssorten zu gewährleisten und wäre mit diesem Vorgehen verhindert.
Was passiert, wenn man die Vorgaben der EJB Spezifikation nicht so genau nimmt und synchronisierten Code benutzt? Ab hier heißt es die Daumen drücken und hoffen, dass die Threadverwaltung des EJB-Containers nicht mit dem eigenen, synchronisierten Code kollidiert.
Im gleichen Zusammenhang muss man auch ein häufiges anzutreffendes Antipattern einordnen: die EJBHomeFactory. Die EJBHomeFactory ist dabei eine Singleton-Implementierung, die EJBHome-Instanzen einmal vom JNDI der Umgebung holt, zentral vorhält und anfragenden Beans zur Verfügung stellt. Ein Antipattern ist es aus diesen Gründen:
- Zunächst einmal ist aus genannten Gründen die übliche Singleton-Implementierung problematisch. Entweder sie verzichtet auf synchronisierten Code (mit der Gefahr, dass die Instanzen nicht korrekt initialisiert werden) oder es wird korrekt synchronisiert, was aber mit der Threadverwaltung der Containerumgebungen kollidieren kann.
- Mit der EJBHomeFactory (sollte sie in Ihrem Container funktionieren) bekommt jede Bean per JVM die gleiche EJBHome-Instanz. Hier muss man sich klarmachen, dass die EJBHome-Instanzen einer konkreten Containerumgebung unbekannten Code zur Verwaltung des Lebenszyklus der EJBRemote-Instanzen enthalten. Dieser Code kann beispielsweise in geclusterten Umgebungen für Loadbalancing, Ausfallsicherheit oder Exceptionhandling sorgen. Da es laut EJB Spezifikation Singeltons nicht geben kann, werden die Server-Programmierer davon ausgehen, dass jede Bean eigene EJBHome-Instanzen besitzt! Home-Instanzen in Stateful Session Beans müssen passiviert und aktiviert werden können, auch hier steckt wieder Programmlogik dahinter, die wir nicht kennen.
- Angenommen alles funktioniert, was gewinnen wir eigentlich? Eine typische EJB-Anwendung mag 10 Beansorten mit durchschnittlich 20 gepoolten Instanzen per Beansorte haben. Nehmen wir an, jede Beansorte benutzt etwa zwei andere Beansorten, dann werden bei korrekter Implementierung im Laufe der Container- und Bean-Initialisierung 400 JNDI Aufrufe erfolgen und ebenso viele Home-Instanzen erzeugt. Mit der EJBHomeFactory reduziert man diese Zahl auf 10. Eine Optimierung, die im Vergleich dazu was der EJB Container sonst an Fachlogik abarbeitet, nicht mal zu messen sein wird! Bei Stateful Session Beans wird pro Nutzer-Session eine Beaninstanz erzeugt. Nehmen wir an, Ihre Anwendung bedient täglich 10000 Sessions. Sie sparen mit dem EJBHomeFactory-Pattern jeweils einen oder zwei JNDI Aufrufe bei der Initialisierung der Bean. Die Stateful Session Bean lebt dann so lang wie ihre Nutzer-Session. In dieser Zeit wird sie eine ganze Menge Logik abarbeiten und wird immer die anfangs beschaffte Home-Instanz verwenden. Auch hier liefert die EJBHomeFactory eine Mikrooptimierung mit nicht messbarer Verbesserung.
Einzige Empfehlung die ich geben kann: verzichten Sie auf eine
EJBHomeFactory, es birgt große Risiken im produktiven Betrieb, ist die Mühe nicht wert und macht Ihren Code unnötig kompliziert.