EJB 3.0 är den nu gällande versionen av Java Enterprise Beans(EJB) arkitekturen som ingår i Java EE 5. Syftet med EJB 3.0 är att förbättra arkitekturen för EJB och minska komplexiteten för utvecklaren av EJB applikationer. Detta innebär tex följande förbättringar:

  • Annoteringar, det finns ett gäng med annoteringar som man kan använda sig utav för att förenkla arbetet. Dessa annoteringar minskar antalet klasser och interface som man måste skapa och man behöver inte skapa någon deployment descriptor (om man inte vill).
  • Defaulta värden, man skall slippa specifiera en massa vanliga förväntade beteenden och krav från EJB-containern.
  • Inkapsling av beroenden och JNDI åtkomst via annoteringar och dependency injection (DI)
    Businessinterfacet för en sessionsböna kan vara ett vanligt Java-interface, det behöver inte vara av typen EJBObject, EJBLocalObject eller javax.rmi.Remote
  • Home-interfacet behövs inte längre för sessionsbönor.
  • Minskning av krav av användning av checked exceptions
  • En interceptor funktionalitet finns för sessions- och message-driven-bönor.
  • Entitetsbönor har fått en helt egen specifikation, Java Persistence API (JPA), är numera vanliga POJO’s.

Det finns ett par olika typer av EJB:er, sessionsbönor och message-driven-bönor. Sessionsbönorna kommer i två olika smaker, Stateless och Stateful. Entitetsbönorna har ju som sagt ersatts med JPA entiteter. Jag tänkte gå igenom dessa med små korta exempel.

Stateless exempel
En stateless sessionsböna behåller inget tillstånd mellan sig själv och klienten mellan anrop av olika metoder. Detta innebär att bönorna är delade mellan olika klienter, viket i sin tur betyder att man skall inte spara tillstånd i instansvariabler när man använder sig utav Statless EJB. Det finns ingen garanti att man får exakt samma instans av EJB:n mellan två olika anrop. EJB-container tillhandahåller en pool med identiska EJB:er och returnerar vilken som till klienten som efterfrågar den. I detta exempel så tänkte jag skapa en kalkylator som behärskar de fyra räknesätten plus, minus, multiplikation och division. Jag börjar med att skapa ett interface som definerar de olika räknesätten:

package se.cygni;

public interface Calculator {
   int add(int v1, int v2);
   int subtract(int v1, int v2);
   int multiply(int v1, int v2);
   double divide(int v1, int v2) throws Exception;
}

Eftersom jag inte annoterar detta interface med någon utav följande annoteringar, javax.ejb.Local eller javax.ejb.Remote så kommer detta per default att bli vårat lokala interface. Jag skulle lika gärna explicit kunnat annoterat det med @Local. Som ni ser så behöver inte ett lokalt interface till en EJB ärva från javax.ejb.EJBLocalObject och behöver inget javax.ejb.EJBLocalHome interface som behövdes i EJB 2.x och tidigare. Lokala interface använder sig av pass-by-reference, vilket betyder att vanlig java semantik används för returvärden, argument som skickas in och undantag som slängs. Jag skapar även ett interface som skall vara vårat remota interface, CalculatorRemote som ärver interfacet Calculator (för samma interface kan inte vara både det lokala och det remota) och jag annoterar det med annotationen javax.ejb.Remote:

package se.cygni;

import javax.ejb.Remote;

@Remote
public interface CalculatorRemote extends Calculator {
}

Remote-interface använder sig av pass-by-value, vilket betyder att alla returvärden, argument som skickas och undantag som slängs serialiseras vid varje anrop. Detta innebär att alla värden som skickas, returneras eller slängs måste implementera java.io.Serializable. Ett remote-interface till en EJB behöver inte ärva från javax.ejb.EJBObject som i tidigare versioner av EJB.

Nu skapar jag själva EJB implementationen, en klass som implementerar mina två interface och annoterar klassen med annotationen javax.ejb.Stateless:

package se.cygni;

import javax.ejb.Stateless;

@Stateless
public class CalculatorBean implements Calculator, CalculatorRemote {

   public int add(int v1, int v2) {
      return v1 + v2;
   }

   public double divide(int v1, int v2) throws Exception {
      if(v2 == 0)
         throw new Exception("zero not allowed");

      return v1 / v2;
   }

   public int multiply(int v1, int v2) {
      return v1 * v2;
   }

   public int subtract(int v1, int v2) {
      return v1 - v2;
   }
}

