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.
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).
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ą.
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; } }
"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.
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(); } } }
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.






