Skip to content

6. Версія 2: Архітектура OpenEJB / JPA

6.1. Вступ до принципів перенесення

Тут ми представляємо принципи, які регулюватимуть перенесення додатка JPA / Spring / Hibernate на додаток JPA / OpenEJB / EclipseLink. Створення проектів Maven відбудеться в розділі 6.2.

6.1.1. Дві архітектури

Поточна реалізація з використанням Spring / Hibernate

Реалізація, яку потрібно створити, з OpenEJB / EclipseLink

6.1.2. Бібліотеки проектів

  • шари [DAO] та [metier] більше не інстанціюються Spring. Вони інстанціюються контейнером OpenEJB.
  • бібліотеки контейнера Spring та його конфігурація замінюються бібліотеками контейнера OpenEJB та його конфігурацією.
  • бібліотеки шару JPA / Hibernate замінюються бібліотеками шару JPA / EclipseLink

  • Файл [META-INF/persistence.xml], що налаштовує рівень JPA, набуває такого вигляду:
<?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">
     <!-- сутності JPA -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
     <!-- постачальник JPA — це EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
     <!-- властивості постачальника -->
    <properties>
      <property name="eclipselink.ddl-generation" value="create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • рядок 3: транзакції в контейнері EJB мають тип JTA (Java Transaction API). У Spring вони мали тип RESOURCE_LOCAL.
  • рядок 9: використовувана реалізація JPA — це EclipseLink
  • рядки 5–7: сутності, що управляються рівнем JPA
  • рядки 11–13: властивості провайдера EclipseLink
  • рядок 12: під час кожного виконання будуть створюватися таблиці

Характеристики JDBC джерела даних JTA, що використовується контейнером OpenEJB, будуть визначені наступним файлом конфігурації [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>
  • рядок 3: ідентифікатор Default JDBC Database використовується під час роботи з контейнером OpenEJB, вбудованим (embedded) у саму програму.
  • рядок 5: ми використовуємо базу MySQL [dbpam_eclipselink]

6.1.4. Реалізація шару [DAO] за допомогою EJB

  • Класи, що реалізують рівень [DAO], стають EJB. Розглянемо приклад класу [CotisationDao]:

Інтерфейс [ICotisationDao] у версії Spring мав такий вигляд:

package dao;

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

public interface ICotisationDao {
   // створити новий внесок
  Cotisation create(Cotisation cotisation);
   // редагувати існуючий внесок
  Cotisation edit(Cotisation cotisation);
   // видалити існуючий внесок
  void destroy(Cotisation cotisation);
   // пошук конкретного внеску
  Cotisation find(Long id);
   // отримати всі об’єкти «Внесок»
  List<Cotisation> findAll();

}

EJB реалізує цей самий інтерфейс у двох різних формах: локальній та віддаленій. Локальний інтерфейс може використовуватися клієнтом, що працює в тому самому JVM, а віддалений — клієнтом, що працює в іншому JVM.

Локальний інтерфейс:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}
  • рядок 6: інтерфейс [ICotisationDaoLocal] успадковує інтерфейс [ICotisationDao], переймаючи всі його методи. Він не додає нових.
  • рядок 5: анотація @Local робить його локальним інтерфейсом для EJB, який його реалізує.

Віддалений інтерфейс:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}
  • рядок 6: інтерфейс [ICotisationDaoRemote] успадковує інтерфейс [ICotisationDao], щоб перейняти всі його методи. Він не додає нових.
  • рядок 5: анотація @Remote робить його віддаленим інтерфейсом для EJB, який його реалізує.

Рівень [DAO] реалізується класом EJB, який реалізує обидва інтерфейси (це не є обов’язковим):

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

  @PersistenceContext
  private EntityManager em;
  • рядок 1: анотація @Stateless, яка робить клас EJB
  • рядок 2: анотація @TransactionAttribute, яка гарантує, що кожен метод класу виконуватиметься в рамках транзакції.
  • рядок 5: анотація @PersistenceContext, яка вводить у клас [CotisationDao] об’єкт EntityManager з шару JPA. Вона ідентична тій, що була у версії Spring.

Коли використовується локальний інтерфейс шару [DAO], клієнт цього інтерфейсу виконується в тому самому JVM.

Вищешари [metier] та [DAO] обмінюються об’єктами за посиланням. Коли один шар змінює спільний об’єкт, інший шар бачить цю зміну.

