Teil 1

CDI managed DynamicObjectAdapter

Sven Ruppert
©Shutterstock.com/tanuha2001

Decorator in CDI sind nützlich, haben aber spezifikationsimmanente Unzulänglichkeiten. Wie kann man diese loswerden? Wie sieht das praktisch aus? Wir werden anhand eines Beispiels einen CDI-basierten DynamicObjectAdapter entwickeln, der einem Entwickler ein sehr flexibles Pattern an die Hand gibt.

Basierend auf dem Newsletter „Object Adapter based on Dynamic Proxy“ [1] von Dr. Kabutz werden wir heute eine dynamische Alternative zu dem Decorator in CDI entwickeln. Das Ziel ist also ein Decorator, der nicht den typischen Limitationen des in der CDI Spec definierten Decorators unterliegt. Aber was sind nun die Dinge die da stören?

Decorator in CDI

Ein Decorator in CDI muss in der beans.xml definiert werden. Damit ist er fix zur Laufzeit verankert. Änderungen bedeuten ein erneutes Laden der beans.xml, sprich z.B. den Neustart des Weld-Containers. Aber auch in der Verwendung hat ein Decorator ein paar Dinge, die nicht so charmant sind. Zum Einen muss die Klasse mit einer speziellen Annotation @Decorator versehen werden. Innerhalb des Decorators muss die Instanz des Originals injiziert werden und der Decorator muss von der Original-Implementierung oder dem Interface ableiten. Ganz schön viele Anforderungen wenn man lediglich eine Methode in ihrem Verhalten ändern möchte, da es sich um eine mandantenspezifische Version handelt. Und durch die Verwendung der Annotation @Decorator ist diese Klasse auch immer ein Spezialfall für den CDI-Container. In Listing 1 ist das Beispiel aus dem Java-EE-Tutorial von Oracle wiedergegeben [2].

@Decorator
public abstract class CoderDecorator implements Coder {
    
    @Inject
    @Delegate
    @Any
    Coder coder;
    
    public String codeString(String s, int tval) {
        int len = s.length();

        return """ + s + "" becomes " + """ + coder.codeString(s, tval) 
                + "", " + len + " characters in length";
    }
}

Inside beans.xml

<decorators>
    <class>decorators.CoderDecorator</class>
</decorators>

Aus der Sicht eines Entwicklers

Beginnen wir mit der Sicht eines Entwicklers. Wie soll es in der Verwendung aussehen? Dank JDK8 kann man die Basisimplementierung als default-Methode in das Interface selbst schreiben. In unserem Beispiel nennen wir das Interface Demologic und es beinhaltet die beiden Methoden add () und sub ().

@CDINotMapped
public interface DemoLogic {

    public default int add(int a, int b){
        return a+b;
    }

    public default int sub(int a, int b){
        return a-b;
    }
}

Das Interface ist absichtlich mit einer Annotation versehen, damit es aus dem Scope Default entfernt wird. Warum das nützlich ist, sehen wir, wenn wir bei dem Producer angekommen sind. Also bitte ich an dieser Stelle noch um ein wenig Geduld. Als nächstes benötigen wir die mandantenspezifische Implementierung der Methode add (). Es soll ausschließlich diese in ihrem Verhalten dahingehend geändert werden, dass zu dem Wert immer 100 hinzugefügt wird. Über die Sinnhaftigkeit kann man an dieser Stelle sicherlich diskutieren. Aus Sicht des Entwicklers ist es angenehm, dass wir lediglich eine Methode implementieren müssen. Kein Delegieren weiterer Methoden, Dummy-Implementierung oder abstract-class-Anforderungen. Wir nennen diese Klasse DemoLogicAdapter_A.

@CDINotMapped
public class DemoLogicAdapter_A implements DemoLogic{

    public int add(int a, int b){
        System.out.println("DemoLogicAdapter_A.add");
        return a+b + 100;
    }
}

Auch diese Implementierung entfernen wir aus dem Default Scope von CDI.