I EJB3 så behöver inte sessionsbönan implementera javax.ejb.SessionBean som man var tvungen att göra i tidigare versioner. Har ni använt tidigare versioner av EJB så kanske ni även reagerar på att klassen implementerar businessinterfacen. I tidigare versioner så fick man inte implementera interface som ärvde från javax.ejb.EJBObject (dvs det remota-interfacet), men det finns inget krav att remota-interfacen skall ärva javax.ejb.EJBObject i EJB3, så nu är det standard att alltid i sin EJB-klass implementera businessinterfacen.

Så där ja, nu är min stateless EJB klar, dags att testa den. EJB3 är betydligt enklare att testa en tidigare versioner av EJB. Jag tänkte skriva ett vanligt JUnit-test och använda mej av OpenEJB som embedded EJB3 container. Det enda jag behöver lägga till för att OpenEJB skall hitta vår EJB är en tom META-INF/ejb-jar.xml i vår classpath, (du skall ha följande innehåll i ejb-jar.xml: <ejb-jar/>). Detta är inget EJB krav, utan jag läste i OpenEJB’s dokumentation att de rekommenderade att alltid tillhandahålla en tom ejb-jar.xml för att underlätta sökandet av annoterade klasser. Men mitt test funkar inte utan den tomma ejb-jar.xml filen?! Hur som helst, jag använder Maven så jag skapar ejb-jar.xml i src/main/resources/META-INF/ejb-jar.xml. Nedan kommer min testklass:

package se.cygni;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import org.junit.Before;
import org.junit.Test;

public class CalculatorTest {

private InitialContext ctx;

   @Before
   public void setUp() throws Exception {
      Properties props = new Properties();
      props.setProperty(Context.INITIAL_CONTEXT_FACTORY,
"org.apache.openejb.client.LocalInitialContextFactory");

      ctx = new InitialContext(props);
   }

   private Calculator getLocal() throws Exception {
      Object obj = ctx.lookup("CalculatorBeanLocal");
      assertNotNull(obj);
      assertTrue(obj instanceof Calculator);
      return (Calculator) obj;
   }
   private CalculatorRemote getRemote() throws Exception {
      Object obj = ctx.lookup("CalculatorBeanRemote");
      assertNotNull(obj);
      assertTrue(obj instanceof CalculatorRemote);
      return (CalculatorRemote) obj;
   }

   @Test
   public void addLocal() throws Exception {
      Calculator cal = getLocal();
      assertEquals(10, cal.add(3, 7));
   }
   @Test
   public void subtractLocal() throws Exception {
      Calculator cal = getLocal();
      assertEquals(10, cal.subtract(17, 7));
   }
   @Test
   public void multiplyLocal() throws Exception {
      Calculator cal = getLocal();
      assertEquals(10, cal.multiply(2, 5));
   }
   @Test
   public void divideLocal() throws Exception {
      Calculator cal = getLocal();
      assertEquals(3d, cal.divide(6, 2));
      try{
         assertEquals(0, cal.divide(3, 0));
         fail("can not divide with zero");
      } catch (Exception e) {
         assertEquals("zero not allowed", e.getMessage());
      }
   }
   @Test
   public void addRemote() throws Exception {
      Calculator cal = getRemote();
      assertEquals(10, cal.add(3, 7));
   }
   @Test
   public void subtractRemote() throws Exception {
      Calculator cal = getRemote();
      assertEquals(10, cal.subtract(17, 7));
   }
   @Test
   public void multiplyRemote() throws Exception {
      Calculator cal = getRemote();
      assertEquals(10, cal.multiply(2, 5));
   }
   @Test
   public void divideRemote() throws Exception {
      Calculator cal = getRemote();
      assertEquals(3d, cal.divide(6, 2));
      try{
         assertEquals(0, cal.divide(3, 0));
         fail("can not divide with zero");
      } catch (Exception e) {
         assertEquals("zero not allowed", e.getMessage());
      }
   }
}

Tyvärr så standardiserar inte EJB3-specifikationen JNDI-namnen för klienterna som använder sig av EJB:erna (detta kanske kommer i EJB3.1). OpenEJB använder sig default av följande schema: <ejbName> + <interfaceType>, i mitt fall ger det mej följande JNDI-namn, CalculatorBeanLocal och CalculatorBeanRemote.

När jag kör mitt test så får jag följande resultat:

Tests run: 8, Failures: 0, Errors: 0, Skipped: 0