Коли використовується віддалений інтерфейс шару [DAO], клієнт цього інтерфейсу зазвичай працює в іншому шарі JVM.

У наведеному вище прикладі шари [metier] та [DAO] обмінюються об’єктами за значенням (серіалізація об’єкта, що обмінюється). Коли один шар змінює спільний об’єкт, інший шар бачить цю зміну лише в тому випадку, якщо змінений об’єкт повертається йому.

6.1.5. Реалізація шару [metier] за допомогою EJB

  • Клас, що реалізує рівень [metier], також стає EJB, який реалізує локальний та віддалений інтерфейси. Початковий інтерфейс [IMetier] мав такий вигляд:
package metier;

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

public interface IMetier {
   // отримати розрахунковий лист
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // список співробітників
  List<Employe> findAllEmployes();
}

Створюємо локальний інтерфейс та віддалений інтерфейс на основі попереднього інтерфейсу:

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{
}

EJB з рівня [metier] реалізує ці два інтерфейси:

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

   // довідка щодо локальних шарів [DAO]
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;
  • рядки 1–2: визначають EJB, кожен метод якого виконується в транзакції.
  • рядок 7: посилання на локальний інтерфейс EJB [CotisationDao].
  • рядок 6: анотація @EJB вимагає, щоб контейнер EJB вставив посилання на локальний інтерфейс EJB [CotisationDao].
  • рядки 8–11: те саме повторюється для локальних інтерфейсів EJB, [EmployeDao] та [IndemniteDao].

У підсумку, коли EJB та [Metier] будуть інстанційовані, поля рядків 7, 9 та 11 будуть ініціалізовані посиланнями на локальні інтерфейси трьох EJB шару [DAO]. Отже, тут ми виходимо з припущення, що шари [metier] та [DAO] будуть виконуватися в одному й тому ж JVM.

6.1.6. Клієнти EJB

На наведеному вище схематичному зображенні для взаємодії з рівнем [metier] рівень [ui] повинен отримати посилання на віддалений інтерфейс EJB від рівня [metier].

На наведеному вище схематичному зображенні для зв’язку з рівнем [metier] рівень [ui] повинен отримати посилання на локальний інтерфейс EJB рівня [metier]. Спосіб отримання цих посилань відрізняється залежно від контейнера. Для контейнера OpenEJB можна вчинити наступним чином:

Посилання на локальному інтерфейсі:

1
2
3
4
5
6
7
8
9
     // налаштування вбудованого контейнера Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // ініціалізація контексту JNDI із зазначеними вище властивостями
    InitialContext initialContext = new InitialContext(properties);
     // створення екземплярів шарів DAO
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
  • рядки 2–5: ініціалізується контейнер OpenEJB.
  • рядок 5: маємо контекст JNDI (Java Naming and Directory Interface), який дозволяє отримати посилання на EJB. Кожен EJB позначається іменем JNDI:
  • (продовження)
    • для локального інтерфейсу до імені EJB додається «Local» (рядки 7–9)
    • для віддаленого інтерфейсу до назви EJB додається «Remote»

У Java EE 5 ці правила змінюються залежно від контейнера EJB. Це створює певні труднощі. У Java EE 6 було запроваджено нотацію JNDI, сумісну з усіма серверами додатків.

Попередній код отримує посилання на локальні інтерфейси EJB за їхніми іменами JNDI. Раніше ми зазначали, що їх також можна отримати за допомогою анотації @EJB. Тому можна було б написати:

@EJB
private IemployeDaoLocal employeDaoLocal ;

Анотація @EJB враховується лише в тому випадку, якщо вона належить класу, завантаженому контейнером EJB. Це стосується, наприклад, класу [Metier]. Наведений вище код належатиме класу консолі, який не завантажуватиметься контейнером EJB. Тому доводиться використовувати імена JNDI або EJB.

Нижче наведено код для отримання посилання на віддалений інтерфейс EJB та [Metier]:

1
2
3
4
5
6
7
8
     // налаштування вбудованого контейнера Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // ініціалізація контексту JNDI контейнера EJB
    InitialContext initialContext = new InitialContext(properties);

     // інстанціювання віддаленого бізнес-шару
metier = (IMetierRemote) initialContext.lookup("MetierRemote");

6.2. Практична робота

Ми пропонуємо перенести додаток NetBeans Spring / Hibernate на архітектуру OpenEJB / EclipseLink.

