Słabo typizowane DI w EJB 3.x - o beanName w @EJB jeszcze raz

Z Jacek Laskowski - Wiki Projektanta Java EE

Posiłkując się doświadczeniami z Embeddable EJB 3.1 z GlassFish 3.1 i NetBeans IDE 7.0 oraz Element beanName w @EJB do rozróżnienia deklaracji ziaren EJB stworzyłem przyczułek do dalszych doświadczeń ze specyfikacjami EJB 3.1 oraz CDI 1.0. Tym razem na warsztat wziąłem zbadanie mechanizmu przekazywania zależności (ang. DI, dependency injection) w EJB 3.x.

Potrzebowałem środowiska, w którym będę mógł uruchomić testy automatycznie bez konieczności zestawiania skomplikowanego środowiska uruchomieniowego. Zaletą takiego podejścia może być chociażby uruchomienie testów w dowolnym momencie, na dowolnym komputerze i przez dowolnego (nawet nowego!) członka zespołu. Starałem się utrzymać minimalny nakład pracy, aby łatwiej było przedstawić problem, z którym się zmagam.

Spis treści

Stworzenie projektu ejb-stateless-beanname

Korzystając z Eclipse IDE i wtyczki Maven Integration for Eclipse stworzyłem projekt ejb-stateless-beanname.

Plik:ejb-stateless-beanname.png

Wprowadziłem zmiany na potrzeby zdefiniowania GlassFish 3.1 jako serwera aplikacyjnego oraz podniesieniem wersji Java SE do 6, co skończyło się poniższym plikiem konfiguracyjnym pom.xml.

