Skip to content

6. Wersja 2: Architektura OpenEJB / JPA

6.1. Wprowadzenie do zasad portowania

W niniejszym dokumencie przedstawiamy zasady, które będą regulować proces przenoszenia aplikacji JPA / Spring / Hibernate do aplikacji JPA / OpenEJB / EclipseLink. Tworzenie projektów Maven zostanie omówione w punkcie 6.2.

6.1.1. Dwie architektury

Obecna implementacja z wykorzystaniem Spring / Hibernate

Implementacja do stworzenia z wykorzystaniem OpenEJB / EclipseLink

6.1.2. Biblioteki projektów

  • Warstwy [DAO] i [metier] nie są już instancjonowane przez Spring. Są one instancjonowane przez kontener OpenEJB.
  • Biblioteki kontenera Spring oraz jego konfiguracja zostają zastąpione bibliotekami kontenera OpenEJB oraz jego konfiguracją.
  • Biblioteki warstwy JPA / Hibernate są zastępowane bibliotekami warstwy JPA / EclipseLink

  • Plik [META-INF/persistence.xml] konfigurujący warstwę JPA przyjmuje następującą postać:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="pam-openejb-ui-metier-dao-jpa-eclipselinkPU" transaction-type="JTA">
     <!-- entities JPA -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
     <!-- dostawcą JPA jest EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
     <!-- właściwości dostawcy -->
    <properties>
      <property name="eclipselink.ddl-generation" value="create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • wiersz 3: transakcje w kontenerze EJB są typu JTA (transakcja Java API). W przypadku Springa były one typu RESOURCE_LOCAL.
  • wiersz 9: używana implementacja JPA to EclipseLink
  • wiersze 5–7: encje zarządzane przez warstwę JPA
  • wiersze 11–13: właściwości dostawcy EclipseLink
  • wiersz 12: przy każdym uruchomieniu zostaną utworzone tabele

Charakterystyki JDBC źródła danych JTA, z którego korzysta kontener OpenEJB, zostaną określone przez następujący plik konfiguracyjny [conf/openejb.conf]:

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<openejb>
  <Resource id="Default JDBC Database">
    JdbcDriver com.mysql.jdbc.Driver
    JdbcUrl jdbc:mysql://localhost:3306/dbpam_eclipselink
    UserName root
  </Resource>
</openejb>
  • wiersz 3: identyfikator „Default JDBC Database” jest używany podczas pracy z kontenerem OpenEJB wbudowanym (embedded) w samą aplikację.
  • wiersz 5: używamy bazy MySQL [dbpam_eclipselink]

6.1.4. Implementacja warstwy [DAO] za pomocą EJB

  • Klasy implementujące warstwę [DAO] stają się klasami EJB. Weźmy na przykład klasę [CotisationDao]:

Interfejs [ICotisationDao] w wersji Spring wyglądał następująco:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
   // utwórz nową składkę
  Cotisation create(Cotisation cotisation);
   // edytuj istniejącą składkę
  Cotisation edit(Cotisation cotisation);
   // usunięcie istniejącej składki
  void destroy(Cotisation cotisation);
   // wyszukanie konkretnej składki
  Cotisation find(Long id);
   // pobierz wszystkie obiekty składki
  List<Cotisation> findAll();

}

EJB będzie implementować ten sam interfejs w dwóch różnych formach: lokalnej i zdalnej. Interfejs lokalny może być używany przez klienta działającego w tym samym JVM, natomiast interfejs zdalny – przez klienta działającego w innym JVM.

Interfejs lokalny:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}
  • wiersz 6: interfejs [ICotisationDaoLocal] dziedziczy po interfejsie [ICotisationDao], przejmując wszystkie jego metody. Nie dodaje żadnych nowych.
  • wiersz 5: adnotacja @Local sprawia, że jest to interfejs lokalny dla interfejsu EJB, który go zaimplementuje.

Interfejs zdalny:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}
  • wiersz 6: interfejs [ICotisationDaoRemote] dziedziczy po interfejsie [ICotisationDao], przejmując wszystkie jego metody. Nie dodaje żadnych nowych.
  • wiersz 5: adnotacja @Remote sprawia, że jest to interfejs zdalny dla klasy EJB, która go zaimplementuje.

Warstwa [DAO] jest implementowana przez klasę EJB, która implementuje oba interfejsy (nie jest to obowiązkowe):