Поточна реалізація з використанням Spring / Hibernate

Реалізація, яку потрібно створити з використанням OpenEJB / EclipseLink

Якщо вона не існує, створіть базу даних MySQL [dbpam_eclipselink]. Якщо вона існує, видаліть усі її таблиці. Створіть з’єднання NetBeans із цією базою даних, як описано в розділі 6.2.1.

6.2.2. Початкове налаштування проєкту NetBeans

  • завантажте проект Maven [mv-pam-spring-hibernate]
  • Створіть новий проект Maven на Java [mv-pam-openejb-eclipselink] [1]
  • на вкладці [Files] [2] створіть папку [conf] [3] у кореневому каталозі проєкту
  • помістіть у цю папку такий файл [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>
  • створіть папку [src / main/ resources/ META-INF] [5]
  • помістити туди файл [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">
    <!-- постачальник JPA — це EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- суб'єкти Jpa -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- властивості постачальника EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • рядок 12: запитуються детальні журнали у EclipseLink,
  • рядок 13: таблиці будуть створені під час інстанціювання шару JPA,
  • додати бібліотеки OpenEJB, EclipseLink, а також драйвер JDBC з MySQL до файлу [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>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>
  • рядки 18–22: залежність OpenEJB,
  • рядки 30–39: залежності EclipseLink,
  • рядки 41–44: залежність драйвера JDBC від MySQL

6.2.3. Портування шару [DAO]

Ми здійснимо перенесення шару [DAO] шляхом копіювання пакетів із проєкту [mv-pam-spring-hibernate] до проєкту [mv-pam-openejb-eclipselink].

  • скопіювати пакети [dao, exception, jpa]
 

Зазначені вище помилки пов’язані з тим, що скопійований шар [DAO] використовує Spring, а бібліотеки Spring більше не входять до складу проєкту.

6.2.3.1. EJB [CotisationDao]

Ми створюємо локальний та віддалений інтерфейси майбутнього EJB [CotisationDao]:

Локальний інтерфейс ICotisationDaoLocal:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}

Щоб отримати потрібні import пакети, виконайте [clic droit sur le code / Fix Imports].

Віддалений інтерфейс ICotisationDaoRemote:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}

Потім ми змінюємо клас [CotisationDao] на EJB:

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

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

  @PersistenceContext
  private EntityManager em;
...  

import, який цей клас створював у фреймворку Spring, зникає. Створити [Clean and Build] для проєкту:

У [1] більше немає помилок щодо класу [CotisationDao].

6.2.3.2. EJB, [EmployeDao] та [IndemniteDao]

Повторюємо ту саму процедуру для інших елементів шару [DAO]:

  • інтерфейси IEmployeDaoLocal, IEmployeDaoRemote, похідні від IEmployeDao
  • EJB та EmployeDao, що реалізують ці два інтерфейси
  • інтерфейси IIndemniteDaoLocal, IIndemniteDaoRemote, похідні від IIndemniteDao
  • EJB та IndemniteDao, що реалізують ці два інтерфейси

Після цього в проєкті [2] більше немає помилок.

6.2.3.3. Клас [PamException]

Клас [PamException] залишається таким самим, як і раніше, за винятком однієї дрібниці:

package exception;

import javax.ejb.ApplicationException;

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

   // код помилки
  private int code;
...

Додано рядок 5. Щоб отримати правильні import, слід змінити на [Fix imports].

Щоб зрозуміти коментар у рядку 5, слід пам’ятати, що кожен метод класу EJB нашого шару [DAO]:

  • виконується в транзакції, яку запускає та завершує контейнер EJB
  • генерує виняток типу [PamException], щойно щось йде не так

Коли шар [metier] викликає метод M шару [DAO], цей виклик перехоплюється контейнером EJB. Все відбувається так, ніби між шаром [metier] і шаром [DAO] існує проміжний клас, який тут називається [Proxy EJB] і перехоплює всі виклики до шару [DAO]. Коли виклик методу M шару [DAO] перехоплюється, проксі EJB запускає транзакцію, а потім передає управління методу M шару [DAO], який потім виконується в цій транзакції. Метод M завершується з винятком або без нього.

  • якщо метод M завершується без винятку, виконання повертається до проксі EJB, який завершує транзакцію, підтверджуючи її за допомогою commit. Потім потік виконання повертається до методу-викликувача шару [metier]
  • якщо метод M завершується з винятком, виконання повертається до проксі EJB, який завершує транзакцію, визнаючи її недійсною за допомогою rollback. Крім того, він інкапсулює цей виняток у тип EJBException. Потім потік виконання повертається до методу-викликувача на рівні [metier], який, таким чином, отримує EJBException. Анотація у рядку 5 вище запобігає цьому інкапсулюванню. Отже, рівень [metier] отримає об’єкт типу PamException. Крім того, атрибут rollback=true вказує проксі-серверу EJB, що при отриманні об’єкта типу PamException він повинен скасувати транзакцію.

6.2.3.4. Тестування шару [DAO]

Наш модуль [DAO], реалізований за допомогою EJB, можна протестувати. Почнемо з копіювання пакета [dao] з [Test Packages] проекту [mv-pam-springhibernate] у проект, що зараз розробляється, — [1]:

Ми залишаємо лише тест [JUnitInitDB], який ініціалізує базу даних деякими даними з [2]. Перейменовуємо клас [ JUnitInitDbLocal] на [3]. Клас [JUnitInitDBLocal] використовуватиме локальний інтерфейс EJB з рівня [DAO].

Спочатку ми змінюємо клас [JUnitInitDBLocal] наступним чином:

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 {
     // налагодження вбудованого контейнера Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // ініціалізація контексту JNDI із попередніми властивостями
    InitialContext initialContext = new InitialContext(properties);
     // інстанціювання локальних шарів DAO
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
}

...
  • рядки 3–5: посилання на локальні інтерфейси EJB з шару [DAO]
  • рядок 7: @BeforeClass позначає метод, що виконується під час запуску тесту JUnit
  • рядки 10–13: ініціалізація контейнера OpenEJB. Ця ініціалізація є власницькою і змінюється для кожного контейнера EJB.
  • рядок 13: маємо контекст JNDI (Java Naming and Directory Interface), який дозволяє отримувати доступ до EJB за допомогою імен. У OpenEJB локальний інтерфейс EJB E позначається як ELocal, а віддалений інтерфейс — як ERemote.
  • рядки 15–17: у контексті JNDI запитується посилання на локальні інтерфейси EJB та [EmployeDao, CotisationDao, IndemniteDao].

Збираємо проект (Build), запускаємо сервер MySQL, якщо це необхідно, виконуємо тест JUnitInitDBLocal. Нагадаємо, що файл [persistence.xml] налаштовано на повторне створення таблиць під час кожного виконання. Перед виконанням тесту бажано видалити можливі таблиці з бази даних MySQL та [dbpam_eclipselink].

  • у [1], на вкладці [Services] видаляються таблиці з підключення до NetBeans, встановленого в розділі 6.2.1.
  • у [2] база даних [dbpam_eclipselink] більше не містить таблиць
  • у [3] проект скомпільовано
  • у [4] виконується тест JUnitInitDBLocal
  • у [5] тест пройшов успішно
  • у [6] оновлюється з'єднання з NetBeans
  • у [7], бачимо 4 таблиці, створені шаром JPA. Мета тесту полягала в тому, щоб заповнити їх. Переглядаємо вміст однієї з них
  • у [8] — вміст таблиці [EMPLOYES]

Контейнер OpenEJB вивів журнали в консоль:

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)
  • рядки 2–3: обидва імена JNDI з EJB та [CotisationDaoLocal],
  • рядки 4–5: обидва імена JNDI з EJB та [CotisationDaoRemote],
  • рядки 7–8: обидва імена JNDI з EJB [EmployeDaoLocal],
  • рядки 9–10: обидва імена JNDI з EJB та [EmployeDaoRemote],
  • рядки 12–13: обидва імена JNDI з EJB та [IndemniteDaoLocal],
  • рядки 14–15: обидва імена JNDI з EJB та [EmployeDaoRemote].

