Einfacher als gedacht

CDI – Cross-Language Injection

Sven Ruppert
©Shutterstock.com/maximmmum

Mit der JVM haben wir eine Laufzeitumgebung, in der es viele Sprachen gibt. Jede hat ihre Eigenarten, Vor- und Nachteile. Warum also nicht jede Sprache für das einsetzen, wofür sie gebaut worden ist? Es stellt sich nur eine Frage: Wie kommen alle Teile zusammen? Wir werden uns das jetzt ansehen, denn die Lösung ist einfacher, als es auf den ersten Blick aussieht.

Das Beispiel

Wir beginnen mit einem sehr einfachen Beispiel. Es gibt ein Interface DemoLogic mit genau einer Methode. Diese Methode macht nichts anderes als einen String mit einem Identifier zurückzuliefern. Von dieser Methode soll es verschiede Implementierungen geben. Die Default-Implementierung wird dank Java 8 direkt im Interface definiert. Das Interface selbst bekommt die Annotation CDINotMapped, damit sie aus dem CDI-Scope Default entfernt wird.

@CDINotMapped
public interface DemoLogic {
    public default String workOnString() {
        return "DemoLogicDefault";
    }
}

Passend zu der Default-Implementierung gibt es einen Default-Producer der für die Instanziierung verantwortlich ist (Listing 2). Hier kommt zum ersten Mal der ManagedInstanceCreator (Listing 3) zur Verwendung, mit dem eine Instanz in den CDI-Kontext gehoben wird. Bei der übergebenen Instanz handelt sich nach dem Aufruf der Methode activateCDI(..) um eine vom CDI-Container verwaltete Instanz.

public class DemoLogicDefaultProducer {

    @Inject ManagedInstanceCreator creator;

    @Produces @DefaultImpl
    public DemoLogic create(){
        final DemoLogic logic = new DemoLogic(){};
        return creator.activateCDI(logic);
    }
}