1
2
3
4
5
6
@Stateless()
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class CotisationDao implements ICotisationDaoLocal, ICotisationDaoRemote {

  @PersistenceContext
  private EntityManager em;
  • wiersz 1: adnotacja @Stateless, która sprawia, że klasa staje się EJB
  • wiersz 2: adnotacja @TransactionAttribute, która sprawia, że każda metoda tej klasy będzie wykonywana w ramach transakcji.
  • wiersz 5: adnotacja @PersistenceContext, która wstrzykuje do klasy [CotisationDao] obiekt EntityManager z warstwy JPA. Jest ona identyczna z tą, którą mieliśmy w wersji Spring.

Gdy wykorzystywany jest lokalny interfejs warstwy [DAO], klient tego interfejsu działa w tej samej warstwie JVM.

W powyższym przykładzie warstwy [metier] i [DAO] wymieniają obiekty przez odwołanie. Gdy jedna warstwa zmienia współdzielony obiekt, druga warstwa widzi tę zmianę.

Gdy wykorzystywany jest interfejs zdalny warstwy [DAO], klient tego interfejsu zazwyczaj działa w innej warstwie JVM.

W powyższym przykładzie warstwy [metier] i [DAO] wymieniają obiekty przez wartość (serializacja wymienianego obiektu). Gdy jedna warstwa zmienia obiekt współdzielony, druga warstwa dostrzega tę zmianę tylko wtedy, gdy zmodyfikowany obiekt zostanie jej ponownie przesłany.

6.1.5. Implementacja warstwy [metier] przez warstwę EJB

  • Klasa implementująca warstwę [metier] również staje się klasą EJB, implementującą interfejs lokalny i zdalny. Pierwotny interfejs [IMetier] wyglądał następująco:
package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
   // pobierz listę płac
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // lista pracowników
  List<Employe> findAllEmployes();
}

Tworzymy interfejs lokalny i zdalny na podstawie poprzedniego interfejsu:

1
2
3
4
5
6
7
package metier;

import javax.ejb.Local;

@Local
public interface IMetierLocal extends IMetier{
}
1
2
3
4
5
6
7
package metier;

import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{
}

Klasa EJB z warstwy [metier] implementuje te dwa interfejsy:

