Sonntag, 12. September 2010

Java EE 6 introduces the Enterprise Singleton

Java EE 6 hat einen neuen Session Bean Typ das Singleton im Portfolio. Für ein Singelton typisch, läuft im Applikationsserver nur eine Instanz des EJBs. Das Singleton eignet sich als Cache für andere EJBs, weil sein Status über Methodenaufrufe hinweg erhalten bleibt. Das Singleton ist ein Hybrid aus einer Stateless und einer Stateful Session Bean. Stateless Session Beans sind einem Client nicht fest zugeordnet und  verwalten deswegen auch keinen clientspezifischen Zustand. Ein Stateful Session Bean hingegen, ist einem Client fest zugeordnet und hält statusspezifische Daten für einen Client vor.

Das Singleton ist als gewöhnliches Session Bean implementierbar, sodass Entitäten ansprechbar sind. Eine Cache-Implementierung könnte beispielsweise eine JPA-Abfrage nutzen, um den Cache des Singeltons in einer Lifecycle-Methode zu initialisieren. Vorsicht ist dennoch geboten. Das Singleton ist kein vollwertiger transaktionaler Cache für den Einsatz in einem Cluster. Ein im Cluster einsetzbarer transaktionaler Cache, der seine Daten im Cluster repliziert, ist der JBoss Cache. Der JBoss Cache ist für komplexe, transaktionale und hochverfügbare Cache-Szenarien geeigneter als eine Eigenentwicklung mit einem Singleton.

Singletons berücksichtigen die Nebenläufigkeit. Der Zugriff auf statusspezifische Datenstrukturen des Singletons findet in der Standardkonfiguration (Container-Managed Concurrency) synchronisiert statt. Die Ausführung der Methoden des Singletons wird dabei serialisiert, sodass zu einem gegebenen Zeitpunkt nur ein Client auf das Singleton zugreifen kann. Man bedenke, ein Singleton wird häufig parallel von mehreren Clients verwendet. Ein Client ist im Kontext des Singletons ein anderes EJB.

In dem folgenden Szenario wird veranschaulicht, wie ein Singleton parallel von zwei EJBs und mehreren Clients angesprochen wird.
Szenario: Enterprise Singleton

Das SingletonBean verwaltet eine Map. Die Map fungiert quasi als Cache. Das ManagementBean und das ConfigurationBean fügen Einträge in den Cache ein bzw. lesen Einträge aus dem Cache. Mehrere Clients nutzen das ManagementBean und das ConfigurationBean in nebenläufiger Umgebung.

Implementierung des Singleton Beans:

import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;

@Singleton
public class SingletonBean<T> {
 
    private Map<T, T> cache;

    @PostConstruct
    void createCache() {
    
        cache = new HashMap<T, T>();
    }

    @PreDestroy
    void destroyCache() {
    
        cache.clear();
        cache = null;
    }

    public T get(T key) {
    
        return cache.get(key);
    }

    public void add(T key, T value) {
    
        cache.put(key, value);
    }
}

Schnittstelle des Management Beans:

public interface Management<T> {

    public void addCacheEntry(T key, T value);
    public T getCacheEntry(T key);
}

Schnittstelle des Configuration Beans:

public interface Configuration<T> {

    public void addCacheEntry(T key, T value);
    public T getCacheEntry(T key);
}

Implementierung des Management Beans:

import javax.ejb.EJB;
import javax.ejb.Remote;
import javax.ejb.Stateless;

@Stateless
@Remote(Management.class)
public class ManagementBean<T> implements Management<T> {

    @EJB
    private SingletonBean<T> singleton;
  
    public T getCacheEntry(T key) {
      
        return singleton.get(key);
    }

    public void addCacheEntry(T key, T value) {
      
        singleton.add(key, value);
    }
}

Implementierung des Configuration Beans:

import javax.ejb.EJB;
import javax.ejb.Remote;
import javax.ejb.Stateless;

@Stateless
@Remote(Configuration.class)
public class ConfigurationBean<T> implements Configuration<T> {

    @EJB
    private SingletonBean<T> singleton;

    public T getCacheEntry(T key) {
   
        return singleton.get(key);
    }

    public void addCacheEntry(T key, T value) {
       
        singleton.add(key, value);
    }
}

Client-Konsolenprogramm:

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.apache.log4j.Logger;

public class TestSingleton {

    private static Logger logger = Logger.getRootLogger();

    private static final int MAX_LOOP_CYCLES = 100;
    private static final int MAX_THREADS = 50;
    private static final String KEY = "key-";
    private static final String VALUE = "value-";