För skojs skull så kan vi testa att deploya denna EJB på Glassfish och skriva en liten klient som anropar för att se att det verkligen funkar med en annan EJB container. Jag börjar med att starta Glassfish:

> asadmin start-domain

Sen så packar jag ihop min EJB:

> mvn package

Därefter så deployar jag den på Glassfish:

> asadmin deploy /path/to/my/jar/ejb3demo.jar

Så här ser klienten ut:

import javax.naming.InitialContext;
import se.cygni.*;

public class GlassfishClient {

   public static void main(String[] args) {
      try {
         InitialContext ctx = new InitialContext();
         Object obj = ctx.lookup("se.cygni.CalculatorRemote");

         if(obj == null){
            System.out.println("obj is null");
            System.exit(-1);
         }

         if(!(obj instanceof CalculatorRemote)){
            System.out.println("obj wrong type?! -> " + obj.getClass());
            System.exit(-2);
         }

         CalculatorRemote cal = (CalculatorRemote) obj;
         System.out.println(" 3 + 9 = " + cal.add(3, 9));
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

Som ni ser så använder sig Glassfish av ett annat schema än vad OpenEJB gör för JNDI-namnen. Jag kompilerar koden och sen kör jag den. När det är dags att köra koden så lägg till följande jarfiler till classpath:en, $GLASSFISH_HOME/lib/appserv-rt.jar och $GLASSFISH_HOME/lib/javaee.jar. I klientkoden så skapar jag en ”tom” InitialContext, detta gör jag för att Glassfish lagt en jndi.properties i appserv-rt.jar med alla JNDI-properties och JNDI-maskineriet i Java SE klarar av att hitta dessa när man skapar en tom InitialContext. När jag kör klientkoden får jag följande resultat:

3 + 9 = 12

Coolt, det funkade även i Glassfish!

WebService-exempel
Med EJB3 så kan man även väldigt enkelt publicera sin stateless sessionsböna som en webservice. I den allra enklaste formen så behövs det inte mer än en extra annotering, javax.jws.WebService. För att testa detta så modifierar jag Calculator-bönan som jag skapade tidigare med följande kod:

package se.cygni;

import javax.ejb.Stateless;
import javax.jws.WebService;

@Stateless
@WebService
public class CalculatorBean implements Calculator, CalculatorRemote {
...
}

Som ni ser så är den enda skillnaden att annoteringen @WebSerivce har tillkommit. Jag testar att det verkligen fungerar genom att packa ihop EJB:en och deploya den på Glassfish

>mvn package
>asadmin deploy /path/to/my/jar/ejb3demo.jar

Nu kan jag kolla på WSDL-filen genom att gå till följande adress i min browser, http://localhost:8080/CalculatorBeanService/CalculatorBean?wsdl

Message-Driven Bean
Message-driven-bönor kom till i EJB 2.0 och stödjer asynkron hantering av JMS meddelanden. I EJB 2.1 så las det till så att en MDB stödjer andra meddelandesystem och inte bara JMS med hjälp av JCA. I EJB 3.0 så har det inte tillkommit så många nyheter förutom att konfigurationen har förenklats med hjälp av annoteringar. Som exempel tänkte jag skapa en enkel MDB som lyssnar på en kö och bara skriver ut ett meddelande till System.out. Jag kommer även skapa en enkel klient som skickar meddelanden till kön som min MDB lyssnar på. Jag börjar med min MDB:

package se.cygni;

import javax.ejb.MessageDriven;
import javax.jms.Message;
import javax.jms.MessageListener;

@MessageDriven(mappedName="MDBDemoQueue")
public class MDB implements MessageListener {

   @Override
   public void onMessage(Message msg) {
      System.out.println("Message received!!!");
   }
}

Väldigt enkel klass. Jag annoterar den med annoteringen javax.ejb.MessageDriven och sätter annoteringselementet mappedName till MDBDemoQueue. Detta är JNDI-namnet på JMS-kön som min MDB skall lyssna på. MDB:n implementerar interfacet javax.jms.MessageListener som har en metod, onMessage(Message). Denna metod anropas varje gång det kommer in ett nytt meddelande i kön MDBDemoQueue. Allt jag gör i den metoden är att jag skriver till System.out. Jag packar ihop min ejb-jar och deployar den på Glassfish.

Först ser jag till att Glassfish är startad

> asadmin start-domain

Sen så skapar jag JMS-kön som min MDB skall lyssna på

> asadmin create-jms-resource --restype javax.jms.Queue --property imqDestinationName=MDBDemoQueue MDBDemoQueue

Jag skapar även en JMS QueueConnectionFactory som min testklient kommer att använda sig utav

> asadmin create-jms-resource --restype javax.jms.QueueConnectionFactory MDBDemoQueueConnectionFactory

Tillsist så deployar jag min MDB

> asadmin deploy /path/to/ejb3mdbdemo.jar

Nu måste jag bara skapa en klient som skickar ett meddelande till kön som min MDB lyssnar på. Jag skapar en Java EE appclient, här är den koden:

package se.cygni;

import javax.annotation.Resource;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.TextMessage;

public class MDBDemoClient {

   @Resource(mappedName="MDBDemoQueueConnectionFactory")
   private static QueueConnectionFactory qcf;

   @Resource(mappedName="MDBDemoQueue")
   private static Queue mdbQueue;

   public static void main(String args[]) {
      try {
         QueueConnection queueCon = qcf.createQueueConnection();
         QueueSession queueSession = queueCon.createQueueSession(false,
Session.AUTO_ACKNOWLEDGE);
         QueueSender queueSender = queueSession.createSender(null);
         TextMessage msg = queueSession.createTextMessage("hello");
         queueSender.send(mdbQueue, msg);
         System.out.println("Sent message to MDB");
         queueCon.close();
      } catch(Exception e) {
         e.printStackTrace();
      }
   }
}

Inga konstigheter. Jag använder DI för att få tag i QueueConnectionFactory och Queue. För att nu testa min MDB så packar jag ihop klienten i en jar-fil och stämplar in i META-INF/MANIFEST.MF, Main-Class: se.cygni.MDBDemoClient och sedan kör jag klienten:

> $GLASSFISH_HOME/bin/appclient -client mdbdemoappclient.jar
----> Sent message to MDB

När jag kollar i Glassfish server.log så hittar jag texten:

[#|2008-10-16T12:30:55.555+0000|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=18;_ThreadName=p: thread-pool-1; w: 12;|Message received!!!|#]

Vad bra, allt fungerade.

Interceptors
En annan nyhet i EJB3 är Interceptors. En interceptor är en metod som genskjuter anrop till business metoder eller lifecycle callback händelser. En interceptormetod kan definieras direkt i EJB-klassen eller i en interceptor klass som associeras med EJB-klassen via annoteringen javax.interceptor.Interceptors. Det enda kravet som finns på en klass som skall vara en interceptor är att det finns en publik konstruktor som inte tar några argument. I övrigt så gäller följande för en interceptor:

  • Anrop till interceptormetoder för businessmetoder sker inom samma transaktion och samma säkerhetskontext som businessmetoden som anropas körs i.
  • Inreceptormetoder för businessmetoder kan slänga RuntimeException eller applikations exceptions som är tillåtna i businessmetodens throws klasul.
  • DI stöds för interceptorklasser
  • Interceptorer kan anropa JNDI, JDBC, JMS, andra EJB:er och Entity Manager. Interceptormetoder delar JNDI namnrymden med EJB:n för vilken den är anropad.

Detta måste självklart testas. Jag börjar med att skapa en ny klass Auditor som skall fungera som interceptor för CalculatorBean.

package se.cygni;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

public class Auditor {
   @AroundInvoke
   public Object audit(InvocationContext ctx) throws Exception {
      try {
         Object result = ctx.proceed();
         System.out.println("[Auditor: Target=" + ctx.getTarget() +
", Method=" + ctx.getMethod().getName() +
", Params=" + ctx.getParameters() + "]");
         return result;
      } catch (Exception e) {
         System.out.println("[Auditor: Exception=" + e.getMessage() + "]");
         throw e;
      }
   }
}

Auditor-klassen har en metod som annoteras med annoteringen javax.interceptor.AroundInvoke. En AroundInvoke-metod har följande signatur public Object (InvocationContext) throws Exception. Denna metod kommer att anropas för varje anrop till businessmetoder i den EJB som interceptorklassen associeras med, om man inte explicit annoterar en businessmetod med annoteringen javax.interceptor.ExcludeClassInterceptors. Interceptorklassen associeras med CalculatorBean med hjälp av annoteringen javax.interceptor.Interceptors på följande sätt:

package se.cygni;

import javax.ejb.Stateless;
import javax.interceptor.ExcludeClassInterceptors;
import javax.interceptor.Interceptors;
import javax.jws.WebService;

@Stateless
@WebService
@Interceptors({Auditor.class})
public class CalculatorBean implements Calculator, CalculatorRemote {

   @ExcludeClassInterceptors
   public int add(int v1, int v2) {
      return v1 + v2;
   }

   ...
}

Jag annoterar även businessmetoden add med annoteringen javax.interceptor.ExcludeClassInterceptors för att verkligen se att inte interceptorklassen anropas när denna metod anropas. När jag nu kör mitt JUnit test för denna EJB så får jag följande resultat:

[Auditor: [email protected], Method=subtract, Params=[Ljava.lang.Object;@1976011]
[Auditor: [email protected], Method=multiply, Params=[Ljava.lang.Object;@137d090]
[Auditor: [email protected], Method=divide, Params=[Ljava.lang.Object;@1a68ef9]
[Auditor: Exception=zero not allowed]
[Auditor: [email protected], Method=subtract, Params=[Ljava.lang.Object;@de1b8a]
[Auditor: Target=se.cygni.Cal[email protected], Method=multiply, Params=[Ljava.lang.Object;@17f409c]
[Auditor: [email protected], Method=divide, Params=[Ljava.lang.Object;@13c6a22]
[Auditor: Exception=zero not allowed]
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0

Som ni ser så anropas aldrig interceptorklassen när businessmetoden add anropas.

Integration med Spring
Som en sista grej tänkte jag visa hur enkelt det är att integrera Spring i en EJB3 applikation. Från och med version 2.5.1 av Spring så finns det en interceptorklass, org.springframework.ejb.interceptor.SpringBeanAutowiringInterceptor som man använder sig utav. Interceptorklassen använder man på de EJB:er där man vill att Spring-bönor skall bli dependency injected med hjälp av Spring’s annotering org.springframework.beans.factory.annotation.Autowired. I övrigt så behövs det en extra xml-fil, beanRefContext.xml och den vanliga Spring-konfigurationsfilen. För att testa detta så börjar jag med att skapa en enkel POJO, MySpringBean:

package se.cygni;

public class MySpringBean {
   public String sayHello(){
      return "Hello from Spring Managed Bean!";
   }
}

Jag deklarerar en ny metod, sayHelloSpring(), på mitt Calculator-interface:

package se.cygni;

public interface Calculator {
   int add(int v1, int v2);
   int subtract(int v1, int v2);
   int multiply(int v1, int v2);
   double divide(int v1, int v2) throws Exception;
   String sayHelloSpring();
}

I CalculatorBean gör jag följande ändringar, lägger till ett beroende till MySpringBean som jag annoterar med Spring’s Autowired annotering. Jag associserar även Spring’s interceptorklass med min EJB och jag implementerar sayHelloSpring() metoden som jag la till i interfacet.

@Stateless
@WebService
@Interceptors({Auditor.class, SpringBeanAutowiringInterceptor.class})
public class CalculatorBean implements Calculator, CalculatorRemote {

   @Autowired
   private MySpringBean mySpringBean;

   ...

   public String sayHelloSpring() {
      return mySpringBean.sayHello();
   }
}

Nu skapar jag filerna beanRefContext.xml och application.xml. Dessa skall finnas tillgängliga direkt på classpath:en. I mitt fall så skapar jag dem i src/main/resources.

beanRefContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

  <bean id="factoryKey"
class="org.springframework.context.support.ClassPathXmlApplicationContext">
        <constructor-arg value="application.xml"/>
    </bean>

</beans>

Till sist så skapar jag två nya testmetoder i min CalculatorTest-klass, en för det lokala interfacet och en för det remote:a interfacet:

public class CalculatorTest {
   ...
   @Test
   public void springLocal() throws Exception {
      Calculator cal = getLocal();
      assertEquals("Hello from Spring Managed Bean!",
cal.sayHelloSpring());
   }
   ...
   @Test
   public void springRemote() throws Exception {
      CalculatorRemote cal = getRemote();
      assertEquals("Hello from Spring Managed Bean!",
cal.sayHelloSpring());
   }
}

När jag kör mitt test för jag följande resultat:

Tests run: 10, Failures: 0, Errors: 0, Skipped: 0

Sammanfattning
Har man använt sig utav tidigare versioner av EJB så kommer man verkligen att uppskatta EJB3. Om man aldrig tidigare har använt sig utav EJB och man börjar med EJB3 direkt, så kommer man inte att förstå vad de gammla stofilerna snackar om när de säger att det är bökigt och komplicerat att utveckla EJB-applikationer.

Referenser