@Stateless()
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote {

   // informacje o lokalnych warstwach [DAO]
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;
  • wiersze 1–2: definiują klasę EJB, której każda metoda jest wykonywana w transakcji.
  • wiersz 7: odwołanie do lokalnego interfejsu EJB w [CotisationDao].
  • wiersz 6: adnotacja @EJB nakazuje, aby kontener EJB wstrzyknął odwołanie do lokalnego interfejsu EJB [CotisationDao].
  • wiersze 8–11: powtarzamy tę samą procedurę dla interfejsów lokalnych obiektów EJB, [EmployeDao] i [IndemniteDao].

Ostatecznie, gdy instancja EJB i [Metier] zostanie utworzona, pola w wierszach 7, 9 i 11 zostaną zainicjowane odwołaniami do lokalnych interfejsów trzech obiektów EJB warstwy [DAO]. Zakładamy zatem, że warstwy [metier] i [DAO] będą wykonywane w tej samej warstwie JVM.

6.1.6. Klienci EJB

Na powyższym schemacie, aby komunikować się z warstwą [metier], warstwa [ui] musi uzyskać odwołanie do zdalnego interfejsu EJB z warstwy [metier].

Na powyższym schemacie, aby komunikować się z warstwą [metier], warstwa [ui] musi uzyskać odwołanie do interfejsu lokalnego warstwy EJB z warstwy [metier]. Sposób uzyskania tych odwołań różni się w zależności od kontenera. W przypadku kontenera OpenEJB można postępować w następujący sposób:

Odwołanie w interfejsie lokalnym:

1
2
3
4
5
6
7
8
9
     // konfiguracja wbudowanego kontenera Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // inicjalizacja kontekstu JNDI z poprzednimi właściwościami
    InitialContext initialContext = new InitialContext(properties);
     // tworzenie instancji warstw DAO
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
  • wiersze 2–5: inicjowany jest kontener OpenEJB.
  • wiersz 5: mamy kontekst JNDI (Java Naming and Directory Interface), który pozwala uzyskać odniesienia do obiektów EJB. Każdy obiekt EJB jest oznaczony nazwą JNDI:
  • (ciąg dalszy)
    • w przypadku interfejsu lokalnego do nazwy EJB dodaje się przedrostek „Local” (wiersze 7–9)
    • w przypadku interfejsu zdalnego do nazwy EJB dodaje się „Remote”

W przypadku Java EE 5 zasady te zmieniają się w zależności od kontenera EJB. Stanowi to pewną trudność. W wersji Java EE 6 wprowadzono notację JNDI, która jest przenośna na wszystkie serwery aplikacji.

Powyższy kod pobiera odwołania do lokalnych interfejsów EJB na podstawie ich nazw JNDI. Jak wspomnieliśmy wcześniej, można je również uzyskać za pomocą adnotacji @EJB. Można by zatem napisać:

@EJB
private IemployeDaoLocal employeDaoLocal ;

Adnotacja @EJB jest honorowana tylko wtedy, gdy należy do klasy ładowanej przez kontener EJB. Tak będzie na przykład w przypadku klasy [Metier]. Powyższy kod będzie natomiast należał do klasy konsoli, która nie zostanie załadowana przez kontener EJB. Jesteśmy zatem zmuszeni do użycia nazw JNDI lub EJB.

Poniżej znajduje się kod umożliwiający uzyskanie odwołania do zdalnego interfejsu EJB i [Metier]:

1
2
3
4
5
6
7
8
     // konfiguracja wbudowanego kontenera Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // inicjalizacja kontekstu JNDI kontenera EJB
    InitialContext initialContext = new InitialContext(properties);

     // instancjonowanie zdalnej warstwy biznesowej
metier = (IMetierRemote) initialContext.lookup("MetierRemote");

6.2. Ćwiczenie praktyczne

Proponujemy przeniesienie aplikacji NetBeans Spring / Hibernate do architektury OpenEJB / EclipseLink.

Obecna implementacja z wykorzystaniem Spring / Hibernate

Implementacja, którą należy stworzyć przy użyciu OpenEJB / EclipseLink

Jeśli baza danych nie istnieje, należy ją utworzyć za pomocą MySQL [dbpam_eclipselink]. Jeśli baza danych istnieje, należy usunąć wszystkie jej tabele. Należy utworzyć połączenie NetBeans z tą bazą danych zgodnie z opisem w punkcie 6.2.1.

6.2.2. Wstępna konfiguracja projektu w NetBeans

  • załaduj projekt Maven o nazwie [mv-pam-spring-hibernate]
  • utwórz nowy projekt Maven Java o nazwie [mv-pam-openejb-eclipselink] [1]
  • w zakładce [Files] [2] utworzyć folder [conf] [3] w katalogu głównym projektu
  • umieścić w tym folderze następujący plik [openejb.conf] [4]:
1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<openejb>
  <Resource id="Default JDBC Database">
    JdbcDriver com.mysql.jdbc.Driver
    JdbcUrl jdbc:mysql://localhost:3306/dbpam_eclipselink
    UserName root
  </Resource>
</openejb>
  • utworzyć folder [src / main/ resources/ META-INF] [5]
  • umieścić w nim następujące pliki: [persistence.xml], [6]:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence              http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="dbpam_eclipselinkPU" transaction-type="JTA">
    <!-- dostawcą JPA jest EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- podmioty Jpa -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- właściwości dostawcy EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • wiersz 12: żądamy szczegółowych logów w pliku EclipseLink,
  • wiersz 13: tabele zostaną utworzone podczas instancjonowania warstwy JPA,
  • dodaj biblioteki OpenEJB, EclipseLink oraz sterownik JDBC z pliku MySQL do pliku [pom.xml] projektu:

<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>istia.st</groupId>
  <artifactId>mv-pam-openejb-eclipselink</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-pam-openejb-eclipselink</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.openejb</groupId>
      <artifactId>openejb-core</artifactId>
      <version>4.0.0</version>
    </dependency>                
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>eclipselink</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>javax.persistence</artifactId>
      <version>2.0.3</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
  
  <repositories>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>
  </repositories>
  
</project>
  • wiersze 18–22: zależność OpenEJB,
  • wiersze 30–39: zależności EclipseLink,
  • wiersze 41–44: zależność sterownika JDBC od MySQL

6.2.3. Portowanie warstwy [DAO]

Przeprowadzimy przeniesienie warstwy [DAO] poprzez skopiowanie pakietów z projektu [mv-pam-spring-hibernate] do projektu [mv-pam-openejb-eclipselink].

  • skopiuj pakiety [dao, exception, jpa]
 

Wymienione powyżej błędy wynikają z faktu, że skopiowana warstwa [DAO] korzysta z biblioteki Spring, a biblioteki Spring nie są już częścią projektu.

6.2.3.1. EJB [CotisationDao]

Tworzymy interfejsy lokalny i zdalny przyszłego EJB [CotisationDao]:

Interfejs lokalny ICotisationDaoLocal:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}