<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.jee6.ejb</groupId>
  <artifactId>ejb-stateless-beanname</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>EJB 3.1 :: beanName attribute of @EJB</name>
  <url>http://www.jaceklaskowski.pl/wiki</url>
  <properties>
    <junit.version>4.8.2</junit.version>
    <glassfish.version>3.1</glassfish.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>glassfish-repository</id>
      <url>http://download.java.net/maven/glassfish</url>
    </repository>
  </repositories>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish</groupId>
      <artifactId>javax.ejb</artifactId>
      <version>${glassfish.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.extras</groupId>
      <artifactId>glassfish-embedded-all</artifactId>
      <version>${glassfish.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Należy jeszcze oddzielnie zmienić konfigurację używanego kompilatora Java SE 6 w projekcie - File > Properties (Cmd+I).

Plik:ejb-stateless-beanname-eclipse-javase6.png

Stworzenie komponentów EJB

Interfejs biznesowy - pl.japila.ejb.Hello

Stworzyłem interfejs biznesowy pl.japila.ejb.Hello z pojedynczą metodą sayHello(String).

package pl.japila.ejb;
 
public interface Hello {
    String sayHello(String whom);
}

Jak łatwo zauważyć jest to czysty interfejs javowy - nic związanego z jakąkolwiek technologią.

Plik:Ejb-stateless-beanname-hello.png

Implementacje jako bezstanowe ziarna sesyjne - HelloInEnglish oraz HelloPoPolsku

Do kompletu utworzyłem dwie implementacje komponentów EJB typu @Stateless.

Pierwsza implementacja to angielskojęzyczna wersja Hello - pl.japila.ejb.impl.HelloInEnglish.

package pl.japila.ejb.impl;
 
import javax.ejb.Stateless;
 
import pl.japila.ejb.Hello;
 
@Stateless
public class HelloInEnglish implements Hello {
 
    @Override
    public String sayHello(String whom) {
        return "Hello, " + whom;
    }
 
}

Druga implementacja to polskojęzyczne Hello - pl.japila.ejb.impl.HelloPoPolsku.

package pl.japila.ejb.impl;
 
import javax.ejb.Stateless;
 
import pl.japila.ejb.Hello;
 
@Stateless
public class HelloPoPolsku implements Hello {
 
    @Override
    public String sayHello(String whom) {
        return "Witaj " + whom;
    }
 
}
Plik:ejb-stateless-beanname-hello-impls.png

"Wrota" do świata Java EE 6 - TestFacadeEJB

Kluczową implementacją, którą potraktowałem jako "wrota" do świata Java EE 6 było stworzenie kolejnej, trzeciej implementacji interfejsu Hello - pl.japila.ejb.test.TestFacadeEJB. Z jej pomocą wejdę w świat EJB i CDI, bo kiedy już dojdzie do uruchomienia jego metod, będzie to w ramach serwera aplikacyjnego.

package pl.japila.ejb.test;
 
import javax.ejb.EJB;
import javax.ejb.Stateless;
 
import pl.japila.ejb.Hello;
 
@Stateless
public class TestFacadeEJB implements Hello {
 
    @EJB
    Hello hello;
 
    @Override
    public String sayHello(String whom) {
        return hello.sayHello(whom);
    }
 
}

Warto zwrócić uwagę na pustą (= z domyślnymi wartościami atrybutów) deklarację @EJB. Zaraz przekonamy się o konsekwencjach.

Plik:ejb-stateless-beanname-hello-test.png

Klasa testująca - pl.japila.ejb.HelloTests

Końcowym elementem doświadczenia jest zdefiniowanie klasy testowej pl.japila.ejb.HelloTests.

W nim uruchamiam wbudowany kontener EJB 3.1 (część serwera GlassFish 3.1) oraz po kolei uruchamiam operację biznesową sayHello na każdym z trzech komponentów EJB - HelloPoPolsku, HelloInEnglish oraz TestFacadeEJB.

package pl.japila.ejb;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
 
import java.util.logging.Level;
import java.util.logging.Logger;
 
import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import javax.naming.NamingException;
 
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
 
public class HelloTests {
 
    static EJBContainer ejbContainer;
    static Context ctx;
 
    @BeforeClass
    public static void setUp() {
 
        // Włączenie śledzenia wykonania GlassFish'a
        Logger.getLogger("").getHandlers()[0].setLevel(Level.FINEST);
        Logger.getLogger("javax.enterprise.system.tools.deployment").setLevel(Level.FINEST);
 
        // Uruchomienie wbudowanego kontenera EJB 3.1
        ejbContainer = EJBContainer.createEJBContainer();
 
        // Dostanie się do kontekstu JNDI
        ctx = ejbContainer.getContext();
    }
 
    @Test
    public void testHelloPoPolsku() throws Exception {
        Object object = ctx.lookup("java:global/classes/HelloPoPolsku");
 
        assertNotNull(object);
        assertTrue(object instanceof Hello);
 
        Hello hello = (Hello) object;
 
        String output = hello.sayHello("Agata");
        assertEquals("Witaj Agata", output);
    }
 
    @Test
    public void testHelloInEnglish() throws Exception {
        Object object = ctx.lookup("java:global/classes/HelloInEnglish");
 
        assertNotNull(object);
        assertTrue(object instanceof Hello);
 
        Hello hello = (Hello) object;
 
        String output = hello.sayHello("Iwetka");
        assertEquals("Hello, Iwetka", output);
    }
 
    @Test
    public void testTestFacadeEJB() throws Exception {
        Object object = ctx.lookup("java:global/classes/TestFacadeEJB");
 
        assertNotNull(object);
        assertTrue(object instanceof Hello);
 
        Hello hello = (Hello) object;
 
        String output = hello.sayHello("Patryk");
        assertEquals("Hello, Patryk", output);
    }
 
    @AfterClass
    public static void tearDown() {
        try {
            if (ctx != null) {
                ctx.close();
            }
        } catch (NamingException ex) {
            // handle error
        }
        if (ejbContainer != null) {
            ejbContainer.close();
        }
    }
 
}
Plik:ejb-stateless-beanname-hellotests.png

Uruchomienie i wnioski

Kiedy tylko dojdzie do uruchomienia TestFacadeEJB (Run > Run As > JUnit Test lub Alt+Cmd+X T) otrzymamy oczekiwany komunikat błędu.

SEVERE: Cannot resolve reference Remote ejb-ref name=pl.japila.ejb.test.TestFacadeEJB/hello,
Remote 3.x interface =pl.japila.ejb.Hello,ejb-link=null,lookup=,mappedName=,jndi-name=,refType=Session
because there are 3 ejbs in the application with interface pl.japila.ejb.Hello.

Zgodnie z komunikatem "because there are 3 ejbs in the application with interface pl.japila.ejb.Hello" rozwiązaniem jest uzupełnienie konfiguracji zależności w TestFacadeEJB o właściwą wartość atrybutu beanName w adnotacji @EJB.

package pl.japila.ejb.test;
 
import javax.ejb.EJB;
import javax.ejb.Stateless;
 
import pl.japila.ejb.Hello;
 
@Stateless
public class TestFacadeEJB implements Hello {
 
    @EJB(beanName="HelloInEnglish")
    Hello hello;
 
    @Override
    public String sayHello(String whom) {
        return hello.sayHello(whom);
    }
 
}

Ponowne uruchomienie zakończy się z pewnością poprawnie.

Łatwo zauważyć, że określenie implementacji, która miałaby spełnić kontrakt opisany przez interfejs Hello, następuje przez użycie atrybutu beanName w @EJB, który jest niczym innym jak ciągiem znaków, a w nim o pomyłkę nietrudno. Ową pomyłką mogłaby być literówka, ale również wskazanie na komponent EJB, który nie spełnia danego interfejsu - stąd słaba typizacja. Tutaj na pewno nie można mówić o bezpieczeństwie typów i zagwarantowanie poprawności już podczas kompilacji. Chciałoby się więcej i rozwiązanie przyszło z nadejściem Java EE 6. Ale o tym w następnym artykule.

Wszelkie uwagi zawsze mile widziane i należy je słać na adres autora.

Osobiste