Wicket 6 und CDI – es kann so schön sein

Wicket 6.7.0 ist released, und das ist wie immer ein guter Zeitpunkt um sich einige Features  anzuschauen.

Heute geht es um Wicket in Verbindung mit CDI. Ein äusserst spannendes und praktisches Thema das bisher in diesem Blog sträflich vernachlässigt wurde. Am besten, wir fangen direkt an.

Ein neues Projekt – Wicket Archetype

Zunächst erzeugen wir uns ein neues Projekt mit Hilfe des Wicket-Maven-Archetypes.

[code lang=”bash” inline=”yes”]
mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.7.0 -DgroupId=de.effectivetrainings -DartifactId=wicket-cdi -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false
[/code]

Dieses Projekt lässt sich sehr einfach starten.

Um mit CDI arbeiten zu können deklarieren wir zusätzlich eine Depdendency in der pom auf das Wicket-CDI Modul, das mittlerweile nativ mit Wicket mitkommt.

[code lang=”xml” inline=”yes”]
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-cdi</artifactId>
<version>${wicket.version}</version>
</dependency>[/code]

Zusätzlich brauchen wir unbedingt eine Implementierung der CDI-Spezifikation. Natürlich verwenden wir hierfür Weld – die Referenzimplementierung.

Hierfür deklarieren wir zusätzlich folgende Abhängigkeiten in unserer Projekt-Pom.

[code lang=”xml”]

   &lt;dependency&gt;
            &lt;groupId&gt;org.jboss.weld&lt;/groupId&gt;
            &lt;artifactId&gt;weld-core&lt;/artifactId&gt;
            &lt;version&gt;1.1.11.Final&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!-- needed for conversation scope propagation --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.jboss.seam.conversation&lt;/groupId&gt;
            &lt;artifactId&gt;seam-conversation-weld&lt;/artifactId&gt;
            &lt;version&gt;3.0.0.CR2&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.jboss.weld.servlet&lt;/groupId&gt;
            &lt;artifactId&gt;weld-servlet-core&lt;/artifactId&gt;
            &lt;version&gt;1.1.11.Final&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;javax.el&lt;/groupId&gt;
            &lt;artifactId&gt;javax.el-api&lt;/artifactId&gt;
            &lt;version&gt;2.2.1&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;javax.servlet.jsp&lt;/groupId&gt;
            &lt;artifactId&gt;javax.servlet.jsp-api&lt;/artifactId&gt;
            &lt;version&gt;2.2.1&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.slf4j&lt;/groupId&gt;
            &lt;artifactId&gt;slf4j-ext&lt;/artifactId&gt;
            &lt;version&gt;1.6.2&lt;/version&gt;
        &lt;/dependency&gt;

[/code]

Zuletzt müssen wir noch das Jboss-Repository (oder müsste das mittlerweile Wildfly-Repository:) heissen?) integrieren, da die Weld-Ressourcen aus einem mir nicht erfindlichen Grund nicht im öffentlichen Maven-Repository hinterlegt sind.

[code lang=”xml”]
<repository>
<id>jboss</id>
<url>https://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
[/code]

Damit Weld / CDI mit unseren Container (Jetty) startet deklarieren wir noch einen ServletContextListener in der web.xml

[code lang=”xml”]
<listener>
<listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
</listener>
[/code]

<em>Hinweis - das initiale Projekt-Setup mit allen Abhängigkeiten findet man im Commit 62479a3888ca93dc6d9095bafb4858df014ad8d1</em>

Immer auf den Scope fokussieren – CDI Scopes in Wicket

Beim Start der Anwendung sieht man sofort, ob CDI mit dem Container startet, wenn man folgende Meldung in der Konsole sieht.

INFO  - Version                    - WELD-000900 1.1.11 (Final)

Jetzt sind wir bereit, uns so richtig schön mit Scopes, Conversations und Dependency Injection zu beschäftigen.

Wicket mit CDI verheiraten

Das Projekt ist soweit vorbereitet und Wicket möchte gerne eine gepflegte Konversation mit CDI führen. Hierfür müssen wir noch einige kleine Schritte vornehmen. Zunächst brauchen wir den BeanManager, der uns vom zuvor konfigurierten ServletContextListener bereitgestellt wird.

Hierzu definieren wir eine neue Methode initCDI() in unserer Application-Klasse und rufen diese Methode aus der bekannten init()-Methode auf.

[code lang=”java”]

   @Override
   public void init()
   {
        super.init();
        initCDI();
   }

    private void initCDI(){
        BeanManager manager = (BeanManager)getServletContext().getAttribute(
            Listener.BEAN_MANAGER_ATTRIBUTE_NAME);
        new CdiConfiguration(manager).configure(this);
    }

[/code]

Den BeanManager können wir uns direkt aus dem ServletContext laden und übergeben diesen in eine neue Instanz der CdiConfiguration (hier sind wir bereits in der Wicket-Welt). Mit Hilfe der Methode configure(..) binden wir die CdiConfiguration an unsere Application und wir sind bereit für CDI und Wicket.

Mehr ist übrigens nicht zu tun. Die CdiConfiguration kümmert sich bereits um die Registrierung der entsprechenden RequestCycle- und ComponentInstantiationListener. Sehr schön gemacht.

<em>Hinweis - Die Konfiguration für CDI findet man im Commit 6eb7a81549a9138f586fc49b13e434457e77f033</em>

Bitte injezieren Sie..

Um CDI testen zu können bauen wir uns zunächst mal eine kleine Mini-Domäne. Wir bauen einen Miniatur-Online-Shop und brauchen hierfür einige Produkte.

[code lang=”java”]

public class Product implements Serializable{

    private String name;
    private Double prize;

    public Product(String name, Double prize) {
        this.name = name;
        this.prize = prize;
    }

    public String getName() {
        return name;
    }

    public Double getPrize() {
        return prize;
    }
}

[/code]

Und einen Provider für unsere Schuhe.

[code lang=”java”]

@ApplicationScoped
public class ProductProvider {

    public List&lt;Product&gt; productList(){

        Product samba = new Product("Adidas Samba",49.95);
        Product air = new Product("Nike Air",79.95);
        Product balerina = new Product("Balerinas",9.95);

        return Arrays.asList(samba, air, balerina);

    }
}

[/code]

Der Provider liegt im ApplicationScope, da er von allen Benutzern unserer Webseite verwendet wird.

Injezieren können wir in alles, was von Wicket gemanaged wird und die ComponentInstantiationListeneres notifiziert – also Session, Komponenten, die Application und Behaviors.

Wir löschen zunächst alles aus der Homepage und dem zugehörigen Markup, injezieren uns den Provider und zeigen alle Produkte in einem ListView an.

[code lang=”java”]

public class HomePage extends WebPage {
	private static final long serialVersionUID = 1L;

    @Inject
    ProductProvider provider;

    public HomePage(final PageParameters parameters) {
		super(parameters);
                add(new ListView&lt;Product&gt;("productsList", provider.productsList()) {
                     @Override
                     protected void populateItem(ListItem&lt;Product&gt; item) {
                          item.add(new Label("name", new PropertyModel&lt;String&gt;(item.getModel(),"name")));
                          item.add(new Label("prize", new PropertyModel&lt;Double&gt;(item.getModel(),"prize")));
                     }
                });
    }
}

[/code]

und das zugehörige Markup.

[code lang=”html”]

&lt;!DOCTYPE html&gt;
&lt;html xmlns:wicket="http://wicket.apache.org"&gt;
	&lt;body&gt;
        &lt;div wicket:id="productsList"&gt;
            &lt;span wicket:id="name"&gt;&lt;/span&gt;
            &lt;span wicket:id="prize"&gt;&lt;/span&gt;
        &lt;/div&gt;
	&lt;/body&gt;
&lt;/html&gt;

[/code]

Die beans.xml wird immer vergessen…

Beim Zugriff auf die Seite bekommen wir als allererstes eine Unsatisfied-Dependency Exception. CDI ist zwar konfiguriert, aber inaktiv, denn das scannen nach Beans und Abhängigkeiten passiert erst, wenn wir die so gehasste beans.xml im Verzeichnis src/main/resources/META-INF/beans.xml anlegen mit folgendem Inhalt.

[code lang=”xml”]

&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"&gt;
&lt;/beans&gt;

[/code]

Endlich ist es soweit

wicket cdi und weld in aktion

Unser ProductProvider wird injeziert und funktioniert. Ideal ist das aber nicht, was wir hier machen. Denn eigentlich sollte die Liste von verfügbaren Produkten nicht direkt beim ProductProvider angefragt werden, sondern über ein entsprechendes LoadableDetachableModel. Machen wir das doch einfach mal.

Injezieren in Non-Managed Klassen

Hierfür bauen wir uns folgendes Model.

[code lang=”java”]

public class ProductsLoadableDetachableModel extends LoadableDetachableModel&lt;List&lt;Product&gt;&gt; {

    @Inject
    ProductProvider provider;

    @Override
    protected List&lt;Product&gt; load() {
        return provider.productList();
    }
}

[/code]

Funktioniert das? NEIN! Denn Models sind nicht unter Wicket-Kontrolle – hier haben wir per Definition keine Möglichkeit, uns Abhängigkeiten über CDI injezieren zu lassen… Oder?

Das Prinzip ist immer dasselbe, ob wir jetzt mit Spring, Guice oder CDI arbeiten. Für die Kombination mit Spring würde man das allseits bekannte Injector.get().inject(this) verwenden.

Das CDI-Pendant hierfür heisst  CdiContainer.get().getNonContextualManager().inject(this);

Die korrekte Model-Implementierung sieht also so aus.

[code lang=”java”]

public class ProductsLoadableDetachableModel extends LoadableDetachableModel&lt;List&lt;Product&gt;&gt; {

    @Inject
    ProductProvider provider;

    public ProductsLoadableDetachableModel() {
        CdiContainer.get().getNonContextualManager().inject(this);
    }

    @Override
    protected List&lt;Product&gt; load() {
        return provider.productList();
    }
}

[/code]

Fahren wir die Anwendung hoch, funktioniert das Ganze natürlich.

Einige CDI-Spielereien

Jetzt können wir uns noch einige Spezialitäten von CDI anschauen, wir können beispielsweise statt uns den ProductProvider direkt in das Model zu injezieren mit @Produces arbeiten.

[code lang=”java”]

@ApplicationScoped
public class ProductProvider {

    public ProductProvider(){}

    @Produces
    public List&lt;Product&gt; productList(){

        Product samba = new Product("Adidas Samba",49.95);
        Product air = new Product("Nike Air",79.95);
        Product balerina = new Product("Balerinas",9.95);

        return Arrays.asList(samba, air, balerina);

    }
}

[/code]

Wir deklarieren im ProductProvider dass dieser eine Liste von Produkten bereitstellt. Im Model sieht das Ganze dann so aus.

[code lang=”java”]

public class ProductsLoadableDetachableModel extends AbstractReadOnlyModel&lt;List&lt;Product&gt;&gt; {

    @Inject
    List&lt;Product&gt; productList;

    public ProductsLoadableDetachableModel() {
        CdiContainer.get().getNonContextualManager().inject(this);
    }

    @Override
    public List&lt;Product&gt; getObject() {
        return productList;
    }
}

[/code]

Achtung, durch das injezieren der Liste lösen wir zwar die Abhängigkeit zum ProductProvider auf, machen die Liste aber statisch. Wir würden keine Updates auf unsere Produktliste für diese Model-Instanz bekommen.

Kauf dir nen Schuh..

Zuletzt bauen wir uns noch die Möglichkeit, einen Schuh zu kaufen.

Hierfür bauen wir die Page folgendermaßen um.

[code lang=”java”]

public class HomePage extends WebPage {
	private static final long serialVersionUID = 1L;

    @Inject
    private Event&lt;Product&gt; productEvent;

	public HomePage(final PageParameters parameters) {
		super(parameters);

        ListView&lt;Product&gt; listView = new ListView&lt;Product&gt;("productsList", new ProductsLoadableDetachableModel()) {
            @Override
            protected void populateItem(ListItem&lt;Product&gt; item) {
                Form&lt;Product&gt; form = new Form&lt;Product&gt;("form", item.getModel()){
                    @Override
                    protected void onSubmit() {
                        super.onSubmit();
                        productEvent.fire(getModelObject());
                    }
                };
                item.add(form);
                form.add(new Label("name", new PropertyModel&lt;String&gt;(item.getModel(), "name")));
                form.add(new Label("prize", new PropertyModel&lt;Double&gt;(item.getModel(), "prize")));
            }
        };
        listView.setReuseItems(true);
        add(listView);
    }
}

[/code]

Und das entsprechende Markup.

[code lang=”html”]

&lt;!DOCTYPE html&gt;
&lt;html xmlns:wicket="http://wicket.apache.org"&gt;
&lt;body&gt;
&lt;div wicket:id="productsList"&gt;
    &lt;form wicket:id="form"&gt;
        &lt;span wicket:id="name"&gt;&lt;/span&gt;
        &lt;span wicket:id="prize"&gt;&lt;/span&gt;
        &lt;input type="submit" value="bestellen"&gt;
    &lt;/form&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;

[/code]

cdi-weld-order-by-event

Wir stellen also ein Formular bereit, über das unsere Schuhe bestellt werden können.

Interessant ist die neue Instanzvariable

[code lang=”java”]

 @Inject
 private Event&lt;Product&gt; productEvent;

[/code]

Wir verwenden den CDI-Eventmechanismus um Schuhe zu kaufen. Beim Klick auf den Submit-Button wird die onSubmit-Methode der Form-Klasse aufgerufen und hier feuern wir ein neues Event.

[code lang=”java”]

 super.onSubmit();
 productEvent.fire(getModelObject());

[/code]

Zuletzt bauen wir uns einen Miniatur-Product-Service, der auf dieses Event reagiert.

[code lang=”java”]

@ApplicationScoped
public class ProductService {

    public void order(@Observes Product product){
        System.out.println("Produkt " +   product.getName() + " bestellt");
    }
}

[/code]

Und bestellen wir beispielsweise den Nike-Schuh sehen wir in der Konsole wie erwartet.

Produkt Nike Air bestellt

Fazit

Die Integration von CDI und Wicket ist genauso einfach wie mit Spring oder Guice und sieht sehr vielversprechend aus. Ich hoffe, ich habe demnächst die Gelegenheit, dieses nette Duo mal in einem Projekt verwenden zu dürfen.

Links

Anleitung von Igor Vaynberg zu Wicket und CDI
Schöne Anleitung zu CDI zum Ausprobieren
API-Docs zu Wicket & CD
Sourcen auf Github

 

 

Effective Trainings & Consulting - Martin Dilger



Hat Ihnen dieser Blog-Eintrag gefallen? Ich stelle in diesem Blog Informationen über Tools, Frameworks und Werkzeuge zur Verfügung, die mich produktiver machen. Vielleicht kann ich auch Ihnen helfen, produktiver zu werden.


Ich unterstütze Sie als freier Mitarbeiter bei der Entwicklung von Software-Projekten, Agiler Arbeit sowie Schulungen / Fortbildungen.


Jeden Tag ein bisschen produktiver - ab heute