Aby uzyskać odpowiednie pakiety import, należy wykonać [clic droit sur le code / Fix Imports].

Interfejs zdalny ICotisationDaoRemote:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}

Następnie modyfikujemy klasę [CotisationDao], aby przekształcić ją w EJB:

...
import javax.persistence.PersistenceContext;
import jpa.Cotisation;

@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class CotisationDao implements ICotisationDaoLocal, ICotisationDaoRemote {

  @PersistenceContext
  private EntityManager em;
...  

Interfejs import, który ta klasa tworzyła w frameworku Spring, znika. Należy utworzyć interfejs [Clean and Build] dla projektu:

W pliku [1] nie ma już błędów dotyczących klasy [CotisationDao].

6.2.3.2. Pliki EJB, [EmployeDao] i [IndemniteDao]

Powtarzamy tę samą procedurę dla pozostałych elementów warstwy [DAO]:

  • interfejsy IEmployeDaoLocal, IEmployeDaoRemote wywodzące się z IEmployeDao
  • EJB i EmployeDao, implementujące te dwa interfejsy
  • interfejsy IIndemniteDaoLocal, IIndemniteDaoRemote pochodne od IIndemniteDao
  • EJB, IndemniteDao implementujące te dwa interfejsy

Po wykonaniu tych czynności w projekcie [2] nie ma już żadnych błędów.

6.2.3.3. Klasa [PamException]

Klasa [PamException] pozostaje taka sama, z jednym wyjątkiem:

package exception;

import javax.ejb.ApplicationException;

@ApplicationException(rollback=true)
public class PamException extends RuntimeException {

   // kod błędu
  private int code;
...

Dodano wiersz 5. Aby uzyskać prawidłowe import, należy wykonać [Fix imports].

Aby zrozumieć adnotację w wierszu 5, należy pamiętać, że każda metoda klasy EJB z naszej warstwy [DAO]:

  • jest wykonywana w transakcji rozpoczętej i zakończonej przez kontener EJB
  • rzuca wyjątek typu [PamException], gdy tylko coś pójdzie nie tak

Gdy warstwa [metier] wywołuje metodę M warstwy [DAO], wywołanie to jest przechwytywane przez kontener EJB. Wszystko przebiega tak, jakby między warstwą [metier] a warstwą [DAO] znajdowała się klasa pośrednicząca, nazwana tutaj [Proxy EJB], przechwytująca wszystkie wywołania skierowane do warstwy [DAO]. Gdy wywołanie metody M warstwy [DAO] zostaje przechwycone, proxy EJB rozpoczyna transakcję, a następnie przekazuje kontrolę metodzie M warstwy [DAO], która następnie jest wykonywana w ramach tej transakcji. Metoda M kończy się z wyjątkiem lub bez niego.

  • jeśli metoda M zakończy się bez wyjątku, wykonanie wraca do proxy EJB, które kończy transakcję, zatwierdzając ją za pomocą commit. Przepływ wykonania jest następnie przekazywany z powrotem do wywołującej metody warstwy [metier]
  • jeśli metoda M zakończy się wyjątkiem, wykonanie wraca do proxy EJB, które kończy transakcję, unieważniając ją za pomocą rollback. Ponadto proxy to enkapsuluje ten wyjątek w typie EJBException. Następnie przepływ wykonania jest przekazywany z powrotem do metody wywołującej z warstwy [metier], która w ten sposób otrzymuje obiekt typu EJBException. Adnotacja w wierszu 5 powyżej uniemożliwia to enkapsulowanie. Warstwa [metier] otrzyma zatem obiekt typu PamException. Ponadto atrybut rollback=true wskazuje proxy EJB, że po otrzymaniu obiektu typu PamException musi unieważnić transakcję.

6.2.3.4. Test warstwy [DAO]

Nasza warstwa [DAO], zaimplementowana przez EJB, jest gotowa do testów. Zaczynamy od skopiowania pakietu [dao] z [Test Packages] z projektu [mv-pam-springhibernate] do projektu [1], który jest obecnie w trakcie tworzenia:

Zachowujemy jedynie test [JUnitInitDB], który inicjuje bazę danymi z pliku [2]. Zmieniamy nazwę klasy [ JUnitInitDbLocal] na [3]. Klasa [JUnitInitDBLocal] będzie korzystać z lokalnego interfejsu klasy EJB z warstwy [DAO].

Najpierw modyfikujemy klasę [JUnitInitDBLocal] w następujący sposób:

public class JUnitInitDBLocal {

  static private IEmployeDaoLocal employeDao = null;
  static private ICotisationDaoLocal cotisationDao = null;
  static private IIndemniteDaoLocal indemniteDao = null;

  @BeforeClass
  public static void init() throws Exception {
     // konfiguracja wbudowanego kontenera Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // inicjalizacja kontekstu JNDI z poprzednimi właściwościami
    InitialContext initialContext = new InitialContext(properties);
     // utworzenie lokalnych warstw DAO
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
}

...
  • wiersze 3–5: odwołania do lokalnych interfejsów klasy EJB w warstwie [DAO]
  • wiersz 7: @BeforeClass opisuje metodę wykonywaną podczas uruchamiania testu JUnit
  • wiersze 10–13: inicjalizacja kontenera OpenEJB. Inicjalizacja ta jest specyficzna dla danego kontenera i zmienia się wraz z każdym kontenerem EJB.
  • wiersz 13: mamy kontekst JNDI (Java Naming and Directory Interface), który umożliwia dostęp do obiektów EJB za pomocą nazw. W przypadku OpenEJB lokalny interfejs obiektu EJB E jest oznaczony jako ELocal, a interfejs zdalny jako ERemote.
  • wiersze 15–17: od kontekstu JNDI żąda się odniesienia do interfejsów lokalnych EJB i [EmployeDao, CotisationDao, IndemniteDao].

Kompilujemy projekt (Build), w razie potrzeby uruchamiamy serwer MySQL, a następnie wykonujemy test JUnitInitDBLocal. Należy pamiętać, że plik [persistence.xml] został skonfigurowany tak, aby odtwarzać tabele przy każdym uruchomieniu. Przed uruchomieniem testu zaleca się usunięcie ewentualnych tabel z bazy MySQL i [dbpam_eclipselink].

  • w [1], w zakładce [Services] usuwa się tabele z połączenia NetBeans ustanowionego w punkcie 6.2.1.
  • w pliku [2] baza danych [dbpam_eclipselink] nie zawiera już żadnych tabel
  • w pliku [3] projekt zostaje skompilowany
  • w [4] uruchamiany jest test JUnitInitDBLocal
  • w [5] test zakończył się powodzeniem
  • w [6] odświeżono połączenie z NetBeans
  • w [7] widoczne są 4 tabele utworzone przez warstwę JPA. Celem testu było ich wypełnienie. Wyświetlana jest zawartość jednej z nich
  • w [8], zawartość tabeli [EMPLOYES]

Kontener OpenEJB wyświetlił logi w konsoli:

Infos - PersistenceUnit(name=dbpam_eclipselinkPU, provider=org.eclipse.persistence.jpa.PersistenceProvider) - provider time 396ms
Infos - Jndi(name=CotisationDaoLocal) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao!dao.ICotisationDaoLocal) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=CotisationDaoRemote) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao!dao.ICotisationDaoRemote) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=EmployeDaoLocal) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao!dao.IEmployeDaoLocal) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=EmployeDaoRemote) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao!dao.IEmployeDaoRemote) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=IndemniteDaoLocal) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao!dao.IIndemniteDaoLocal) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=IndemniteDaoRemote) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao!dao.IIndemniteDaoRemote) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao) --> Ejb(deployment-id=IndemniteDao)
Infos - existing thread singleton service in SystemInstance() org.apache.openejb.cdi.ThreadSingletonServiceImpl@624a240d
Infos - OpenWebBeans Container is starting...
Infos - Adding OpenWebBeansPlugin : [CdiPlugin]
Infos - All injection points were validated successfully.
Infos - OpenWebBeans Container has started, it took [70] ms.
Infos - Created Ejb(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
Infos - Created Ejb(deployment-id=EmployeDao, ejb-name=EmployeDao, container=Default Stateless Container)
Infos - Created Ejb(deployment-id=CotisationDao, ejb-name=CotisationDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=EmployeDao, ejb-name=EmployeDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=CotisationDao, ejb-name=CotisationDao, container=Default Stateless Container)
Infos - Deployed Application(path=D:\data\istia-1112\netbeans\glassfish\mv-pam\tmp\mv-pam-openejb-eclipselink\classpath.ear)
  • wiersze 2–3: dwie nazwy JNDI z EJB i [CotisationDaoLocal],
  • wiersze 4–5: dwie nazwy JNDI z EJB i [CotisationDaoRemote],
  • wiersze 7–8: dwie nazwy JNDI z EJB [EmployeDaoLocal],
  • wiersze 9–10: dwie nazwy JNDI z EJB i [EmployeDaoRemote],
  • wiersze 12–13: dwie nazwy JNDI z EJB i [IndemniteDaoLocal],
  • wiersze 14–15: dwie nazwy JNDI z EJB i [EmployeDaoRemote].

Powtarzamy ten sam test, tym razem korzystając ze zdalnego interfejsu EJB.

W [1] klasa [JUnitInitDBLocal] została skopiowana (kopiuj/wklej) do [JUnitInitDBRemote]. W tej klasie zastępujemy interfejsy lokalne interfejsami zdalnymi:

public class JUnitInitDBRemote {

  static private IEmployeDaoRemote employeDao = null;
  static private ICotisationDaoRemote cotisationDao = null;
  static private IIndemniteDaoRemote indemniteDao = null;

  @BeforeClass
  public static void init() throws Exception {
     // konfiguracja wbudowanego kontenera Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // inicjalizacja kontekstu JNDI z poprzednimi właściwościami
    InitialContext initialContext = new InitialContext(properties);
     // instancjonowanie warstw zdalnych DAO
    employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
}

Po wykonaniu tej czynności można uruchomić nową klasę testową. Wcześniej, korzystając z połączenia NetBeans o nazwie [dbpam_eclipselink], należy usunąć tabele z bazy danych [dbpam_eclipselink].

 

Korzystając z połączenia NetBeans o nazwie [dbpam_eclipselink], należy sprawdzić, czy baza została wypełniona.

6.2.4. Przeniesienie warstwy [metier]

Przeprowadzimy przeniesienie warstwy [metier] poprzez skopiowanie pakietów z projektu [mv-pam-spring-hibernate] do projektu [mv-pam-openejb-eclipselink].

Wspomniane powyżej błędy [1] wynikają z faktu, że skopiowana warstwa [metier] korzysta z biblioteki Spring, a biblioteki Spring nie są już częścią projektu.

6.2.4.1. EJB [Metier]

Postępujemy tak samo, jak opisano w przypadku EJB [CotisationDao]. Najpierw tworzymy w [2] interfejsy lokalny i zdalny przyszłego EJB [Metier]. Oba wywodzą się z pierwotnego interfejsu [IMetier].

1
2
3
4
5
6
7
8
package metier;

import javax.ejb.Local;

@Local
public interface IMetierLocal extends IMetier{

}
1
2
3
4
5
6
7
8
package metier;

import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{

}

Po wykonaniu tych czynności w pliku [3] modyfikujemy klasę [Metier], tak aby stała się klasą EJB:

@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote {

   // odwołania do warstwy lokalnej [DAO]
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;

   // pobieranie listy płac
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
          double nbHeuresTravaillées, int nbJoursTravaillés) {
     // pobieranie informacji dotyczących pracownika
...
  • wiersz 1: adnotacja @Stateless sprawia, że klasa staje się EJB
  • wiersz 2: każda metoda tej klasy będzie wykonywana w ramach transakcji
  • wiersz 3: klasa EJB [Metier] implementuje oba interfejsy – lokalny i zdalny – które właśnie zdefiniowaliśmy
  • wiersz 7: klasa EJB [Metier] będzie korzystać z klasy EJB [CotisationDao] poprzez jej interfejs lokalny. Oznacza to, że warstwy [metier] i [DAO] muszą być uruchamiane w tej samej instancji JVM.
  • wiersz 6: adnotacja @EJB sprawia, że kontener EJB sam wstrzykuje odwołanie do lokalnego interfejsu EJB [CotisationDao]. Innym sposobem, z którym się spotkaliśmy, jest użycie kontekstu JNDI.
  • wiersze 8–11: ten sam mechanizm jest stosowany w przypadku dwóch pozostałych obiektów EJB warstwy [DAO].

6.2.4.2. Test warstwy [metier]

Możemy przetestować naszą warstwę [metier] zaimplementowaną przez EJB. Zaczynamy od skopiowania pakietu [metier] z [Test Packages] z projektu [mv-pam-spring-hibernate] do aktualnie tworzonego projektu [1]:

  • do [1], wynik kopiowania
  • do [2], usuwa się pierwszy test
  • w [3] pozostały test zostaje przemianowany na [JUnitMetierLocal]

Klasa [JUnitMetierLocal] przyjmuje następujący kształt:

public class JUnitMetierLocal {

// lokalna warstwa biznesowa
  static private IMetierLocal metier;

  @BeforeClass
  public static void init() throws NamingException {
     // konfiguracja wbudowanego kontenera Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // inicjalizacja kontekstu JNDI z poprzednimi właściwościami
    InitialContext initialContext = new InitialContext(properties);

     // instancjonowanie lokalnych warstw DAO
    IEmployeDaoLocal employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    ICotisationDaoLocal cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    IIndemniteDaoLocal indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
     // tworzenie instancji lokalnej warstwy biznesowej
    metier = (IMetierLocal) initialContext.lookup("MetierLocal");

     // opróżnianie bazy danych
...
}
  • wiersz 4: odwołanie do lokalnego interfejsu EJB [Metier]
  • wiersze 8–12: konfiguracja kontenera OpenEJB identyczna z tą wykonaną w teście warstwy [DAO]
  • wiersze 15–19: wysyłane są żądania do kontekstu JNDI z wiersza 12, odniesienia do trzech obiektów EJB z warstwy [DAO] oraz do obiektu EJB z warstwy [metier]. Elementy EJB z warstwy [DAO] posłużą do zainicjowania bazy danych, a element EJB z warstwy [metier] do przeprowadzenia testów obliczeń wynagrodzeń.

Wykonanie testu [JUnitMetierLocal] daje następujący wynik [1]:

W [2] duplikujemy [JUnitMetierLocal] jako [JUnitMetierRemote], aby tym razem przetestować interfejs zdalny EJB i [Metier]. Kod pliku [JUnitMetierRemote] zostaje zmodyfikowany tak, aby korzystał z tego interfejsu zdalnego. Pozostałe elementy pozostają bez zmian.

public class JUnitMetierRemote {

   // zdalna warstwa biznesowa
  static private IMetierRemote metier;

  @BeforeClass
  public static void init() throws NamingException {
     // konfiguracja wbudowanego kontenera Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // inicjalizacja kontekstu JNDI z poprzednimi właściwościami
    InitialContext initialContext = new InitialContext(properties);

     // instancjonowanie zdalnych warstw DAO
    IEmployeDaoRemote employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    ICotisationDaoRemote cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    IIndemniteDaoRemote indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
     // utworzenie instancji zdalnej warstwy biznesowej
    metier = (IMetierRemote) initialContext.lookup("MetierRemote");

     // opróżnianie bazy danych
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // wypełnianie bazy
    Indemnite indemnite1=new Indemnite(1,1.93,2,3,12);
    Indemnite indemnite2=new Indemnite(2,2.1,2.1,3.1,15);
    indemnite1=indemniteDao.create(indemnite1);
    indemnite2=indemniteDao.create(indemnite2);
    employeDao.create(new Employe("254104940426058","Jouveinal","Marie","5 rue des oiseaux","St Corentin","49203",indemnite2));
    employeDao.create(new Employe("260124402111742","Laverti","Justine","La brûlerie","St Marcel","49014",indemnite1));
    cotisationDao.create(new Cotisation(3.49,6.15,9.39,7.88));
  }
}
  • wiersze 4 i 19: wykorzystano interfejs zdalny z EJB [Metier].
  • wiersze 15–17: wykorzystuje się interfejsy zdalne warstwy [DAO]
  • wiersze 34–35: ponieważ w przypadku interfejsów zdalnych obiekty wymieniane między klientem a serwerem są przekazywane przez wartość, należy pobrać wynik zwracany przez metodę create(Indemnite i). Nie było to konieczne w przypadku interfejsów lokalnych, gdzie obiekty są przekazywane przez odwołanie.

Po wykonaniu tych czynności można skompilować projekt i uruchomić test [JUnitMetierRemote]:

  

6.2.5. Przeniesienie warstwy [console]

Przeprowadzimy przeniesienie warstwy [console] poprzez skopiowanie pakietów z projektu [mv-pam-spring-hibernate] do projektu [mv-pam-openejb-eclipselink].

Wymienione powyżej błędy [1] wynikają z faktu, że skopiowana warstwa [metier] korzysta z biblioteki Spring, a biblioteki Spring nie są już częścią projektu. W [2] klasa [Main] została przemianowana na [MainLocal]. Będzie ona korzystać z lokalnego interfejsu EJB [Metier].

Kod klasy [MainLocal] zmienia się w następujący sposób:

  public static void main(String[] args) {
     // dane lokalne
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
...
     // czy są jakieś błędy?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // w porządku – można zażądać listy płac z warstwy [métier]
    IMetierLocal metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
       // konfigurujemy wbudowany kontener Open EJB
      Properties properties = new Properties();
      properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
       // inicjalizacja kontekstu JNDI z poprzednimi właściwościami
      InitialContext initialContext = new InitialContext(properties);
       // utworzenie instancji lokalnej warstwy biznesowej
      metier = (IMetierLocal) initialContext.lookup("MetierLocal");
       // obliczanie listy płac
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (PamException ex) {
      System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
      return;
    } catch (Exception ex) {
      System.err.println("L'erreur suivante s'est produite : " + ex.toString());
      return;
    }
     // wyświetlanie szczegółów
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
....

Zmiany dotyczą wierszy 13–25. Zmienia się sposób odwołania do warstwy [metier] (wiersze 17–22). Nie wyjaśniamy nowego kodu, który został już omówiony w poprzednich przykładach. Po wprowadzeniu tych zmian projekt nie zawiera już żadnych błędów (patrz [3]).

Konfigurujemy projekt tak, aby był uruchamiany z argumentami [1]:

Aby aplikacja konsolowa działała poprawnie, w bazie danych muszą znajdować się dane. W tym celu należy zmodyfikować plik [META-INF/persistence.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence              http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="dbpam_eclipselinkPU" transaction-type="JTA">
    <!-- dostawcą JPA jest EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- podmioty Jpa -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- właściwości dostawcy EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <!--
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
      -->
    </properties>
  </persistence-unit>
</persistence>

Wiersz 14, który powodował, że tabele bazy danych były odtwarzane przy każdym uruchomieniu, został skomentowany. Aby ta zmiana została uwzględniona, należy ponownie skompilować projekt (Clean and Build). Po wykonaniu tej czynności można uruchomić program. Jeśli wszystko przebiegnie pomyślnie, na konsoli pojawi się komunikat podobny do poniższego:

.......
INFO - Created EJB(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
INFO - Deployed Application(path=classpath.ear)
[EL Info]: 2009-09-30 15:09:21.109--ServerSession(16658781)--EclipseLink, version: Eclipse Persistence Services - 1.1.2.v20090612-r4475
[EL Info]: 2009-09-30 15:09:21.937--ServerSession(16658781)--file:/C:/temp/09-09-28/pam-console-metier-dao-openejb-eclipselink-0910/build/classes/-jpa login successful
Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé : 
Nom : Jouveinal
Prénom : Marie
Adresse : 5 rue des oiseaux
Ville : St Corentin
Code Postal : 49203
Indice : 2

Informations Cotisations : 
CSGRDS : 3.49 %
CSGD : 6.15 %
Retraite : 7.88 %
Sécurité sociale : 9.39 %

Informations Indemnités : 
Salaire horaire : 2.1 euro
Entretien/jour : 2.1 euro
Repas/jour : 3.1 euro
Congés Payés : 15.0 %

Informations Salaire : 
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

BUILD SUCCESSFUL (total time: 4 seconds)

W tym przypadku wykorzystaliśmy lokalny interfejs warstwy [metier]. Teraz używamy jej interfejsu zdalnego w drugiej klasie konsoli:

W [1] klasa [MainLocal] została zduplikowana do [MainRemote]. Kod klasy [MainRemote] został zmodyfikowany tak, aby korzystał ze zdalnego interfejsu warstwy [metier]:

// w porządku – można poprosić o listę płac na poziomie warstwy [metier]
    IMetierRemote metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
       // konfigurujemy wbudowany kontener Open EJB
...
       // instancjonowanie zdalnej warstwy biznesowej
      metier = (IMetierRemote) initialContext.lookup("MetierRemote");
       // obliczanie listy płac
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (PamException ex) {
...
    } catch (Exception ex) {
...
    }

Zmiany wprowadzono w wierszach 2 i 8. Projekt o nazwie [2] skonfigurowano tak, aby uruchamiał klasę [MainRemote]. Jego uruchomienie daje takie same wyniki jak poprzednio.

6.3. Conclusion

Pokazaliśmy, jak przenieść architekturę Spring / Hibernate do architektury OpenEJB / EclipseLink.

Architektura Spring / Hibernate

Architektura OpenEJB / EclipseLink

Przeniesienie aplikacji przebiegło bez większych trudności, ponieważ pierwotna aplikacja miała strukturę warstwową. Należy to dobrze zrozumieć.