Wicket und CDI 1.1 – auf den richtigen Kontext kommt es an

Es gab viel Diskussion um Wicket und CDI. Insbesondere der Sprung von CDI 1.0 auf CDI 1.1 hat viele Probleme gemacht. Die CDI Integration für Wicket wurde komplett überarbeitet und verbessert. CDI gefällt mir aufgrund der Einfachheit besonders gut und ich arbeite sehr gerne damit.

Schon die Integration mit CDI 1 und Weld war sehr schön gemacht und ist hier beschrieben.

Ein erster Archetype

Wir generieren uns ein neues Projekt und arbeiten auf der Wicket 7 SNAPSHOT Version. Das ist zum jetzigen Zeitpunkt Work in Progress, uns stehen aber alle neuen Features zur Verfügung. In der aktuell veröffentlichten Version 6.12.0 ist der neue CDI Support leider noch nicht verfügbar.

mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=7.0.0-SNAPSHOT -DgroupId=de.effectivetrainings -DartifactId=wicket-6-cdi -DarchetypeRepository=https://repository.apache.org/content/repositories/snapshots/ -DinteractiveMode=false

Der Stand auf dem wir hier arbeiten ist wirklich brandneu. Es ist also notwendig, den Wicket Source Code auszuchecken und zumindest das CDI Modul einmal zu bauen.

git clone http://git-wip-us.apache.org/repos/asf/wicket.git

mvn clean install -Dmaven.test.skip -fae -rf :wicket-cdi-1.1

Anschliessend deklarieren wir die Dependency in unserem Projekt.

<dependency>
     <groupId>org.apache.wicket</groupId>
     <artifactId>wicket-cdi-1.1-weld</artifactId>
     <version>0.2-SNAPSHOT</version>
</dependency>

Zusätzlich holen wir uns die aktuelle WELD Version. WELD ist die Referenzimplementierung für die CDI 1.1 Spezifikation.

<dependency>
            <groupId>org.jboss.weld</groupId>
            <artifactId>weld-core</artifactId>
            <version>2.1.0.Final</version>
</dependency>

 <dependency>
            <groupId>org.jboss.weld.servlet</groupId>
            <artifactId>weld-servlet-core</artifactId>
            <version>2.1.0.Final</version>
</dependency>

<dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>2.2.1</version>
            <scope>provided</scope>
</dependency>
<dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.2.1</version>
            <scope>provided</scope>
</dependency>

Und wir definieren wie das letzte Mal den Weld-Servlet-Listener.

 <listener>
        <listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
 </listener>

Zuletzt legen wir uns unter src/main/resources/META-INF eine leere Datei beans.xml an. Eigentlich ist dieser Schritt seit CDI 1.1 nicht mehr notwendig, sondern es reicht, eine Bean mit einer Scope-Annotation (@RequestScoped, @SessionScoped, @ApplicationScoped) im Classpath zu haben. Leider scheint dies mit Jetty noch nicht zu funktionieren, so dass die beans.xml weiterhin notwendig ist.

Beim Startup des Jetty sehen wir folgende Meldung und wissen dadurch, dass WELD aktiv ist.

INFO  - Version                    - WELD-000900: 2.1.0 (Final)

Die ersten Schritte

Um CDI für Wicket zu aktivieren braucht es nach wie vor nur eine Zeile in der init()-Methode der WicketApplication.

CdiConfiguration.get().configure(this);

Wir definieren uns eine Klasse EventProvider, der uns Events generiert (Event-basierte Systeme sind derzeit sehr gefragt).

@ApplicationScoped
public class EventProvider {

    @Produces
    @RequestScoped
    public Event newEvent(){
        return new Event("Simple Default Event");
    }
}

Und zusätzlich eine einfache Event-Bean.

@Vetoed
public class Event implements Serializable {

    private String value;

    private long random = new Random().nextInt();

    public Event(String value) {
        this.value = value;
    }

    public Event() {
    }

    public String getValue() {
        return value + random;
    }
}

Die Klasse ist deswegen als @Vetoed markiert, damit sie nicht vom CDI Container geparsed und als Bean erkannt wird. Das übernimmt der EventProvider für uns. Ohne das @Vetoed würden wir folgende Exception sehen.

WELD-001409: Ambiguous dependencies for type Event with qualifiers @Default

Nur um zu testen ob das Ganze funktioniet injezieren wir eine Event-Instanz in unsere WicketPage.

public class HomePage extends WebPage {

   @Inject
   private Event event;

   private static final long serialVersionUID = 1L;

   public HomePage(final PageParameters parameters) {
        super(parameters);
        add(new Label("eventName", new PropertyModel<Event>(event, "value")));
    }
}

Sobald wir starten sehen wir, dass CDI aktiv ist und ein Event bereitgestellt wurde.

CDI Injected Event Class

CDI Injected Event Class

Jetzt wirds fachlich

Nehmen wir das Beispiel aus dem letzten Artikel zum Thema CDI nochmal auf.

Wir haben einen ProductProvider und eine Product-Klasse.

@ApplicationScoped
public class ProductProvider {

    public ProductProvider(){}

    @Produces
    public List<Product> 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);

    }
}

Und ein LoadableDetachableModel zum Laden der Produkte in der WicketPage.

public class ProductsModel extends LoadableDetachableModel<List<Product>> {

    @Inject
    private ProductProvider productProvider;

    public ProductsModel() {
        CdiConfiguration.get().getNonContextualManager().inject(this);
    }

    @Override
    protected List<Product> load() {
        return productProvider.productList();
    }
}

Das Prinzip ist das Gleiche. Model-Klassen stehen weder unter Wicket- noch unter CdiContainer-Kontrolle sondern sind einfache Pojos. Um den ProductProvider also injezieren zu können müssen wir die CDI-Injection manuell anstossen. Das funktioniert mit folgender Zeile.

CdiConfiguration.get().getNonContextualManager().inject(this);

Doch halt.

Was macht eine CDI Bean zu einer CDI Bean? CDI Beans sind normale Pojos mit einem Default-Konstruktor oder einem Konstruktor der mit @Inject annotiert ist. Unser LoadableDetachableModel ist zur CDI-Bean qualifiziert.

Ändern wir das Model folgendermaßen ab.

public class ProductsModel extends LoadableDetachableModel<List<Product>> {

    @Inject
    private ProductProvider productProvider;

    @Override
    protected List<Product> load() {
        return productProvider.productList();
    }
}

Und injezieren uns das Model in der WicketPage.

public class HomePage extends WebPage {

    @Inject
    private Event event;

    @Inject
    private ProductsModel productsModel;

    private static final long serialVersionUID = 1L;

    public HomePage(final PageParameters parameters) {
	super(parameters);
        add(new ListView<Product>("products", productsModel) {
            @Override
            protected void populateItem(ListItem<Product> item) {
                item.add(new Label("productName", new PropertyModel(item.getModel(), "name")));
                item.add(new Label("productPrize", new PropertyModel(item.getModel(), "prize")));
            }
        });
    }
}

Wir könnten das Model sogar als @ApplicationScoped definieren und würden in der gesamten Anwendung mit einer einzigen Instanz des Models arbeiten. Valide da das Model selbst keinen State besitzt und nur an den ProductProvider delegiert.

Auf diese Art und Weise funktioniert CDI-Injection sogar in Model-Klassen.

WicketTester und CDI

Wie gut oder schlecht funktioniert jetzt die Zusammenarbeit mit dem WicketTester und CDI?

Wie es scheint, ohne gut zureden nicht besonders gut, denn starten wir den Test aus dem Archetype sehen wir folgendes.

java.lang.IllegalStateException: Singleton is not set. Is your Thread.currentThread().getContextClassLoader() set correctly?
	at org.jboss.weld.bootstrap.api.helpers.IsolatedStaticSingletonProvider$IsolatedStaticSingleton.get

Was müssen wir also tun, um den WicketTester mit CDI bekannt zu machen?

Wir brauchen CDI-Unit von JGlue.  Diese Bibliothek wird übrigens auch von Wicket und dem Wicket-CDI Modul selbst verwendet.

Wir deklarieren folgende Abhängigkeit in unserer pom.

<dependency>
	<groupId>org.jglue.cdi-unit</groupId>
	<artifactId>cdi-unit</artifactId>
	<version>2.2.1</version>
	<scope>test</scope>
</dependency>

Und ändern unseren Test folgendermaßen ab.

@RunWith(CdiRunner.class)
@AdditionalClasses(value = {
    CdiConfiguration.class,
    ConversationPropagator.class,
    ConversationExpiryChecker.class,
    DetachEventEmitter.class},
    late = "org.apache.wicket.cdi.NonContextualManager")
public class TestHomePage
{
	private WicketTester tester;

	@Before
	public void setUp()
	{
		tester = new WicketTester(new WicketApplication());
	}

	@Test
	public void homepageRendersSuccessfully()
	{
		// start and render the test page
		tester.startPage(HomePage.class);

	}
}

Zunächst sorgen wir dafür, dass der Test als CDI Test ausgeführt wird.

@RunWith(CdiRunner.class)

Leider findet der CdiRunner nicht alle Klassen. Warum genau verstehe ich nicht. Wir müssen also manuell dafür sorgen, dass die fehlenden Klassen geladen werden.

@AdditionalClasses(value = {
    CdiConfiguration.class,
    WeldCdiContainer.class,
    ProductsModel.class,
    AbstractCdiContainer.class,
    ConversationPropagator.class,
    ConversationExpiryChecker.class,
    DetachEventEmitter.class},
    late = "org.apache.wicket.cdi.NonContextualManager")

Die Klasse NonContextualManager ist in Wicket als Packageprivate deklariert. Deswegen verwenden wir das late-Binding und müssen die Klasse über den vollqualifizierten Namen ansprechen.

Beim Start bekommen wir jetzt die nächste Fehlermeldung.

org.jboss.weld.context.ContextNotActiveException: WELD-001303: No active contexts for scope type javax.enterprise.context.ConversationScoped

Die Initialisierung scheint unvollständig.

Wir haben am Anfang des Artikels den ServletListener org.jboss.weld.environment.servlet.Listener in der web.xml konfiguriert. Dies fehlt uns jetzt natürlich.

Das lässt sich aber simulieren (analog der Implementierung in Wicket-CDI).

@Inject
private ContextController contextController;

/*
before test
*/
@Before
public void setUp()
{
     tester = new WicketTester(new WicketApplication());
     prepareRequest(tester.getRequest());
}

/*
prepare contexts
*/
private void prepareRequest(HttpServletRequest request)
{
     contextController.openRequest(request);
}

Wir sorgen für jeden Request dafür, dass der CDI-Kontext aktiviert wird.

Insgesamt ist der Overhead ziemlich groß und für jede neue Klasse muss die @AdditionalClasses – Liste erweitert werden. Ohne etwas wie Arquillian zu verwenden ist mir aktuell nicht klar, wie das vereinfacht werden könnte.

Links

Source Code (GitHub)

Dev List Diskussion

Weitere Dev List Diskussion

Hint to use Arquillian

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