Ми повторюємо той самий тест, цього разу використовуючи віддалений інтерфейс EJB.

У [1] клас [JUnitInitDBLocal] було скопійовано (копіювання/вставлення) у [JUnitInitDBRemote]. У цьому класі ми замінюємо локальні інтерфейси на віддалені:

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 {
     // налаштування вбудованого контейнера Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // ініціалізація контексту JNDI із зазначеними вище властивостями
    InitialContext initialContext = new InitialContext(properties);
     // створення екземплярів віддалених шарів DAO
    employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
}

Після цього новий тестовий клас можна запустити. Перед цим, у з’єднанні NetBeans [dbpam_eclipselink], видаліть таблиці з бази даних [dbpam_eclipselink].

 

За допомогою підключення NetBeans [dbpam_eclipselink] переконайтеся, що база даних заповнена.

6.2.4. Перенесення шару [metier]

Ми здійснимо перенесення шару [metier] шляхом копіювання пакетів із проєкту [mv-pam-spring-hibernate] до проєкту [mv-pam-openejb-eclipselink].

Помилки, зазначені вище ([1]), пов’язані з тим, що скопійований шар [metier] використовує Spring, а бібліотеки Spring більше не входять до складу проєкту.

6.2.4.1. EJB [Metier]

Ми дотримуємося того самого підходу, що й описано для EJB та [CotisationDao]. Спочатку ми створюємо в [2] локальний та віддалений інтерфейси майбутнього EJB [Metier]. Обидва вони походять від початкового інтерфейсу [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{

}

Після цього у [3] ми змінюємо клас [Metier], щоб він став EJB:

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

   // посилання на локальний шар [DAO]
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;

   // отримання відомості про заробітну плату
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
          double nbHeuresTravaillées, int nbJoursTravaillés) {
     // отримання інформації про працівника
...
  • рядок 1: анотація @Stateless перетворює клас на EJB
  • рядок 2: кожен метод класу виконуватиметься в транзакції
  • рядок 3: EJB [Metier] реалізує обидва інтерфейси — локальний та віддалений, які ми щойно визначили
  • рядок 7: EJB [Metier] використовуватиме EJB [CotisationDao] через його локальний інтерфейс. Це означає, що шари [metier] та [DAO] повинні виконуватися в одному й тому ж JVM.
  • рядок 6: анотація @EJB забезпечує, щоб контейнер EJB самостійно вводив посилання на локальний інтерфейс EJB [CotisationDao]. Інший спосіб, з яким ми стикалися, полягає у використанні контексту JNDI.
  • рядки 8–11: той самий механізм використовується для двох інших EJB на рівні [DAO].

6.2.4.2. Тестування шару [metier]

Наш рівень [metier], реалізований за допомогою EJB, можна протестувати. Спочатку скопіюємо пакет [metier] з [Test Packages] проекту [mv-pam-spring-hibernate] у проект, що зараз створюється, — [1]:

  • у [1], результат копіювання
  • у [2], видаляємо перший тест
  • у [3], тест, що залишився, перейменовано на [JUnitMetierLocal]

Клас [JUnitMetierLocal] набуває такого вигляду:

public class JUnitMetierLocal {

// локальний бізнес-шар
  static private IMetierLocal metier;

  @BeforeClass
  public static void init() throws NamingException {
     // налаштування вбудованого контейнера Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // ініціалізація контексту JNDI з попередніми властивостями
    InitialContext initialContext = new InitialContext(properties);

     // створення екземплярів локальних шарів DAO
    IEmployeDaoLocal employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    ICotisationDaoLocal cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    IIndemniteDaoLocal indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
     // створення екземпляра локального бізнес-шару
    metier = (IMetierLocal) initialContext.lookup("MetierLocal");

     // очищення бази даних
...
}
  • рядок 4: посилання на локальний інтерфейс EJB [Metier]
  • рядки 8–12: конфігурація контейнера OpenEJB, ідентична тій, що була виконана під час тестування шару [DAO]
  • рядки 15–19: запитуються посилання на контекст JNDI з рядка 12, посилання на 3 EJB шару [DAO] та на EJB шару [metier]. EJB з шару [DAO] будуть використовуватися для ініціалізації бази даних, а EJB з шару [metier] — для тестування розрахунків заробітної плати.

Виконання тесту [JUnitMetierLocal] дає такий результат: [1]:

У [2] [JUnitMetierLocal] дублюється в [JUnitMetierRemote], щоб цього разу протестувати віддалений інтерфейс EJB [Metier]. Код [JUnitMetierRemote] змінюється для використання цього віддаленого інтерфейсу. Решта залишається без змін.

public class JUnitMetierRemote {

   // віддалений бізнес-шар
  static private IMetierRemote metier;

  @BeforeClass
  public static void init() throws NamingException {
     // конфігурація вбудованого контейнера Open EJB
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // ініціалізація контексту JNDI з попередніми властивостями
    InitialContext initialContext = new InitialContext(properties);

     // створення екземплярів віддалених шарів DAO
    IEmployeDaoRemote employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    ICotisationDaoRemote cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    IIndemniteDaoRemote indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
     // створення екземпляра віддаленого бізнес-шару
    metier = (IMetierRemote) initialContext.lookup("MetierRemote");

     // очищення бази даних
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // заповнення бази даних
    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));
  }
}
  • рядки 4 та 19: використовується віддалений інтерфейс EJB [Metier].
  • рядки 15–17: використовуються віддалені інтерфейси рівня [DAO]
  • рядки 34–35: оскільки при використанні віддалених інтерфейсів об’єкти, що обмінюються між клієнтом і сервером, передаються за значенням, необхідно отримати результат, повернений методом create(Indemnite i). Це не було обов’язковим у випадку з локальними інтерфейсами, де об’єкти передаються за посиланням.