@Qualifier
@Retention(value = RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface DefaultImpl {
}

 

public class ManagedInstanceCreator {

    @Inject BeanManager beanManager;

    public <T> T getManagedInstance(final Class<T> beanType,
                                    final AnnotationLiteral annotationLiteral ){

        T result = null;

        final Set<Bean<?>> beanSet = beanManager
                .getBeans(beanType, annotationLiteral);
        result = beanSet.stream()
                .map((b)-> b.getTypes()
                        .stream()
                        .filter(t -> t.equals(beanType))
                        .findFirst()
                        .map((bean) -> {
                            final Bean<T> beanTyped = (Bean<T>) b;
                            final CreationalContext<T> context
                                    = beanManager
                                    .createCreationalContext(beanTyped);
                            return beanTyped.create(context);
                        })
                        .get())
                .findFirst()
                .get();
        return result;
    }

    public <T> T activateCDI(T t) {
        final Class aClass = t.getClass();
        final AnnotatedType annotationType
                = beanManager.createAnnotatedType(aClass);
        final InjectionTarget injectionTarget
                = beanManager.createInjectionTarget(annotationType);
        final CreationalContext creationalContext
                = beanManager.createCreationalContext(null);
        injectionTarget.inject(t, creationalContext);
        injectionTarget.postConstruct(t);
        return t;
    }
}

Für den initialen Aufbau benötigen wir noch die Möglichkeit den Kontext der Anwendung zu verändern. Das Konzept ContextResolver wurde ausführlicher in meinem Artikel CDI – entscheide spät, entscheide gut beschrieben. Hier in Kurzform die beteiligten Komponenten für dieses Beispiel.
Der Kontext wird durch die Klasse Context repräsentiert (Listing 4). Er wird in dem DemoLogicProducer mittels ContextResolver (Listing 4) aufgelöst um sich für die benötigte Implementierung zu entscheiden.

@Singleton
public class Context {
    public boolean defaultImpl = true;
}
public class ContextResolver {

    @Inject Context context;

    public AnnotationLiteral resolveContext(
            final Class<? > targetClass){
        if (context.defaultImpl){
            return  new AnnotationLiteral<DefaultImpl> () {};
        }else{
            return  new AnnotationLiteral<KotlinImpl> () {};
        }
    }
}

public class DemoLogicProducer {

    @Inject ContextResolver contextResolver;
    @Inject BeanManager beanManager;
    @Inject ManagedInstanceCreator creator;

    @Produces
    public DemoLogic create(){
        final Class<DemoLogic> beanType = DemoLogic.class;
        final AnnotationLiteral annotationLiteral
                = contextResolver.resolveContext(beanType);
        final DemoLogic logic = creator
            .getManagedInstance(beanType, annotationLiteral);
        return logic;
    }
}

Aufmacherbild: flat icons for web and mobile applications with beverages (flat design with long shadows) von Shutterstock / Urheberrecht: maximmmum

[ header = Seite 2: Inject my language please ]

Inject my language please

Bis jetzt wurde lediglich die Java-Implementierung zur Verfügung gestellt. Nun kommen wir zu der Implementierung in einer anderen Sprache. In diesem Beispiel verwende ich Kotlin aus dem Hause JetBrains. Hier werden wir mit der Tatsache konfrontiert, dass Java 8 sehr neu ist. Nicht alle Werkzeuge unterstützen es schon vollständig. Im Speziellen betrifft das die Unterstützung der default-Methoden. Kotlin-Klassen können von Java-Klassen erben, nicht jedoch wenn es sich um Interfaces handelt die eine default-Methode haben. Da das derzeit nicht möglich ist, wird die Vererbung hier gebrochen und ein allgemeiner Ansatz beschritten.
Der allgemeine Ansatz geht über einen Wrapper, der die Vererbung wieder herstellt. Der Wrapper (DemoLogicKotlinWrapper) in Listing 5 erbt/implementiert das Interface DemoLogic und delegiert die Aufrufe an die Implementierung der jeweiligen anderen Sprache. Für den Wrapper wird wie gewohnt ein Producer erzeugt.

@CDINotMapped
public class DemoLogicKotlinWrapper implements DemoLogic {

    private @Inject DemoLogicKotlin kotlin;

    public String workOnString() {
        return kotlin.workOnString();
    }
}

public class DemoLogicKotlinProducer {

    @Inject ManagedInstanceCreator creator;
    @Inject DemoLogicKotlin kotlin;

    @Produces @KotlinImpl
    public DemoLogic create(){
        final DemoLogic logic = new DemoLogicKotlinWrapper();
        return creator.activateCDI(logic);
    }
}

@Qualifier
@Retention(value = RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface KotlinImpl {
}

Bei Sprachen die mittels Default-Konstruktor eingebunden werden können, kann man direkt per Inject eine Instanz erzeugen (siehe Wrapper). Leider ergeben sich hier einige Einschränkungen, die sich bei den verschiedenen Sprachen natürlich unterschiedlich darstellen. Bei Kotlin ist es leider nicht möglich, der Klasse ein AnnotationsLiteral anzuhängen. Deswegen muss der Umweg über den Wrapper und den Producer gegangen werden. Die Kotlin-Instanz befindet sich immer im DEFAULT-Scope von CDI. Es bleibt abzuwarten ob dieses irgendwann möglich sein wird. Die zweite Einschränkung besteht wie schon angedeutet darin, dass keine default-Methoden unterstützt werden. Ich gehe davon aus, dass dies im Laufe der Zeit in Kotlin möglich sein wird.
Der Vollständigkeit halber ist in Listing 6 die Kotlin-Implementierung dargestellt. Die auskommentierten Zeilen zeigen wie es bei der sprachlichen Unterstützung von Java 8 aussehen würde.

class DemoLogicKotlin() {
//class DemoLogicKotlin() : DemoLogic {
//    public override fun workOnString(): String {
    public fun workOnString(): String {
        return "DemoLogicKotlin"
    }
}

Nachdem alles implementiert ist, kann mit Arquillian das Verhalten auf funktionale Richtigkeit hin geprüft werden (Listing 7).

@RunWith(Arquillian.class)
public class DemoLogicTest {
    @Deployment
    public static JavaArchive createDeployment() {
        return ShrinkWrap.create(JavaArchive.class)
                .addPackages(true, "org.rapidpm")
                .addAsManifestResource(EmptyAsset
                    .INSTANCE, "beans.xml");
    }

    @Inject Instance<DemoLogic> demoLogicInstance;
    @Inject Context context;

    @Test
    public void testNotNull() throws Exception {
        Assert.assertNotNull(demoLogicInstance);
        final DemoLogic demoLogic = demoLogicInstance
//                .select(new AnnotationLiteral<Blog0014> () {})
                .get();
        Assert.assertNotNull(demoLogic);
        Assert.assertTrue(demoLogic.workOnString().equals("DemoLogicDefault"));

        context.defaultImpl = false;
        final DemoLogic demoLogicKotlin = demoLogicInstance
//                .select(new AnnotationLiteral<Blog0014> () {})
                .get();
        Assert.assertNotNull(demoLogicKotlin);
        Assert.assertTrue(demoLogicKotlin.workOnString()
            .equals("DemoLogicKotlin"));
    }
}

Fazit

CDI kann sehr schön zur Trennung von unterschiedlichen Programmteilen verwendet werden. Hier wurde gezeigt, dass auch die Trennung von verschiedenen Sprachen in der JVM leicht zu realisieren ist. Der Vorteil meines Erachtens liegt darin, dass die fachlichen Tests weiterhin in Java geschrieben werden können. Damit sollte jeder Entwickler in der Lage sein, auf der fachlichen Ebene zu arbeiten. Keine spezifischen Eigenarten einer anderen Sprache sind notwendig. Erst bei der Implementierung selbst muss auf die Implementierungssprache Rücksicht genommen werden. Ebenfalls eignet sich dieser Ansatz um eine sanfte Migration hin zu einer anderen Sprache zu ermöglichen. Die bestehenden fachlichen Tests können somit gegen die Alt- als auch Neuimplementierung laufen.
Die Quelltexte zu diesem Text sind unter [1] zu finden. Wer umfangreichere Beispiele zu diesem Thema sehen möchte, dem empfehle einen Blick auf [2].

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: