Mechanizm zdarzeń w CDI z JBoss Weld, Arquillian i Apache Maven

Z Jacek Laskowski - Wiki Projektanta Java EE

Pojawiła się potrzeba rozpracowania tematu obsługi zdarzeń w JSR-299: Contexts and Dependency Injection for the Java EE platform (w skrócie CDI). Jako to u mnie bywa, teoria musi być utrwalona praktyką i potrzebowałem zestawić właściwe środowisko uruchomieniowe. Wcześniejsze doświadczenia z CDI wykonałem z referencyjną implementacją JBoss Weld, które spisałem w artykule Contexts and Dependency Injection (CDI) praktycznie - zestawienie środowiska z JBoss Weld, Arquillian i Apache Maven 2, więc nie trwało długo, zanim zdecydowałem się na ponowne spotkanie z Weld.

Jako materiał teoretyczny posłużył mi rozdział 10. Events specyfikacji CDI.

W przeciwieństwie do poprzedniego artykułu, w którym pracowałem z NetBeans IDE, w tym użyłem Eclipse IDE (Helios) 3.6.2 (w wydaniu rozszerzonym o obsługę JEE - Eclipse IDE for Java EE Developers, aczkolwiek jej użycie, to efekt "już ją po prostu miałem").

Skoro w użyciu mam Apache Maven i Eclipse, to ich naturalnym połączeniem jest rozszerzenie Eclipse Integration for Eclipse (instalacja sprowadza się do wybrania menu Help/Eclipse Marketplace... i wybrania Install na liście wyszukanych pakietów).

Z tak zestawionym środowiskiem rozpocząłem rozpoznanie - nic tak przecież nie utrwala świeżo zdobytej wiedzy, jak jej użycie praktyczne.

Kody źródłowe projektu cdi-events dostępne są jako cdi-events.zip.

Stworzenie projektu cdi-events

Skorzystam ze wsparcia Eclipse IDE i rozszerzenia Eclipse Integration for Eclipse do stworzenia projektu cdi-events.

Wybieram File > New > Other... (lub krócej Cmd+N) i w polu Wizards wpisuję maven.

Plik:cdi-events-new-maven-project.png

Później podaję dane projektu i wciskam Finish (w razie wątpliwości, uprasza się o kontakt z autorem).

Należy wprowadzić kilka zmian związanych z wersją Java SE 6 oraz zależności i inne konfiguracje projektowe, aby ostatecznie plik konfiguracyjny projektu pom.xml zawierał następującą treść.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>pl.japila.cdi</groupId>
  <artifactId>cdi-events</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>CDI :: Chapter 10. Events</name>
  <url>http://www.jaceklaskowski.pl/wiki</url>
  <properties>
    <arquillian.version>1.0.0.Alpha5</arquillian.version>
    <junit.version>4.8.2</junit.version>
    <weld.version>1.1.0.Final</weld.version>
  </properties>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <repositories>
    <repository>
      <id>jboss-public-repository-group</id>
      <name>JBoss Public Maven Repository Group</name>
      <url>https://repository.jboss.org/nexus/content/groups/public/</url>
      <layout>default</layout>
      <releases>
        <enabled>true</enabled>
        <updatePolicy>never</updatePolicy>
      </releases>
      <snapshots>
        <enabled>true</enabled>
        <updatePolicy>never</updatePolicy>
      </snapshots>
    </repository>
  </repositories>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.jboss.weld</groupId>
        <artifactId>weld-core-bom</artifactId>
        <version>${weld.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.jboss.arquillian</groupId>
      <artifactId>arquillian-junit</artifactId>
      <version>${arquillian.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>javax.enterprise</groupId>
      <artifactId>cdi-api</artifactId>
      <version>1.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.jboss.arquillian.container</groupId>
      <artifactId>arquillian-weld-se-embedded-1.1</artifactId>
      <version>${arquillian.version}</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.weld</groupId>
      <artifactId>weld-core</artifactId>
    </dependency>
    <dependency>
      <groupId>org.jboss.weld</groupId>
      <artifactId>weld-api</artifactId>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
    </dependency>
    <dependency>
      <groupId>javax.el</groupId>
      <artifactId>el-api</artifactId>
      <version>2.2</version>
    </dependency>
  </dependencies>
</project>

Stworzenie klasy ziarna zarządzanego - pl.japila.cdi.EnabledBean

W 10.1. Event types and qualifier types (strona 71) specyfikacji CDI czytamy:

"An event object is an instance of a concrete Java class with no type variables."

Oznacza to, że dowolna klasa javowa, która nie jest typem generycznym, może być typem komunikatu, który z kolei będzie nośnikiem rozsyłanej informacji. Treść rozsyłanego komunikatu zawarta będzie w stanie obiektu dowolnego, przez nas wybranego, typu. W szczególności może to być dowolna istniejąca klasa z Java SE, np. java.lang.String.

W 10.2. Observer resolution (strona 71) specyfikacji CDI czytamy:

"An event is delivered to an observer method if:

  • The observer method belongs to an enabled bean."

Mnie zastanowiło, czym może być an enabled bean. Odpowiedź znalazłem w 3.1. Managed beans (strona 21), w którym czytamy, że dowolna klasa javowa (bez udziwnień - tutaj liczę na intuicję) jest klasą ziarna zarządzanego. Łatwo i przyjemnie, aż trudno uwierzyć, że to się dzieje w ramach specyfikacji Java EE (!)

Pozostało rozpoznać jak owa metoda przechwytująca miałaby wyglądać. Odpowiedź jest w 10.4.2. Declaring an observer method (strona 74):

"An observer method may be declared by annotating a parameter @javax.enterprise.event.Observes."

Z tą wiedzą stworzyłem poniższą klasę zarządzaną. Zwróć uwagę na użycie wspomnianej adnotacji @Observes przy parametrze wejściowym firstName metody observerMethod.

package pl.japila.cdi;
 
import javax.enterprise.event.Observes;
 
public class EnabledBean {
    public void observerMethod(@Observes String firstName) {
        System.out.println("Observed firstName: " + firstName);
    }
}

Sygnatura metody nie jest określona przez specyfikację, za wyjątkiem użycia @Observes i kilku innych wyjątków związanych z CDI. Innymi słowy, jeśli tylko trzymamy się zasad rządzących językiem Java, jesteśmy zgodni.

Klasę zapisujemy jako EnabledBean.java w katalogu src/main/java/pl/japila/cdi.

Stworzenie klasy testowej - pl.japila.cdi.EventsTest

W 10.3. Firing events (strona 73) czytamy:

"Beans fire events via an instance of the javax.enterprise.event.Event interface, which may be injected."

Klasa kliencka - nadawcza - korzysta z przekazanego (wstrzelonego) obiektu realizującego interfejs javax.enterprise.event.Event, którego typem jest typ komunikatu i przez metodę javax.enterprise.event.Event.fire(Event) wysyła komunikat.

Możemy również skorzystać z przekazanego obiektu manager typu javax.enterprise.inject.spi.BeanManager, aczkolwiek ten sposób nazwałbym niskopoziomowym i dedykowanym dla rozszerzeń CDI. Metoda javax.enterprise.inject.spi.BeanManager.fireEvent(Object, Annotation) jest mechanizmem nadania komunikatu przez "szynę" CDI.

Dociekliwi z pewnością zauważą podobieństwo mechanizmu komunikatów w CDI do wzorca komunikacyjnego publish/subscribe, w którym jeden komunikat może być dostarczony do wielu odbiorców (w szczególności do żadnego).

package pl.japila.cdi;
 
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
 
import java.lang.annotation.Annotation;
 
import javax.enterprise.event.Event;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
 
import org.jboss.arquillian.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ArchivePaths;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Test;
import org.junit.runner.RunWith;
 
@RunWith(Arquillian.class)
public class EventsTest {
 
    @Deployment
    public static JavaArchive createTestArchive() {
        return ShrinkWrap.create(JavaArchive.class, "test.jar")
                .addPackage(EnabledBean.class.getPackage())
                .addAsManifestResource(EmptyAsset.INSTANCE, ArchivePaths.create("beans.xml"));
    }
 
    @Inject
    BeanManager manager;
 
    @Inject
    @Any
    Event<String> firstNameEvent;
 
    @Test
    public void shouldReturnNotNullReferenceOfBeanManager() {
        assertNotNull(manager);
        assertThat(manager.getClass().getName(), is("org.jboss.weld.manager.BeanManagerImpl"));
 
        // wysłanie komunikatu właściwie - korzystając z "klienckiego" API
        firstNameEvent.fire("Jacek");
 
        // wysłanie komunikatu niskopoziomowo - jedynie dla zaawansowanych
        manager.fireEvent("Agata", new Annotation[0]);
    }
 
}

Klasę zapisujemy jako EventsTest.java w katalogu src/test/java/pl/japila/cdi.

Uruchomienie pozostawiam najbardziej wytrwałym. Uprasza się o kontakt z autorem celem wyjaśnienia szczegółów w razie problemów lub gdyby pojawiła się potrzeba uzupełnienia treści artykułu. Powodzenia!

Osobiste