Після цього проект можна скомпілювати та виконати тест [JUnitMetierRemote]:

  

6.2.5. Портування шару [console]

Ми здійснимо перенесення шару [console] шляхом копіювання пакетів із проєкту [mv-pam-spring-hibernate] до проєкту [mv-pam-openejb-eclipselink].

Помилки, зазначені вище ([1]), пов’язані з тим, що скопійований шар [metier] використовує Spring, а бібліотеки Spring більше не входять до складу проєкту. У [2] клас [Main] перейменовано на [MainLocal]. Він використовуватиме локальний інтерфейс EJB [Metier].

Код класу [MainLocal] змінюється наступним чином:

  public static void main(String[] args) {
     // локальні дані
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
...
     // є помилки?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // все гаразд — можна запросити розрахунок заробітної плати у шарі [métier]
    IMetierLocal metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
       // налаштовуємо вбудований контейнер Open EJB
      Properties properties = new Properties();
      properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
       // ініціалізація контексту JNDI із попередніми властивостями
      InitialContext initialContext = new InitialContext(properties);
       // створення екземпляра локального бізнес-шару
      metier = (IMetierLocal) initialContext.lookup("MetierLocal");
       // розрахунок відомості про заробітну плату
      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;
    }
     // детальний перегляд
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
....

Зміни містяться у рядках 13–25. Змінюється спосіб посилання на шар [metier] (рядки 17–22). Ми не пояснюємо новий код, оскільки він уже розглядався в попередніх прикладах. Після внесення цих змін у проєкті більше немає помилок (див. [3]).

Налаштовуємо проект так, щоб він виконувався з аргументами [1]:

Щоб консольна програма працювала нормально, у базі даних мають бути дані. Для цього потрібно змінити файл [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">
    <!-- постачальник JPA — це EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- об'єкти Jpa -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- властивості постачальника EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <!--
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
      -->
    </properties>
  </persistence-unit>
</persistence>

Рядок 14, який призводив до того, що таблиці бази даних відтворювалися при кожному запуску, виведено у коментарі. Проєкт потрібно перекомпілювати (Clean and Build), щоб ця зміна набула чинності. Після цього можна запустити програму. Якщо все гаразд, на консолі з’явиться вивід, подібний до такого:

.......
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)

Тут ми використовували локальний інтерфейс шару [metier]. Тепер ми використовуємо його віддалений інтерфейс у другому консольному класі:

У [1] клас [MainLocal] було продубльовано в [MainRemote]. Код [MainRemote] змінено для використання віддаленого інтерфейсу шару [metier]:

// все гаразд — можна запитати відомості про заробітну плату на рівні [metier]
    IMetierRemote metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
       // налаштовуємо вбудований контейнер Open EJB
...
       // створення екземпляра віддаленого бізнес-шару
      metier = (IMetierRemote) initialContext.lookup("MetierRemote");
       // розрахунок відомості про заробітну плату
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (PamException ex) {
...
    } catch (Exception ex) {
...
    }

Зміни внесено у рядки 2 та 8. Проєкт [2] налаштовано на виконання класу [MainRemote]. Його виконання дає ті самі результати, що й раніше.

6.3. Conclusion

Ми продемонстрували, як перенести архітектуру Spring / Hibernate на архітектуру OpenEJB / EclipseLink.

Архітектура Spring / Hibernate

Архітектура OpenEJB / EclipseLink

Портування вдалося здійснити без особливих труднощів, оскільки початкова програма була побудована за багаторівневою архітектурою. Цей момент важливо зрозуміти.