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
6.1.3. Конфігурація шару JPA / EclipseLink / OpenEJB
- Файл [META-INF/persistence.xml], що налаштовує рівень JPA, набуває такого вигляду:
- рядок 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]:
- рядок 3: ідентифікатор Default JDBC Database використовується під час роботи з контейнером OpenEJB, вбудованим (embedded) у саму програму.
- рядок 5: ми використовуємо базу MySQL [dbpam_eclipselink]
6.1.4. Реалізація шару [DAO] за допомогою EJB
- Класи, що реалізують рівень [DAO], стають EJB. Розглянемо приклад класу [CotisationDao]:
Інтерфейс [ICotisationDao] у версії Spring мав такий вигляд:
EJB реалізує цей самий інтерфейс у двох різних формах: локальній та віддаленій. Локальний інтерфейс може використовуватися клієнтом, що працює в тому самому JVM, а віддалений — клієнтом, що працює в іншому JVM.
Локальний інтерфейс:
- рядок 6: інтерфейс [ICotisationDaoLocal] успадковує інтерфейс [ICotisationDao], переймаючи всі його методи. Він не додає нових.
- рядок 5: анотація @Local робить його локальним інтерфейсом для EJB, який його реалізує.
Віддалений інтерфейс:
- рядок 6: інтерфейс [ICotisationDaoRemote] успадковує інтерфейс [ICotisationDao], щоб перейняти всі його методи. Він не додає нових.
- рядок 5: анотація @Remote робить його віддаленим інтерфейсом для EJB, який його реалізує.
Рівень [DAO] реалізується класом EJB, який реалізує обидва інтерфейси (це не є обов’язковим):
- рядок 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] мав такий вигляд:
Створюємо локальний інтерфейс та віддалений інтерфейс на основі попереднього інтерфейсу:
EJB з рівня [metier] реалізує ці два інтерфейси:
- рядки 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 можна вчинити наступним чином:
Посилання на локальному інтерфейсі:
- рядки 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 враховується лише в тому випадку, якщо вона належить класу, завантаженому контейнером EJB. Це стосується, наприклад, класу [Metier]. Наведений вище код належатиме класу консолі, який не завантажуватиметься контейнером EJB. Тому доводиться використовувати імена JNDI або EJB.
Нижче наведено код для отримання посилання на віддалений інтерфейс EJB та [Metier]:
6.2. Практична робота
Ми пропонуємо перенести додаток NetBeans Spring / Hibernate на архітектуру OpenEJB / EclipseLink.
Поточна реалізація з використанням Spring / Hibernate
![]() |
Реалізація, яку потрібно створити з використанням OpenEJB / EclipseLink
![]() |
6.2.1. Налаштування бази даних [dbpam_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]:
![]() |
- створіть папку [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:
Щоб отримати потрібні import пакети, виконайте [clic droit sur le code / Fix Imports].
Віддалений інтерфейс ICotisationDaoRemote:
Потім ми змінюємо клас [CotisationDao] на EJB:
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] залишається таким самим, як і раніше, за винятком однієї дрібниці:
Додано рядок 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] наступним чином:
- рядки 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 вивів журнали в консоль:
- рядки 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]. У цьому класі ми замінюємо локальні інтерфейси на віддалені:
Після цього новий тестовий клас можна запустити. Перед цим, у з’єднанні 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].
Після цього у [3] ми змінюємо клас [Metier], щоб він став EJB:
- рядок 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] набуває такого вигляду:
- рядок 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] змінюється для використання цього віддаленого інтерфейсу. Решта залишається без змін.
- рядки 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] змінюється наступним чином:
Зміни містяться у рядках 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), щоб ця зміна набула чинності. Після цього можна запустити програму. Якщо все гаразд, на консолі з’явиться вивід, подібний до такого:
Тут ми використовували локальний інтерфейс шару [metier]. Тепер ми використовуємо його віддалений інтерфейс у другому консольному класі:
![]() |
У [1] клас [MainLocal] було продубльовано в [MainRemote]. Код [MainRemote] змінено для використання віддаленого інтерфейсу шару [metier]:
Зміни внесено у рядки 2 та 8. Проєкт [2] налаштовано на виконання класу [MainRemote]. Його виконання дає ті самі результати, що й раніше.
6.3. Conclusion
Ми продемонстрували, як перенести архітектуру Spring / Hibernate на архітектуру OpenEJB / EclipseLink.
Архітектура Spring / Hibernate
![]() |
Архітектура OpenEJB / EclipseLink
![]() |
Портування вдалося здійснити без особливих труднощів, оскільки початкова програма була побудована за багаторівневою архітектурою. Цей момент важливо зрозуміти.




