    private static final String MANAGEMENT_JNDI_NAME = "SingletonEAR/ManagementBean/remote";
    private static final String CONFIGURATION_JNDI_NAME = "SingletonEAR/ConfigurationBean/remote";

    private static Context context;
    private static Configuration<String> configurationBean;
    private static Management<String> managementBean;

    public static void main(String[] args) throws NamingException {

        lookupAndInitialize();

        final Thread configurationAddThread[] = getThreadPool(new Runnable() {

            @Override
            public void run() {

                for (int i = 0; i < MAX_LOOP_CYCLES; i++) {

                    configurationBean.addCacheEntry(KEY + i, VALUE + i);
                }
            }
        });

        final Thread configurationGetThread[] = getThreadPool(new Runnable() {

            @Override
            public void run() {

                for (int i = 0; i < MAX_LOOP_CYCLES; i++) {

                    TestSingleton.sleep();
                    logger.info(configurationBean.getCacheEntry(KEY + i));
                }
            }
        });

        final Thread managementAddThread[] = getThreadPool(new Runnable() {

            @Override
            public void run() {

                for (int i = 0; i < MAX_LOOP_CYCLES; i++) {

                    managementBean.addCacheEntry(KEY + i, VALUE + i);
                }
            }
        });

        final Thread[] managementGetThread = getThreadPool(new Runnable() {

            @Override
            public void run() {

                for (int i = 0; i < MAX_LOOP_CYCLES; i++) {

                    TestSingleton.sleep();
                    logger.info(managementBean.getCacheEntry(KEY + i));
                }
            }
        });

        start(configurationAddThread, configurationGetThread,
                managementGetThread, managementAddThread);
    }

    private static void lookupAndInitialize() throws NamingException {

        context = new InitialContext();
        configurationBean = (Configuration<String>) context
                .lookup(CONFIGURATION_JNDI_NAME);
        managementBean = (Management<String>) context
                .lookup(MANAGEMENT_JNDI_NAME);
    }

    private static Thread[] getThreadPool(final Runnable runnable) {

        final Thread threadPool[] = new Thread[MAX_THREADS];
        for (int i = 0; i < MAX_THREADS; i++) {

            threadPool[i] = new Thread(runnable);
        }

        return threadPool;
    }

    private static void start(final Thread[] configurationAddThread,
                                     final Thread[] configurationGetThread,
                                     final Thread[] managementGetThread,
                                     final Thread[] managementAddThread) {

        for (int i = 0; i < MAX_THREADS; i++) {

            configurationAddThread[i].start();
            configurationGetThread[i].start();
            managementGetThread[i].start();
            managementAddThread[i].start();
        }
    }

    private static void sleep() {

        try {

            Thread.sleep(5);

        } catch (InterruptedException ex) {

            ex.printStackTrace();
        }
    }
}

Das Client-Programm ist kein klassischer Unit-Test, weil über einen Thread-Pool mehrere Threads gestartet werden, die das ManagementBean und das ConfigurationBean ansprechen. Die Anwendung eines Unit-Tests ist in Bezug auf die Nebenläufigkeit im vorliegenden Szenario problematisch.

Das ManagementBean und das ConfigurationBean fügen Cache-Einträge hinzu, um die Einträge anschliessend wieder zu lesen. Beide Beans beinhalten die gleiche Funktionalität. In einem reellen Szenario würden weitere unterschiedliche Funktionalitäten in den EJBs implementiert sein. Aufgrund des stark vereinfachten Szenarios ist in den Beans keine Rücksicht auf das DRY-Prinzip genommen worden.

Der parallele Zugriff auf das SingletonBean über zwei EJBs und mehrere Clients erweist sich in den Tests als stabil. Es konnte in den Testläufen kein Problem entdeckt werden, das aufgrund der Nebenläufigkeit entstanden ist.

Das Szenario zeigt darüber hinaus, dass EJBs leichtgewichtige Plain Old Java Objects (POJOs) sind, die generisch programmiert werden können. Durch generische Programmierung sind allgemeine typsichere EJBs erstellbar. Ein weiterer Vorteil ist, dass EJBs in Unit-Tests mit Mock-Objekten (Fake it till you make it) gestestet werden können, ohne dabei im Applikationsserver als "managed" EJBs zu laufen.

Java EE 5 und nachfolgende Versionen sind eine gute Basis für die Programmierung von Enterprise Anwendungen. Der Java EE Standard ist gereift und erleichtert deutlich die Programmierung von serverseitigen Java  Anwendungen.

Once again, stay tuned


Der Rechtshinweis des Java Blog für Clean Code Developer ist bei der Verwendung und Weiterentwicklung des Quellcodes des Blogeintrages zu beachten.