Als nächstes benötigen wir die Möglichkeit, dynamisch den Kontext zu verändern bzw. zu erhalten, um Entscheidungen treffen zu können, welche Implementierung aktiv sein soll. Um das Beispiel sehr einfach an dieser Stelle zu halten, wird dieses hier durch ein Singleton simuliert. Die Klasse hat den Namen Context und besteht lediglich aus einem Attribut vom Typ boolean und dem Namen original. Default-Wert von dem Attribut ist true.

@Singleton
public class Context {

    public boolean original = true;
}

Nun sind die grundlegenden Elemente definiert. Wie aber sieht die Verwendung aus? Hierzu schreiben wir einen JUnit-Test, um zu verdeutlichen wie die Handhabung sein wird. Der Einsatz von Arquillian hilft uns, den Test in einer CDI-Umgebung ausführen zu können. Der Test selbst besteht aus den Schritten:

  • Injizieren der Referenz auf DemoLogic
  • Holen einer Instanz der DemoLogic
  • Aufruf der Methode add(1,1) 
  • Überprüfung ob das Ergebnis den Erwartungen entspricht, hier der Wert 2.
  • Context-Switch auf mandantenabhängige Implementierung
  • Holen einer Instanz der DemoLogic
  • Aufruf der Methode add(1,1) 
  • Überprüfung ob das Ergebnis den Erwartungen entspricht, hier der Wert 102.
  • Kaffee trinken.
@RunWith(Arquillian.class)
public class DemoLogicTest {
    @Deployment
    public static JavaArchive createDeployment() {
        return ShrinkWrap.create(JavaArchive.class)
                .addPackages(true, "org.rapidpm.commons")
                .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
    }

    @Inject @DynamicDecoratorTest Instance<DemoLogic> demoLogic;
    @Inject Context context;
    @Test
    public void testDemoLogicOriginalTest() throws Exception {
        Assert.assertNotNull(demoLogic);
        final DemoLogic demoLogic1 = demoLogic.get();
        final int add = demoLogic1.add(1, 1);
        Assert.assertEquals(2,add);
        System.out.println("add = " + add);

        context.original = false;

        final DemoLogic demoLogic2 = demoLogic.get();
        final int addAdapted = demoLogic2.add(1, 1);
        Assert.assertEquals(102,addAdapted);
        System.out.println("addAdapted = " + addAdapted);
    }
}

 

Damit ist die Sicht des Entwicklers vollständig entkoppelt und besteht ausschließlich aus der Verwendung der abzubildenden Logik. Der Switch kann vom System transparent im Hintergrund erfolgen.

Aufmacherbild: Abstract blue background von Shutterstock / Urheberrecht: tanuha2001

[ header = Der Kern – CDIInvocationHandler ]

Der Kern – CDIInvocationHandler

Nachdem wir nun beschrieben habe was wir haben möchten, gehen wir zu der Implementierung. Als Einstieg nehmen wir den Producer, der für die Instanziierung von der DemoLogic und den verschiedenen Varianten verantwortlich ist. Dem Producer geben wir den Namen DemoLogicProducer.

public class DemoLogicProducer {

    @Inject Instance<DynamicObjectAdapterFactory> 
        dynamicObjectAdapterFactoryInstance;

    @Inject Context context;

    @Produces @DynamicDecoratorTest
    public DemoLogic create(ManagedInstanceCreator instanceCreator){
        final DemoLogic demoLogic = instanceCreator
            .activateCDI(new DemoLogic() {});

        final DynamicObjectAdapterFactory dynamicObjectAdapterFactory = 
             dynamicObjectAdapterFactoryInstance.get();

        final Object adapter;
        if (context.original){
            adapter = new Object();
        } else {
            adapter = instanceCreator
                .activateCDI(new DemoLogicAdapter_A());
        }

        return dynamicObjectAdapterFactory
            .adapt(demoLogic, DemoLogic.class, adapter);
    }
}

Innerhalb der Methode create wird entschieden, welche der beiden Implementierung aktiv sein soll. Um das Beispiel auch hier sehr kurz zu halten, wurde es explizit kodiert. In einem der nächsten Teile werden wir sehen, wie das mittels ContextResolver dynamisch passieren kann. Nachdem die Endscheidung getroffen wurde, wird die DemoLogic mit einem Adapter versehen und in einem Proxy eingebettet. Das Einbetten in den Proxy übernimmt die Klasse DynamicObjectAdapterFactory.

public class DynamicObjectAdapterFactory {

    @Inject Instance<CDIInvocationHandler> 
        cdiInvocationHandlerInstance;

    public  <T> T adapt(final Object adaptee,
        final Class<T> target,final Object adapter) {

        final CDIInvocationHandler invocationHandler =
            cdiInvocationHandlerInstance
                .get()
                .adapter(adapter)
                .adaptee(adaptee);

        return (T) Proxy.newProxyInstance(
                target.getClassLoader(),
                new Class[]{target},
                invocationHandler
                );
    }
}

In der Methode adapt wird die Verbindung zwischen dem Original dem Adapter und dem InvocationHandler erzeugt und als klassischer Proxy zurückgeliefert. Sobald der Proxy verwendet wird, erfolgt bei jedem Aufruf einer Methode der Aufruf der Methode invoke des InvocationHandlers.

public class CDIInvocationHandler implements InvocationHandler {

    @Inject @CDILogger Logger logger;

    private Map<MethodIdentifier, Method> adaptedMethods = 
        new HashMap< > ();

    private Object adapter;
    private Object adaptee;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
        throws Throwable {

        if (adaptedMethods.isEmpty()){
            final Class<?> adapterClass = adapter.getClass();
            Method[] methods = adapterClass.getDeclaredMethods();
            for (Method m : methods) {
                adaptedMethods.put(new MethodIdentifier(m), m);
            }
        }else{
            if (logger.isDebugEnabled()) {
                logger.debug("adaptedMethods is initialized..");
            }
        }
        try {
            Method other = adaptedMethods.get(
                new MethodIdentifier(method));
            if (other != null) {
                return other.invoke(adapter, args);
            } else {
                return method.invoke(adaptee, args);
            }
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }
    public CDIInvocationHandler adapter(final Object adapter) {
        this.adapter = adapter;
        return this;
    }
    public CDIInvocationHandler adaptee(final Object adaptee) {
        this.adaptee = adaptee;
        return this;
    }
}

public class MethodIdentifier {
    private final String name;
    private final Class[] parameters;

    public MethodIdentifier(Method m) {
        name = m.getName();
        parameters = m.getParameterTypes();
    }

    // we can save time by assuming that we only compare against
    // other MethodIdentifier objects
    public boolean equals(Object o) {
        MethodIdentifier mid = (MethodIdentifier) o;
        return name.equals(mid.name) &&
                Arrays.equals(parameters, mid.parameters);
    }

    public int hashCode() {
        return name.hashCode();
    }
}

Die beiden wichtigen Abschnitte in der Klasse CDIInvocationHandler sind zum einen das Auslesen des Adapters mittels Reflection um die Methoden zu erhalten. Diese werden in einer HashMap hinterlegt. Der Key besteht aus dem Methodennamen und der Signatur der Methode. Damit ist sie eindeutig zu bestimmen. Wird also eine Methode mit dem resultierenden Key aufgerufen, wird die Instanz der Methode aus der HashMap verwendet. Alle anderen Aufrufe werden von dem Original bedient.

Fazit

Mit diesem Pattern sind wir in der Lage die Unzulänglichkeiten des CDI Decorators auszugleichen. Das angenehme ist, dass spätere Änderungen an dem Interface nicht dazu führen, dass alle Adapter angepasst werden müssen. Natürlich nur wenn Methoden betroffen sind die nicht mit einem Adapter versehen worden sind. Eine Erweiterung des Interfaces ist somit immer aus dieser Sicht unproblematisch. Alle Komponenten sind von dem verwendeten CDI-Container verwaltet. Damit ergeben sich viele Möglichkeiten, die wir in den nächsten Teilen noch beleuchten werden. An dieser Stelle möchte ich noch auf meinen Artikel in der Java Magazin Ausgabe 3.2014 (erscheint am 05.02.) hinweisen. Dort werden wir uns mit dem CDI Bootstrap in JavaFX beschäftigen.

Die Quelltexte zu diesem Text sind unter [3] zu finden. Wer umfangreichere Beispiele zu diesem Thema sehen möchte, dem empfehle einen Blick auf [4].

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: