3. JPA у багатошаровій архітектурі
Для вивчення API JPA ми використовували таку тестову архітектуру:
![]() |
Наші тестові програми були консольними додатками, які безпосередньо зверталися до шару JPA. Під час цього ми виявили основні методи шару JPA. Ми працювали в так званому середовищі «Java SE» (Standard Edition). JPA працює як у середовищі Java SE, так і в Java EE5 (Enterprise Edition).
Тепер, коли ми вже певною мірою опанували як налаштування реляційно-об’єктного мосту, так і використання методів шару JPA, повернемося до більш класичної багатошарової архітектури:
![]() |
Доступ до шару [JPA] здійснюватиметься через двошарову архітектуру [metier] та [dao]. Для зв’язування цих рівнів між собою використовуватимуться фреймворк Spring [7], а потім контейнер EJB3 з JBoss та [8].
Вище ми зазначали, що JPA доступний у середовищах SE та EE5. Середовище Java EE5 надає численні сервіси у сфері доступу до постійних даних, зокрема пули з’єднань, менеджери транзакцій тощо. Розробнику може бути цікаво скористатися цими сервісами. Java-середовище EE5 поки що не дуже поширене (травень 2007 року). Наразі його можна знайти на сервері додатків Sun Application Server 9.x (Glassfish). Сервер додатків — це, по суті, сервер веб-додатків. Якщо створювати автономний графічний додаток типу Swing, то середовище EE та послуги, які воно надає, недоступні. Це є проблемою. Починають з’являтися «автономні» середовища EE, c.a.d. які можна використовувати поза межами сервера додатків. Це стосується JBoss та EJB3, які ми будемо використовувати в цьому документі.
У середовищі EE5 шари реалізовані об’єктами, що називаються EJB (Enterprise Java Bean). У попередніх версіях EE об’єкти EJB (EJB, 2.x) вважалися складними у впровадженні та тестуванні, а іноді й малопродуктивними. Розрізняють «entity» EJB2.x та «session» EJB2.x. Коротко кажучи, EJB2.x «entity» — це відображення рядка таблиці бази даних, а EJB2.x «session» — об’єкт, що використовується для реалізації шарів [metier], [dao] багаторівневої архітектури. Одне з головних зауважень щодо шарів, реалізованих за допомогою EJB, полягає в тому, що їх можна використовувати лише в межах контейнерів EJB — служби, що надається середовищем EE. Це ускладнює проведення модульних тестів. Отже, на наведеному вище схематичному зображенні модульні тести шарів [metier] та [dao], побудованих на основі EJB, вимагали б розгортання сервера додатків — досить трудомісткої операції, яка не дуже спонукає розробника часто проводити тестування.
Фреймворк Spring з’явився як відповідь на складність EJB2. Spring надає в середовищі SE значну кількість сервісів, які зазвичай забезпечуються середовищами EE. Так, у розділі «Збереження даних», який нас тут цікавить, Spring надає пули з’єднань та менеджери транзакцій, необхідні для додатків. Поява Spring сприяла розвитку культури модульного тестування, яке відразу стало набагато простішим у реалізації. Spring дозволяє реалізовувати рівні додатка за допомогою класичних об’єктів Java (POJO, Plain Old/Ordinary Java Object), що дає змогу повторно використовувати їх в іншому контексті. Нарешті, він досить прозоро інтегрує численні сторонні інструменти, зокрема інструменти збереження даних, такі як Hibernate, Ibatis тощо.
Java EE5 була розроблена для усунення недоліків попередньої специфікації EE. EJB та 2.x стали EJB3. Це POJOs, позначені анотаціями, що роблять їх особливими об’єктами, коли вони знаходяться всередині контейнера EJB3. У цьому контейнері EJB3 зможе користуватися послугами контейнера (пул з’єднань, менеджер транзакцій тощо). Поза контейнером EJB3 об’єкт EJB3 стає звичайним об’єктом Java. Його анотації EJB ігноруються.
Вище ми зобразили Spring та JBoss EJB3 як можливу інфраструктуру (фреймворк) нашої багаторівневої архітектури. Саме ця інфраструктура надаватиме необхідні нам послуги: пул з’єднань та менеджер транзакцій.
- У Spring шари будуть реалізовані за допомогою POJOs. Вони матимуть доступ до сервісів Spring (пул з’єднань, менеджер транзакцій) шляхом ін’єкції залежностей у ці POJOs: під час їх створення Spring ін’єктує в них посилання на сервіси, які їм знадобляться.
- JBoss EJB3 — це контейнер EJB, який може працювати поза межами сервера додатків. Принцип його роботи (з точки зору розробника) аналогічний тому, що описано для Spring. Ми знайдемо лише кілька відмінностей.
Завершимо цей документ прикладом трирівневого веб-додатку — простим, але типовим:
![]() |
3.1. Приклад 1: Spring / JPA з сутністю «Особа»
Ми беремо суть Personne, розглянуту в розділі 2.1, і інтегруємо її в багатошарову архітектуру, де інтеграція шарів здійснюється за допомогою Spring, а шар персистентності реалізовано за допомогою Hibernate.
![]() |
Тут передбачається, що читач має базові знання про Spring. Якщо це не так, можна ознайомитися з наступним документом, у якому пояснюється поняття ін’єкції залежностей, що лежить в основі Spring:
[ref3]: Spring IOC (Inversion Of Control) [http://tahe.developpez.com/java/springioc].
3.1.1. Проєкт « » на базі Eclipse / Spring / Hibernate
Проект Eclipse виглядає так:
![]() |
![]() |
- у [1]: проект Eclipse. Його можна знайти у [6] серед прикладів підручника [5]. Його потрібно імпортувати.
- у [2]: Java-код шарів, представлений у пакетах:
- [entites]: пакет сутностей JPA
- [dao]: рівень доступу до даних — базується на рівні JPA
- [service]: шар, що є скоріше сервісним, ніж бізнес-орієнтованим. У ньому використовуватиметься сервіс транзакцій контейнерів.
- [tests]: об’єднує тестові програми.
- у [3]: бібліотека [jpa-spring] об’єднує JAR-файли, необхідні для Spring (див. також [7] та [8]).
- en [4]: папка [conf] містить файли конфігурації Spring для кожного з SGBD, що використовуються в цьому посібнику.
3.1.2. Об’єкти JPA
![]() |
Тут розглядається лише один об’єкт — Personne, про який йшлося в розділі 2.1, конфігурацію якого наведено нижче:
package entites;
...
@Entity
@Table(name="jpa01_hb_personne")
public class Personne {
@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "VERSION", nullable = false)
@Version
private int version;
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
@Column(name = "PRENOM", length = 30, nullable = false)
private String prenom;
@Column(name = "DATENAISSANCE", nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(name = "MARIE", nullable = false)
private boolean marie;
@Column(name = "NBENFANTS", nullable = false)
private int nbenfants;
// конструктори
public Personne() {
}
public Personne(String nom, String prenom, Date datenaissance, boolean marie,
int nbenfants) {
...
}
// toString
public String toString() {
return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
.format(getDatenaissance()), isMarie(), getNbenfants());
}
// методи getter та setter
...
}
3.1.3. Рівень [dao]
![]() | ![]() |
Рівень [dao] має такий інтерфейс IDao:
package dao;
import java.util.List;
import entites.Personne;
public interface IDao {
// отримати особу за її ідентифікатором
public Personne getOne(Integer id);
// отримати всіх осіб
public List<Personne> getAll();
// зберегти особу
public Personne saveOne(Personne personne);
// оновити особу
public Personne updateOne(Personne personne);
// видалити особу за її ідентифікатором
public void deleteOne(Integer id);
// отримати осіб, ім'я яких відповідає шаблону
public List<Personne> getAllLike(String modele);
}
Реалізація [Dao] цього інтерфейсу виглядає наступним чином:
package dao;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import entites.Personne;
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
// видалити особу за її ідентифікатором
public void deleteOne(Integer id) {
Personne personne = em.find(Personne.class, id);
if (personne == null) {
throw new DaoException(2);
}
em.remove(personne);
}
@SuppressWarnings("unchecked")
// отримати всіх осіб
public List<Personne> getAll() {
return em.createQuery("select p from Personne p").getResultList();
}
@SuppressWarnings("unchecked")
// отримати осіб, ім'я яких відповідає шаблону
public List<Personne> getAllLike(String modele) {
return em.createQuery("select p from Personne p where p.nom like :modele")
.setParameter("modele", modele).getResultList();
}
// отримати особу за її ідентифікатором
public Personne getOne(Integer id) {
return em.find(Personne.class, id);
}
// зберегти особу
public Personne saveOne(Personne personne) {
em.persist(personne);
return personne;
}
// оновити особу
public Personne updateOne(Personne personne) {
return em.merge(personne);
}
}
- По-перше, слід відзначити простоту реалізації [Dao]. Це зумовлено використанням шару JPA, який виконує основну частину роботи з доступом до даних.
- рядок 10: клас [Dao] реалізує інтерфейс [IDao]
- рядок 13: об’єкт типу [EntityManager], який буде використовуватися для роботи з контекстом персистентності JPA. У неформальному вживанні ми іноді будемо плутати його з самим контекстом персистентності. Контекст персистентності міститиме сутності Personne.
- рядок 12: ніде в коді поле [EntityManager em] не ініціалізується. Воно буде ініціалізовано під час запуску додатка за допомогою Spring. Саме анотація JPA @PersistenceContext у рядку 12 вимагає від Spring ввести в em менеджер контексту персистентності.
- рядки 26–28: список усіх осіб отримується за допомогою запиту JPQL.
- рядки 32–35: список усіх осіб, ім’я яких відповідає певному шаблону, отримується за допомогою запиту JPQL.
- рядки 38–40: особу з таким ідентифікатором отримують за допомогою методу find об’єкта API JPA. Повертає покажчик null, якщо особа не існує.
- рядки 43–46: особа зберігається у постійній пам’яті за допомогою методу `persist` класу `API JPA`. Цей метод забезпечує збереження особи у постійній пам’яті.
- рядки 49–51: оновлення особи здійснюється методом `merge` класу `API JPA`. Цей метод має сенс лише в тому випадку, якщо особа, що оновлюється, раніше була від’єднана. Метод зберігає у пам’яті створену таким чином особу.
- рядки 16–22: видалення особи, ідентифікатор якої передається нам як параметр, відбувається у два етапи:
- рядок 17: її шукають у контексті збереження
- рядки 18–20: якщо її не знайдено, генерується виняток із кодом помилки 2
- рядок 21: якщо її знайдено, її видаляють із контексту збереження за допомогою методу remove класу API JPA.
- Наразі не видно, що кожен метод виконуватиметься в рамках транзакції, запущеної рівнем [service].
Додаток має власний тип винятку з назвою [DaoException]:
package dao;
@SuppressWarnings("serial")
public class DaoException extends RuntimeException {
// код помилки
private int code;
public DaoException(int code) {
super();
this.code = code;
}
public DaoException(String message, int code) {
super(message);
this.code = code;
}
public DaoException(Throwable cause, int code) {
super(cause);
this.code = code;
}
public DaoException(String message, Throwable cause, int code) {
super(message, cause);
this.code = code;
}
// геттер і сеттер
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- рядок 4: [DaoException] походить від [RuntimeException]. Отже, це тип винятків, які компілятор не вимагає обробляти за допомогою try/catch або вказувати в сигнатурі методів. Саме з цієї причини [DaoException] відсутній у сигнатурі методу [deleteOne] інтерфейсу [IDao]. Це дозволяє реалізувати цей інтерфейс класом, який генерує винятки іншого типу, за умови, що цей клас також походить від [RuntimeException].
- Щоб розрізнити помилки, які можуть виникнути, використовується код помилки з рядка 7. Три конструктори в рядках 14, 19 та 24 — це конструктори батьківського класу [RuntimeException], до яких додано один параметр: код помилки, який потрібно присвоїти винятковій ситуації.
3.1.4. Рівень [metier / service]
![]() |
Рівень [service] має такий інтерфейс [IService]:
package service;
import java.util.List;
import entites.Personne;
public interface IService {
// отримати особу за її ідентифікатором
public Personne getOne(Integer id);
// отримати всіх осіб
public List<Personne> getAll();
// зберегти особу
public Personne saveOne(Personne personne);
// Оновити особу
public Personne updateOne(Personne personne);
// видалити особу за її ідентифікатором
public void deleteOne(Integer id);
// отримати осіб, ім'я яких відповідає шаблону
public List<Personne> getAllLike(String modele);
// видалити одразу декількох осіб
public void deleteArray(Personne[] personnes);
// зберегти декількох осіб одночасно
public Personne[] saveArray(Personne[] personnes);
// оновити декількох осіб одночасно
public Personne[] updateArray(Personne[] personnes);
}
- рядки 8–24: інтерфейс [IService] повторює методи інтерфейсу [IDao]
- рядок 27: метод [deleteArray] дозволяє видалити групу осіб у рамках однієї транзакції: видаляються всі особи або жодна.
- рядки 30 та 33: методи, аналогічні до [deleteArray], для збереження (рядок 30) або оновлення (рядок 33) набору осіб у рамках однієї транзакції.
Реалізація [Service] інтерфейсу [IService] виглядає наступним чином:
package service;
...
// усі методи класу виконуються в рамках однієї транзакції
@Transactional
public class Service implements IService {
// шар [dao]
private IDao dao;
public IDao getDao() {
return dao;
}
public void setDao(IDao dao) {
this.dao = dao;
}
// видалити декількох осіб одночасно
public void deleteArray(Personne[] personnes) {
for (Personne p : personnes) {
dao.deleteOne(p.getId());
}
}
// видалити особу за її ідентифікатором
public void deleteOne(Integer id) {
dao.deleteOne(id);
}
// отримати всіх осіб
public List<Personne> getAll() {
return dao.getAll();
}
// отримати осіб, ім'я яких відповідає шаблону
public List<Personne> getAllLike(String modele) {
return dao.getAllLike(modele);
}
// отримати особу за її ідентифікатором
public Personne getOne(Integer id) {
return dao.getOne(id);
}
// зберегти одразу декількох осіб
public Personne[] saveArray(Personne[] personnes) {
Personne[] personnes2 = new Personne[personnes.length];
for (int i = 0; i < personnes.length; i++) {
personnes2[i] = dao.saveOne(personnes[i]);
}
return personnes2;
}
// зберегти одну особу
public Personne saveOne(Personne personne) {
return dao.saveOne(personne);
}
// оновити декількох осіб одночасно
public Personne[] updateArray(Personne[] personnes) {
Personne[] personnes2 = new Personne[personnes.length];
for (int i = 0; i < personnes.length; i++) {
personnes2[i] = dao.updateOne(personnes[i]);
}
return personnes2;
}
// оновити дані однієї особи
public Personne updateOne(Personne personne) {
return dao.updateOne(personne);
}
}
- рядок 6: анотація Spring @Transactional вказує, що всі методи класу повинні виконуватися в рамках транзакції. Транзакція розпочнеться перед початком виконання методу та закриється після його виконання. Якщо під час виконання методу виникає виняток типу [RuntimeException] або його похідний, автоматичний відкат (rollback) скасовує всю транзакцію; в іншому випадку автоматичне підтвердження (commit) її фіксує. Варто зауважити, що в коді Java не потрібно піклуватися про транзакції. Ними керує Spring.
- рядок 10: посилання на шар [dao]. Пізніше ми побачимо, що це посилання ініціалізується Spring під час запуску додатка.
- Методи [Service] просто викликають методи інтерфейсу [IDao dao] із рядка 10. Ми залишаємо читачеві можливість ознайомитися з кодом. Особливих складнощів тут немає.
- Раніше ми зазначали, що кожен метод класу [Service] виконується в рамках транзакції. Ця транзакція прив’язана до потоку виконання методу. У цьому потоці виконуються методи рівня [dao]. Вони автоматично прив’язуються до транзакції потоку виконання. Наприклад, метод [deleteArray] (рядок 21) має виконати N разів метод [deleteOne] з рівня [dao]. Ці N виконань відбуватимуться в рамках потоку виконання методу [deleteArray], тобто в межах однієї транзакції. Отже, вони будуть або всі підтверджені (commit), якщо все пройде успішно, або всі скасовані (rollback), якщо в одному з N виконань методу [deleteOne] шару [dao] виникне виняток.
3.1.5. Налаштування рівнів
![]() | ![]() |
Конфігурація шарів [service], [dao] та [JPA] забезпечується двома вищезазначеними файлами: [META-INF/persistence.xml] та [spring-config.xml]. Обидва файли мають бути в папці classpath додатка, що пояснює їхнє розміщення в папці [src] проекту Eclipse. Назву файлу [spring-config.xml] можна вибрати довільно.
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="jpa" transaction-type="RESOURCE_LOCAL" />
</persistence>
- рядок 4: файл оголошує одиницю персистентності з назвою jpa, яка використовує «локальні» транзакції c.a.d, що не надаються контейнером EJB3. Ці транзакції створюються та управляються Spring і налаштовуються у файлі [spring-config.xml].
spring-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<!-- рівні додатків -->
<bean id="dao" class="dao.Dao" />
<bean id="service" class="service.Service">
<property name="dao" ref="dao" />
</bean>
<!-- шар збереження даних JPA -->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--
<property name="showSql" value="true" />
-->
<property name="databasePlatform"
value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<property name="generateDdl" value="true" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean
class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
<!-- джерело даних DBCP -->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="username" value="jpa" />
<property name="password" value="jpa" />
</bean>
<!-- менеджер транзакцій -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory"
ref="entityManagerFactory" />
</bean>
<!-- перетворення винятків -->
<bean
class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- анотації збереження -->
<bean
class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
- рядки 2–5: кореневий тег <beans> файлу конфігурації. Ми не коментуємо різні атрибути цього тегу. Слід ретельно скопіювати та вставити текст, оскільки помилка в одному з цих атрибутів може спричинити помилки, які іноді важко зрозуміти.
- рядок 8: «bean» «dao» — це посилання на екземпляр класу [dao.Dao]. Буде створено єдиний екземпляр (singleton), який реалізує рівень [dao] додатка.
- рядки 9–11: створення екземпляра шару [service]. Бін «service» — це посилання на екземпляр класу [service.Service]. Буде створено єдиний екземпляр (синглтон), який реалізує рівень [service] додатка. Ми бачили, що клас [service.Service] має приватне поле [IDao dao]. Це поле ініціалізується в рядку 10 біном «dao», визначеним у рядку 8.
- У підсумку рядки 8–11 налаштували шари [dao] та [service]. Далі ми розглянемо, коли та як вони будуть інстанційовані.
- рядки 35–42: визначається джерело даних. Ми вже знайомилися з поняттям джерела даних під час вивчення сутностей JPA у Hibernate:
![]() |
У наведеному вище прикладі [c3p0], який називається «пулом з’єднань», можна було б назвати «джерелом даних». Джерело даних надає послугу «пулу з’єднань». У Spring ми будемо використовувати джерело даних, відмінне від [c3p0]. Це [DBCP] із проєкту Apache Commons DBCP [http://jakarta.apache.org/commons/dbcp/]. Архіви [DBCP] було розміщено в бібліотеці користувача [jpa-spring]:
![]() |
- рядки 38–41: для встановлення з’єднання з цільовою базою даних джерело даних має знати, який драйвер JDBC використовується (рядок 38), URL-адресу бази даних (рядок 39), ім’я користувача для з’єднання та його пароль (рядки 40–41).
- рядки 14–32: налаштовують рівень JPA
- рядки 14–15: визначають бін типу [EntityManagerFactory], здатний створювати об’єкти типу [EntityManager] для управління контекстами персистентності. Інстанційований клас [LocalContainerEntityManagerFactoryBean] надається Spring. Для його інстанціювання потрібна певна кількість параметрів, які визначені у рядках 16–31.
- рядок 16: джерело даних, яке використовується для отримання з’єднань із SGBD. Це джерело [DBCP], визначене в рядках 35–42.
- рядки 17–27: реалізація JPA, яку слід використовувати
- рядки 18–26: визначають Hibernate (рядок 19) як реалізацію JPA, яку слід використовувати
- рядки 23–24: діалект SQL, який Hibernate має використовувати з цільовим SGBD, у даному випадку MySQL5.
- рядок 25: вимагає, щоб під час запуску додатка база даних була згенерована (drop та create).
- рядки 28–31: визначають «завантажувач класів». Я не можу чітко пояснити роль цього біна, який використовується EntityManagerFactory у шарі JPA. Проте він передбачає передачу до JVM, який виконує додаток, імені архіву, вміст якого керуватиме завантаженням класів під час запуску додатка. У даному випадку цим архівом є [spring-agent.jar], розміщений у бібліотеці користувача [jpa-spring] (див. вище). Ми побачимо, що Hibernate не потребує цього агента, на відміну від Toplink.
- рядки 45–50: визначають менеджер транзакцій, який слід використовувати
- рядок 45: вказує, що транзакції обробляються за допомогою анотацій Java (їх також можна було б оголосити у класі spring-config.xml). Зокрема, це анотація @Transactional, яка зустрічається у класі [Service] (рядок 6).
- рядки 46–50: менеджер транзакцій
- рядок 47: менеджер транзакцій — це клас, що надається Spring
- рядки 48–49: менеджеру транзакцій Spring потрібно знати клас EntityManagerFactory, який керує шаром JPA. Це той клас, що визначений у рядках 14–32.
- рядки 57–58: визначають клас, який керує анотаціями персистентності Spring, знайденими в коді Java, такими як анотація @PersistenceContext класу [dao.Dao] (рядок 12).
- рядки 53–54: визначають клас Spring, який, зокрема, обробляє анотацію @Repository, що робить клас, позначений цією анотацією, придатним для перетворення нативних винятків драйвера Jdbc типу SGBD у загальні винятки Spring типу [DataAccessException]. Це перетворення інкапсулює нативне виключення Jdbc у тип [DataAccessException], що має різні підкласи:

Цей переклад дозволяє клієнтській програмі обробляти винятки узагальнено, незалежно від цільового SGBD. У нашому Java-коді ми не використовували анотацію @Repository. Тому рядки 53–54 є зайвими. Ми залишили їх лише для інформації.
Ми завершили роботу з конфігураційним файлом Spring. Він є складним, і багато що залишається незрозумілим. Він був взятий із документації Spring. На щастя, його адаптація до різних ситуацій часто зводиться до двох змін:
- зміна цільової бази даних: рядки 38–41. Наведемо приклад для Oracle.
- зміна реалізації JPA: рядки 14–32. Наведемо приклад із Toplink.
3.1.6. Клієнтська програма [InitDB]
Ми приступаємо до написання першого клієнта для описаної вище архітектури:
![]() |
Код [InitDB] виглядає так:
package tests;
...
public class InitDB {
// сервісний шар
private static IService service;
// конструктор
public static void main(String[] args) throws ParseException {
// конфігурація додатка
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// сервісний рівень
service = (IService) ctx.getBean("service");
// очищення бази даних
clean();
// заповнення
fill();
// візуальна перевірка
dumpPersonnes();
}
// відображення вмісту таблиці
private static void dumpPersonnes() {
System.out.format("[personnes]%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// заповнення таблиці
public static void fill() throws ParseException {
// створення осіб
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// збереження
service.saveArray(new Personne[] { p1, p2 });
}
// видалення елементів з таблиці
public static void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
}
- рядок 12: файл [spring-config.xml] використовується для створення об’єкта [ApplicationContext ctx], який є зображенням файлу в пам’яті. При цьому створюються екземпляри бінів, визначених у [spring-config.xml].
- рядок 14: у контексті додатка ctx запитується посилання на шар [service]. Відомо, що цей шар представлений біном під назвою «service».
- рядок 16: база даних очищується за допомогою методу clean рядків 41–45:
- рядки 42–44: запитується список усіх осіб у контексті персистентності, і за допомогою циклу вони видаляються по черзі. Можливо, ви пам’ятаєте, що [spring-config.xml] визначає, що база даних має бути згенерована під час запуску додатка. Тому в нашому випадку виклик методу clean є зайвим, оскільки ми починаємо з порожньої бази даних.
- рядок 18: метод fill заповнює базу даних. Вона визначена в рядках 32–38:
- рядки 34–35: створюються два записи про осіб
- рядок 37: до шару [service] надсилається запит на збереження цих записів.
- рядок 20: метод dumpPersonnes виводить на екран постійні записи про осіб. Він визначений у рядках 24–29
- рядки 26–28: до шару [service] надсилається запит на отримання списку всіх постійних осіб, які виводяться на консоль.
Виконання [InitDB] дає такий результат:
3.1.7. Одиничні тести [TestNG]
Встановлення плагіна [TestNG] описано в розділі 5.2.4. Код програми [TestNG] такий:
package tests;
....
public class TestNG {
// сервісний шар
private IService service;
@BeforeClass
public void init() {
// журнал
log("init");
// конфігурація додатка
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// сервісний рівень
service = (IService) ctx.getBean("service");
}
@BeforeMethod
public void setUp() throws ParseException {
// очищення бази даних
clean();
// заповнення бази
fill();
}
// журнали
private void log(String message) {
System.out.println("----------- " + message);
}
// відображення вмісту таблиці
private void dump() {
log("dump");
System.out.format("[personnes]%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// заповнення таблиці
public void fill() throws ParseException {
log("fill");
// створення осіб
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// які ми зберігаємо
service.saveArray(new Personne[] { p1, p2 });
}
// видалення елементів з таблиці
public void clean() {
log("clean");
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
@Test()
public void test01() {
...
}
...
}
- рядок 9: анотація @BeforeClass вказує на метод, який слід виконати для ініціалізації конфігурації, необхідної для тестів. Він виконується до запуску першого тесту. Анотація @AfterClass, яка тут не використовується, позначає метод, що має бути виконаний після завершення всіх тестів.
- рядки 10–17: метод init, позначений анотацією @BeforeClass, використовує файл конфігурації Spring для створення екземплярів різних рівнів додатка та отримання посилання на рівень [service]. Усі тести потім використовують це посилання.
- рядок 19: анотація @BeforeMethod вказує на метод, який слід виконати перед кожним тестом. Анотація @AfterMethod, яка тут не використовується, вказує на метод, який слід виконати після кожного тесту.
- рядки 20–25: метод setUp, позначений анотацією @BeforeMethod, очищає базу даних (clean, рядки 52–56), а потім заповнює її даними про двох осіб (fill, рядки 42–49).
- рядок 59: анотація @Test вказує на тестовий метод, який потрібно виконати. Тепер ми опишемо ці тести.
@Test()
public void test01() {
log("test1");
dump();
// список осіб
List<Personne> personnes = service.getAll();
assert 2 == personnes.size();
}
@Test()
public void test02() {
log("test2");
// пошук осіб за іменем
List<Personne> personnes = service.getAllLike("p1%");
assert 1 == personnes.size();
Personne p1 = personnes.get(0);
assert "Paul".equals(p1.getPrenom());
}
@Test()
public void test03() throws ParseException {
log("test3");
// створення нової особи
Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// збереження запису
service.saveOne(p3);
// повторний запит
Personne loadedp3 = service.getOne(p3.getId());
// виведення даних
System.out.println(loadedp3);
// перевірка
assert "p3".equals(loadedp3.getNom());
}
- рядки 2–8: тест 01. Слід пам’ятати, що на початку кожного тесту база даних містить двох осіб із іменами p1 та p2 відповідно.
- рядок 6: запитується список осіб
- рядок 7: перевіряємо, чи кількість осіб у отриманому списку дорівнює 2
- рядок 14: запитується список осіб, ім'я яких починається з p1
- перевіряємо, що отриманий список містить лише один елемент (рядок 15) і що ім'я єдиної отриманої особи — «Paul» (рядок 17)
- рядок 24: створюється особа з ім’ям p3
- рядок 25: її зберігають
- рядок 28: запитуємо її знову у контексті збереження для перевірки
- рядок 32: перевіряємо, чи отримана особа дійсно має ім’я p3.
@Test()
public void test04() throws ParseException {
log("test4");
// завантаження особи p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// виводиться
System.out.println(p1);
// перевіряється
assert "p1".equals(p1.getNom());
int version1 = p1.getVersion();
// змінюється ім'я
p1.setPrenom("x");
// зберігаємо
service.updateOne(p1);
// перезавантажується
p1 = service.getOne(p1.getId());
// відображається
System.out.println(p1);
// перевіряємо, чи було збільшено версію
assert (version1 + 1) == p1.getVersion();
}
- рядок 5: запитуємо особу p1
- рядок 10: перевіряємо її ім’я
- рядок 11: записуємо її номер версії
- рядок 13: змінюємо її ім’я
- рядок 15: зберігаємо зміну
- рядок 17: знову запитуємо особу p1
- рядок 21: перевіряємо, чи номер версії збільшився на 1
@Test()
public void test05() {
log("test5");
// завантажуємо особу p2
List<Personne> personnes = service.getAllLike("p2%");
Personne p2 = personnes.get(0);
// виводимо
System.out.println(p2);
// перевіряємо
assert "p2".equals(p2.getNom());
// видаляємо особу p2
service.deleteOne(p2.getId());
// завантажуємо її знову
p2 = service.getOne(p2.getId());
// перевіряємо, чи отримано нульовий покажчик
assert null == p2;
// виводимо таблицю
dump();
}
- рядок 5: запитуємо особу p2
- рядок 10: перевіряємо її ім'я
- рядок 12: видаляємо її
- рядок 14: запитуємо її знову
- рядок 16: перевіряється, чи не знайдено її
@Test()
public void test06() throws ParseException {
log("test6");
// створюється таблиця з 2 особами з однаковим ім'ям (порушується правило унікальності імені)
Personne[] personnes = { new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2)};
// зберігаємо цей масив — має виникнути виняток і відбутися відкат
boolean erreur = false;
try {
service.saveArray(personnes);
} catch (RuntimeException e) {
erreur = true;
}
// вивід даних
dump();
// перевірки
assert erreur;
// пошук особи з іменем p3
List<Personne> personnesp3 = service.getAllLike("p3%");
assert 0 == personnesp3.size();
// зливання
dump();
}
- рядок 5: створюється масив із трьох осіб, дві з яких мають однакове ім’я «p4». Це порушує правило унікальності імені @Entity Personne:
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
- рядок 11: масив із трьох осіб поміщається в контекст персистентності. Додавання другої особи p4 має завершитися невдачею. Оскільки метод [saveArray] виконується в рамках транзакції, усі вставки, які могли бути зроблені раніше, будуть скасовані. У підсумку жодне додавання не відбудеться.
- рядок 18: перевіряється, чи метод [saveArray] дійсно викликав виняток
- рядки 20–21: перевіряється, що особа p3, яку можна було б додати, не була додана.
@Test()
public void test07() {
log("test7");
// тест оптимістичної блокування
// завантажуємо особу p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// виводимо її
System.out.println(p1);
// збільшуємо кількість її дітей
int nbEnfants1 = p1.getNbenfants();
p1.setNbenfants(nbEnfants1 + 1);
// зберігаємо p1
Personne newp1 = service.updateOne(p1);
assert (nbEnfants1 + 1) == newp1.getNbenfants();
System.out.println(newp1);
// зберігаємо вдруге — має виникнути виняток, оскільки p1 більше не має правильної версії
// це newp1
boolean erreur = false;
try {
service.updateOne(p1);
} catch (RuntimeException e) {
erreur = true;
}
// перевірка
assert erreur;
// збільшуємо кількість дочірніх елементів newp1
int nbEnfants2 = newp1.getNbenfants();
newp1.setNbenfants(nbEnfants2 + 1);
// зберігаємо newp1
service.updateOne(newp1);
// перезавантажуємо
p1 = service.getOne(p1.getId());
// перевіряємо
assert (nbEnfants1 + 2) == p1.getNbenfants();
System.out.println(p1);
}
- рядок 6: запитується особа p1
- рядок 12: збільшуємо на 1 кількість її дітей
- рядок 14: оновлюємо особу p1 у контексті збереження даних. Метод [updateOne] зберігає нову версію newp1 як постійну версію p1. Вона відрізняється від p1 номером версії, який мав бути збільшений.
- рядок 15: перевіряється кількість дітей у newp1.
- рядок 21: повторно запитується оновлення особи p1 на основі старої версії p1. Має виникнути виняток, оскільки p1 не є останньою версією особи p1. Останньою версією є newp1.
- рядок 23: перевіряється, чи дійсно сталася помилка
- рядки 27–35: перевіряємо, що якщо оновлення виконується на основі останньої версії newp1, то все відбувається нормально.
@Test()
public void test08() {
log("test8");
// тест відкоту на updateArray
// завантажується особа p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// виводиться
System.out.println(p1);
// збільшуємо кількість її дітей
int nbEnfants1 = p1.getNbenfants();
p1.setNbenfants(nbEnfants1 + 1);
// зберігаємо 2 зміни, друга з яких має завершитися невдачею (особа неправильно ініціалізована)
// через транзакцію обидві операції мають бути скасовані
boolean erreur = false;
try {
service.updateArray(new Personne[] { p1, new Personne() });
} catch (RuntimeException e) {
erreur = true;
}
// перевірки
assert erreur;
// перезавантажується особа p1
personnes = service.getAllLike("p1%");
p1 = personnes.get(0);
// кількість її дітей не мала змінитися
assert nbEnfants1 == p1.getNbenfants();
}
- тест 8 подібний до тесту 6: він перевіряє rollback на updateArray, що працює з масивом із двох осіб, де друга особа не була правильно ініціалізована. З точки зору JPA операція злиття щодо другої особи, якаіснує, згенерує операцію SQL insert, яка завершиться невдачею через обмеження nullable=false, що існують для деяких полів сутності Personne.
@Test()
public void test09() {
log("test9");
// тест відкоту на deleteArray
// дамп
dump();
// завантажуємо особу p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// виводимо її
System.out.println(p1);
// виконується 2 видалення, друге з яких має завершитися невдачею (невідома особа)
// через транзакцію обидва записи мають бути скасовані
boolean erreur = false;
try {
service.deleteArray(new Personne[] { p1, new Personne() });
} catch (RuntimeException e) {
erreur = true;
}
// перевірки
assert erreur;
// поповнюється рахунок особи p1
personnes = service.getAllLike("p1%");
// перевірка
assert 1 == personnes.size();
// зливання даних
dump();
}
- тест 9 подібний до попереднього: він перевіряє rollback на deleteArray, що працює з масивом із двох осіб, де друга особа не існує. Однак у цьому випадку метод [deleteOne] з рівня [dao] генерує виняток.
// оптимістична блокування — багатопотоковий доступ
@Test()
public void test10() throws Exception {
// додавання особи
Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
service.saveOne(p3);
int id3 = p3.getId();
// створення N потоків для оновлення кількості дітей
final int N = 20;
Thread[] taches = new Thread[N];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajEnfants("thread n° " + i, service, id3);
taches[i].start();
}
// очікування завершення роботи потоків
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// отримання особи
p3 = service.getOne(id3);
// вона повинна мати N дітей
assert N == p3.getNbenfants();
// видалення особи p3
service.deleteOne(p3.getId());
// перевірка
p3 = service.getOne(p3.getId());
// повинен бути покажчик null
assert p3 == null;
}
- Ідея тесту 10 полягає в запуску N потоків (рядок 9) для паралельного збільшення кількості дітей у людини. Ми хочемо перевірити, чи система номеру версії добре витримує цей випадок. Вона була створена саме для цього.
- рядки 5–6: створюється особа з іменем p3, яка потім зберігається. Спочатку вона має 0 дітей.
- рядок 7: записується її ідентифікатор.
- рядки 9–14: паралельно запускаються N потоків, кожен з яких має завдання збільшити на 1 кількість дітей p3.
- рядки 16–18: очікуємо завершення роботи всіх потоків
- рядок 20: запитуємо про особу p3
- рядок 22: перевіряємо, чи має вона тепер N дітей
- рядок 24: особа p3 видаляється.
Потік [ThreadMajEnfants] виглядає так:
package tests;
...
public class ThreadMajEnfants extends Thread {
// ім'я потоку
private String name;
// посилання на рівень [service]
private IService service;
// ідентифікатор особи, з якою будемо працювати
private int idPersonne;
// конструктор
public ThreadMajEnfants(String name, IService service, int idPersonne) {
this.name = name;
this.service = service;
this.idPersonne = idPersonne;
}
// ядро потоку
public void run() {
// відстеження
suivi("lancé");
// цикл повторюється, доки не вдасться збільшити значення на 1
// кількість дітей особи idPersonne
boolean fini = false;
int nbEnfants = 0;
while (!fini) {
// отримуємо копію особи з idPersonne
Personne personne = service.getOne(idPersonne);
nbEnfants = personne.getNbenfants();
// продовження
suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
// збільшуємо на 1 кількість дітей особи
personne.setNbenfants(nbEnfants + 1);
// очікування 10 мс, щоб звільнити процесор
try {
// продовження
suivi("début attente");
// призупиняємося, щоб звільнити процесор
Thread.sleep(10);
// продовження
suivi("fin attente");
} catch (Exception ex) {
throw new RuntimeException(ex.toString());
}
// очікування завершено — намагаємося підтвердити копію
// тим часом інші потоки могли змінити оригінал
try {
// намагаємося змінити оригінал
service.updateOne(personne);
// пройшли — оригінал було змінено
fini = true;
} catch (javax.persistence.OptimisticLockException e) {
// неправильна версія об’єкта: ігноруємо виняток, щоб почати спочатку
} catch (org.springframework.transaction.UnexpectedRollbackException e2) {
// виняток Spring, який час від часу виникає
} catch (RuntimeException e3) {
// інший тип винятку — його передають вище
throw e3;
}
}
// відстеження
suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
}
// відстеження
private void suivi(String message) {
System.out.println(name + " [" + new Date().getTime() + "] : " + message);
}
}
- рядки 15–19: конструктор запам’ятовує інформацію, необхідну для роботи: своє ім’я (рядок 16), посилання на шар [service], яке він має використовувати (рядок 17), та ідентифікатор особи p, кількість дітей якої він має збільшити (рядок 18).
- рядки 22–66: метод [run], що виконується всіма потоками паралельно.
- рядок 29: потік неодноразово намагається збільшити кількість дітей особи p. Він зупиняється лише тоді, коли йому це вдається.
- рядок 31: запитується особа p
- рядок 36: кількість її дітей збільшується в пам'яті
- рядки 38–47: робиться пауза тривалістю 10 мс. Це дозволить іншим потокам отримати ту саму версію особи p. Отже, в один і той самий момент кілька потоків матимуть ту саму версію особи p і прагнутимуть її змінити. Саме цього й потрібно.
- рядок 52: після закінчення паузи потік звертається до шару [service] із запитом зберегти зміну. Ми знаємо, що час від часу траплятимуться винятки, тому цю операцію ми обгорнули блоком try / catch.
- рядок 55: тести показують, що виникають винятки типу [javax.persistence.OptimisticLockException]. Це нормально: це виняток, який генерує рівень JPA, коли потік намагається змінити особу p, не маючи її останньої версії. Цей виняток ігнорується, щоб дозволити потоку повторювати спробу операції, доки він не досягне успіху.
- рядок 57: тести показують, що також виникають винятки типу [org.springframework.transaction.UnexpectedRollbackException]. Це неприємно й несподівано. Я не можу цього пояснити. Ось ми й опинилися в залежності від Spring, хоча хотіли б цього уникнути. Це означає, що якщо запускати наш додаток, наприклад, у JBoss Ejb3, код потоку доведеться змінити. Виняток Spring тут також ігнорується, щоб дозволити потоку повторити спробу операції інкрементування.
- рядок 59: інші типи винятків передаються до додатка.
Під час виконання [TestNG] отримуємо такі результати:

Усі 10 тестів пройшли успішно.
Тест 10 заслуговує на додаткові пояснення, оскільки той факт, що він пройшов успішно, має певну магію. Спочатку повернемося до конфігурації шару [dao]:
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
- рядок 4: об’єкт [EntityManager] вставляється в поле em завдяки анотації JPA @PersistenceContext. Шар [dao] інстанціюється лише один раз. Це синглтон, який використовується всіма потоками, що використовують шар JPA. Отже, EntityManager em є спільним для всіх потоків. Це можна перевірити, вивівши значення em у методі [updateOne], який використовують потоки [ThreadMajEnfants]: для всіх потоків ми отримаємо одне й те саме значення.
Отже, можна задатися питанням, чи не змішаються між собою об’єкти, що зберігаються у різних потоках і обробляються методом EntityManager em, який є однаковим для всіх потоків, і чи не виникнуть між ними конфлікти. Приклад того, що може статися, наведено в [ThreadMajEnfants]:
while (!fini) {
// отримуємо копію запису про особу з idPersonne
Personne personne = service.getOne(idPersonne);
nbEnfants = personne.getNbenfants();
// відстеження
suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
// збільшує на 1 кількість дітей цієї особи
personne.setNbenfants(nbEnfants + 1);
// очікування 10 мс, щоб звільнити процесор
try {
// продовження
suivi("début attente");
// призупиняється, щоб звільнити процесор
Thread.sleep(10);
// відстеження
suivi("fin attente");
} catch (Exception ex) {
throw new RuntimeException(ex.toString());
}
- рядок 3: потік T1 отримує особу p
- рядок 8: він збільшує кількість дітей p
- рядок 14: потік T1 робить паузу
Потік T2 бере на себе управління і також виконує рядок 3: він запитує ту саму особу p, що й T1. Якби контекст збереження потоків був однаковим, то особа p, яка вже перебуває в контексті завдяки T1, мала б бути повернута до T2. Дійсно, метод [getOne] використовує метод [EntityManager].API знаходить JPA, і цей метод звертається до бази даних лише в тому випадку, якщо запитуваний об’єкт не входить до контексту збереження; в іншому разі він повертає об’єкт із контексту збереження. У такому випадку T1 та T2 володіли б однією й тією ж особою p. Тоді T2 знову збільшив би кількість дітей p на 1 (рядок 8). Якщо один із потоків успішно виконає оновлення після паузи, то кількість дочірніх елементів p збільшиться на 2, а не на 1, як передбачалося. Тоді можна було б очікувати, що N потоків змінять кількість дочірніх елементів не на N, а на більше. Однак це не так. Отже, можна зробити висновок, що T1 та T2 не мають однакового посилання на p. Це можна перевірити, вивівши адресу p у потоках: вона є різною для кожного з них.
Отже, схоже, що ці потоки:
- використовують один і той самий менеджер контексту персистентності (EntityManager)
- але кожен з них має власний контекст збереження.
Це лише припущення, і тут була б корисна думка експерта.
3.1.8. Змінити на SGBD
![]() |
Щоб змінити SGBD, достатньо замінити файл [src/spring-config.xml] [2] на файл [spring-config.xml] або SGBD у відповідній папці [conf] [1].
Наприклад, файл [spring-config.xml] від Oracle має такий вигляд:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
...
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--
<property name="showSql" value="true" />
-->
<property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
<property name="generateDdl" value="true" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
<!-- джерело даних DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="username" value="jpa" />
<property name="password" value="jpa" />
</bean>
...
</beans>
Змінилися лише деякі рядки порівняно з тим самим файлом, що використовувався раніше для MySQL5:
- рядок 14: діалект SQL, який має використовувати Hibernate
- рядки 25–28: параметри з’єднання JDBC з SGBD
Читачеві пропонується повторити тести, описані для MySQL5, з іншими SGBD.
3.1.9. Зміна реалізації JPA
Повернемося до архітектури попередніх тестів:
![]() |
Ми замінюємо реалізацію JPA / Hibernate на реалізацію JPA / Toplink. Оскільки Toplink використовує інші бібліотеки, ніж Hibernate, ми використовуємо новий проєкт Eclipse:
![]() |
- у [1]: проект Eclipse. Він ідентичний попередньому. Змінюються лише файл конфігурації [spring-config.xml] [2] та бібліотека [jpa-toplink], яка замінює бібліотеку [jpa-hibernate].
- У [3]: папка з прикладами до цього посібника. У [4] — проект Eclipse, який потрібно імпортувати.
Файл конфігурації [spring-config.xml] для Toplink має такий вигляд:
<?xml version="1.0" encoding="UTF-8"?>
<!-- JVM слід запустити з аргументом -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar
(à remplacer par le chemin exact de spring-agent.jar)-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<!-- прикладні шари -->
<bean id="dao" class="dao.Dao" />
<bean id="service" class="service.Service">
<property name="dao" ref="dao" />
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
<!--
<property name="showSql" value="true" />
-->
<property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
<property name="generateDdl" value="true" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
<!-- джерело даних DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="username" value="jpa" />
<property name="password" value="jpa" />
</bean>
<!-- менеджер транзакцій -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- переклад винятків -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- збереження даних -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
Для переходу з Hibernate на Toplink потрібно змінити лише кілька рядків:
- рядок 19: реалізація JPA тепер виконується Toplink
- рядок 23: властивість [databasePlatform] має інше значення, ніж у Hibernate: ім’я класу, властивого Toplink. Де знайти це ім’я, пояснено в параграфі 2.1.15.2.
Ось і все. Варто відзначити, наскільки легко можна змінити SGBD або реалізацію JPA за допомогою Spring.
Проте ми ще не зовсім закінчили. Наприклад, під час виконання [InitDB] виникає виняток, який не так просто зрозуміти:
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [spring-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.
Caused by: java.lang.IllegalStateException: Must start with Java agent to use
Повідомлення про помилку в рядку 1 спонукає ознайомитися з документацією Spring. Там ми дізнаємося трохи більше про роль, яку відіграє незрозуміла декларація у файлі [spring-config.xml]:
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--
<property name="showSql" value="true" />
-->
<property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
<property name="generateDdl" value="true" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
У рядку 1 винятку згадується клас із назвою [InstrumentationLoadTimeWeaver], який можна знайти в рядку 13 конфігураційного файлу Spring. У документації Spring пояснюється, що цей клас у деяких випадках необхідний для завантаження класів додатка і що для його використання JVM має запускатися разом з агентом. Цей агент надається Spring і називається [spring-agent]:
![]() |
- файл [spring-agent.jar] знаходиться у папці <exemples>/lib [1]. Він входить до складу дистрибутива Spring 2.x (див. параграф 5.11).
- У [3] створюється конфігурація виконання [Run/Run...]
- у [4] створюється конфігурація виконання Java (існують різні типи конфігурацій виконання)
![]() |
- у [5] вибираємо вкладку [Main]
- у [6], присвоюємо назву конфігурації
- у [7] вкажіть назву проекту Eclipse, до якого відноситься ця конфігурація (використовуйте кнопку «Browse»)
- у [8] вкажіть назву класу Java, що містить метод [main] (використовуйте кнопку «Browse»)
- у полі [9] перейдіть на вкладку [Arguments]. На ній можна вказати два типи аргументів:
- у [9] — ті, що передаються методу [main]
- у [10] — ті, що передаються до JVM, який виконуватиме код. Агент Spring визначається за допомогою параметра -javaagent:значення у JVM. Значенням є шлях до файлу [spring-agent.jar].
- у [11]: перевіряємо конфігурацію
- у [12]: конфігурація створена
- у [13]: виконується
Після цього запускається [InitDB] і дає ті самі результати, що й з Hibernate. Для [TestNG] потрібно діяти так само:
![]() |
- у [1] створюємо конфігурацію виконання [Run/Run...]
- у [2] створюється конфігурація виконання TestNG
- у [3] вибираємо вкладку [Test]
- у [4], присвоюємо ім’я конфігурації
- у [5] вказуємо назву проекту Eclipse, до якого відноситься ця конфігурація (використовуйте кнопку «Browse»)
- у [6] вкажіть назву класу тестів (використовуйте кнопку «Browse»)
![]() |
- у [7] переходимо на вкладку [Arguments].
- у [8]: задайте аргумент -javaagent для JVM.
- у [9]: підтверджуємо конфігурацію
- у [10]: конфігурація створена
- у [11]: її запускають
Після цього запускається [TestNG] і видає ті самі результати, що й із Hibernate.
3.2. Приклад 2: JBoss EJB3 / JPA з сутністю «Особа»
Ми використовуємо той самий приклад, що й раніше, але виконуємо його в контейнері EJB3, який належить до JBoss:
![]() |
Контейнер Ejb3 зазвичай інтегрований у сервер додатків. JBoss надає «автономний» контейнер Ejb3, який можна використовувати поза сервером додатків. Ми побачимо, що він надає сервіси, аналогічні тим, що надає Spring. Спробуємо з’ясувати, який із цих контейнерів виявиться найзручнішим.
Встановлення контейнера JBoss Ejb3 описано в розділі 5.12.
3.2.1. Проєкт Eclipse / JBoss EJB3 / Hibernate
Проект Eclipse має такий вигляд:
![]() |
![]() |
- у [1]: проєкт Eclipse. Його можна знайти у [6] серед прикладів підручника [5]. Його потрібно імпортувати.
- у [2]: Java-код шарів, представлений у пакетах:
- [entites]: пакет сутностей JPA
- [dao]: рівень доступу до даних — базується на рівні JPA
- [service]: шар, що надає переважно сервіси, а не бізнес-функції. У ньому використовуватиметься сервіс транзакцій контейнера EJB3.
- [tests]: містить тестові програми.
- у [3]: бібліотека [jpa-jbossejb3] містить JAR-файли, необхідні для JBoss EJB3 (див. також [7] та [8]).
- en [4]: папка [conf] містить файли конфігурації для кожного з SGBD, що використовуються в цьому посібнику. Їх щоразу по два: [persistence.xml], що налаштовує рівень JPA, та [jboss-config.xml], що налаштовує контейнер Ejb3.
3.2.2. Елементи JPA
![]() |
Тут керується лише одна суть — суть Personne, розглянута раніше в розділі 3.1.2.
3.2.3. Рівень [dao]
![]() |
Рівень [dao] реалізує інтерфейс [IDao], описаний раніше в розділі 3.1.3.
Реалізація [Dao] цього інтерфейсу є такою:
package dao;
...
@Stateless
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
// видалити особу за її ідентифікатором
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void deleteOne(Integer id) {
Personne personne = em.find(Personne.class, id);
if (personne == null) {
throw new DaoException(2);
}
em.remove(personne);
}
// отримати всіх осіб
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public List<Personne> getAll() {
return em.createQuery("select p from Personne p").getResultList();
}
// отримати осіб, ім'я яких відповідає шаблону
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public List<Personne> getAllLike(String modele) {
return em.createQuery("select p from Personne p where p.nom like :modele")
.setParameter("modele", modele).getResultList();
}
// отримати особу за її ідентифікатором
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne getOne(Integer id) {
return em.find(Personne.class, id);
}
// зберегти особу
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne saveOne(Personne personne) {
em.persist(personne);
return personne;
}
// оновити особу
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne updateOne(Personne personne) {
return em.merge(personne);
}
}
- цей код у всьому ідентичний тому, що ми мали у Spring. Змінюються лише анотації Java, і саме це ми й коментуємо.
- рядок 4: анотація @Stateless робить клас [Dao] безстатусним EJB. Анотація @Stateful робить клас статусним EJB. EJB зі станом має приватні поля, значення яких потрібно зберігати протягом часу. Класичним прикладом є клас, що містить інформацію, пов’язану з веб-користувачем додатка. Екземпляр цього класу пов’язаний із конкретним користувачем, і коли потік виконання запиту цього користувача завершується, екземпляр має зберігатися, щоб бути доступним під час наступного запиту того самого клієнта. EJB @Stateless не має стану. Якщо повернутися до того самого прикладу, то після завершення потоку виконання запиту користувача EJB @Stateless потрапляє до пулу EJB @Stateless і стає доступним для потоку виконання запиту іншого користувача.
- Для розробника поняття EJB 3 @Stateless близьке до поняття синглтона в Spring. Він використовуватиме його в тих самих випадках.
- рядок 7: анотація @PersistenceContext є такою самою, як і та, що зустрічається у версії Spring шару [dao]. Вона позначає поле, яке прийме EntityManager, що дозволить шару [dao] оперувати контекстом персистентності.
- рядок 11: анотація @TransactionAttribute, застосована до методу, слугує для налаштування транзакції, в рамках якої буде виконуватися метод. Ось кілька можливих значень цієї анотації:
- TransactionAttributeType.REQUIRED: метод повинен виконуватися в транзакції. Якщо транзакція вже розпочалася, операції збереження даних методу відбуваються в ній. В іншому випадку створюється та запускається нова транзакція.
- TransactionAttributeType.REQUIRES_NEW: метод повинен виконуватися в новій транзакції. Така транзакція створюється та запускається.
- TransactionAttributeType.MANDATORY: метод повинен виконуватися в існуючій транзакції. Якщо такої транзакції не існує, генерується виняток.
- TransactionAttributeType.NEVER: метод ніколи не виконується в транзакції.
- ...
Анотацію можна було б розмістити безпосередньо на самому класі:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {
У такому разі атрибут застосовується до всіх методів класу.
3.2.4. Рівень [metier / service]
![]() |
Рівень [service] реалізує інтерфейс [IService], розглянутий раніше в розділі 3.1.4. Реалізація [Service] інтерфейсу [IService] ідентична реалізації, розглянутій раніше в розділі 3.1.4, за винятком трьох деталей:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {
// шар [dao]
@EJB
private IDao dao;
public IDao getDao() {
return dao;
}
public void setDao(IDao dao) {
this.dao = dao;
}
- рядок 2: клас [Service] є безстатевим EJB
- рядок 3: усі методи класу [Service] повинні виконуватися в рамках транзакції
- рядки 7–8: посилання на EJB шару [dao] буде введено контейнером EJB у поле [IDao dao] у рядку 8. Саме анотація @EJB у рядку 7 вимагає цього введення. Введений об’єкт має бути EJB. Це важлива відмінність від Spring, де будь-який тип об’єкта може бути введений в інший об’єкт.
3.2.5. Конфігурація шарів
![]() |
Конфігурація шарів [service], [dao] та [JPA] забезпечується наступними файлами:
- [META-INF/persistence.xml] налаштовує шар JPA
- [jboss-config.xml] налаштовує контейнер Ejb3. Він сам використовує файли [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. Ці файли постачаються разом із Jboss Ejb3 і забезпечують конфігурацію за замовчуванням, яку зазвичай не змінюють. Розробника цікавить лише файл [jboss-config.xml]
Розглянемо обидва файли конфігурації:
persistence.xml
<persistence 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" version="1.0">
<persistence-unit name="jpa">
<!-- провайдер JPA — це Hibernate -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<!-- DataSource JTA, що керується середовищем Java EE5 -->
<jta-data-source>java:/datasource</jta-data-source>
<properties>
<!-- пошук сутностей шару JBA -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- журнали SQL Hibernate
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- тип, що підтримується SGBD -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
<!-- відновлення всіх таблиць (drop+create) під час розгортання модуля персистентності -->
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Цей файл схожий на ті, з якими ми вже стикалися під час вивчення сутностей JPA. Він налаштовує шар Hibernate JPA. Нові елементи такі:
- рядок 5: одиниця персистентності jpa не має атрибута transaction-type, який досі завжди був присутній:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
За відсутності значення атрибут transaction-type має значення за замовчуванням «JTA» (для Java Transaction API), що вказує на те, що менеджер транзакцій надається контейнером EJB3. Менеджер «JTA» може робити більше, ніж менеджер «RESOURCE_LOCAL»: він може керувати транзакціями, що охоплюють кілька з’єднань. За допомогою JTA можна відкрити транзакцію t1 на з’єднанні c1 на SGBD 1, транзакцію t2 на з'єднанні c2 з SGBD 2 і розглядати (t1,t2) як єдину транзакцію, в якій або всі операції завершуються успішно (commit), або жодна (rollback).
Тут ми працюємо з менеджером JTA контейнера Jboss Ejb3.
- рядок 11: оголошує джерело даних, яке має використовувати менеджер JTA. Воно вказане у вигляді імені JNDI (Java Naming and Directory Interface). Це джерело даних визначено в [jboss-config.xml].
jboss-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
xmlns="urn:jboss:bean-deployer:2.0">
<!-- фабрика DataSource -->
<bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
<!-- ім'я JNDI для DataSource -->
<property name="jndiName">java:/datasource</property>
<!-- керована база даних -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="connectionURL">jdbc:mysql://localhost:3306/jpa</property>
<property name="userName">jpa</property>
<property name="password">jpa</property>
<!-- властивості пулу з'єднань -->
<property name="minSize">0</property>
<property name="maxSize">10</property>
<property name="blockingTimeout">1000</property>
<property name="idleTimeout">100000</property>
<!-- менеджер транзакцій, тут JTA -->
<property name="transactionManager">
<inject bean="TransactionManager" />
</property>
<!-- менеджер кешу Hibernate -->
<property name="cachedConnectionManager">
<inject bean="CachedConnectionManager" />
</property>
<!-- властивості інстанціювання JNDI? -->
<property name="initialContextProperties">
<inject bean="InitialContextProperties" />
</property>
</bean>
<!-- DataSource запитується у фабриці -->
<bean name="datasource" class="java.lang.Object">
<constructor factoryMethod="getDatasource">
<factory bean="datasourceFactory" />
</constructor>
</bean>
</deployment>
- рядок 3: кореневим тегом файлу є <deployment>. Цей файл розгортання призначений, в основному, для налаштування джерела даних java:/datasource, яке було оголошено у файлі persistence.xml.
- Джерело даних визначається біном «datasource» у рядку 38. Бачимо, що джерело даних отримується (рядок 40) з «фабрики», визначеної біном «datasourceFactory» у рядку 7. Щоб отримати джерело даних додатка, клієнт повинен викликати метод [getDatasource] класу factory (рядок 39).
- рядок 7: factory, що надає джерело даних, є класом Jboss.
- рядок 9: ім’я джерела даних — JNDI. Воно має збігатися з ім’ям, заявленим у тезі <jta-data-source> файлу persistence.xml. Адже рівень JPA використовуватиме саме це ім’я JNDI для запиту до джерела даних.
- рядки 12–15: дещо більш стандартне: параметри JDBC для підключення до SGBD
- рядки 18–21: налаштування внутрішнього пулу з’єднань контейнера JBoss EJB3.
- рядки 24–26: менеджер JTA. Клас [TransactionManager], введений у рядку 25, визначений у файлі [embedded-jboss-beans.xml].
- рядки 28–30: кеш Hibernate — поняття, яке ми ще не розглядали. Клас [CachedConnectionManager], вставлений у рядку 29, визначений у файлі [embedded-jboss-beans.xml]. Слід зауважити, що конфігурація тепер залежить від Hibernate, що створить нам проблеми, коли ми захочемо перейти на Toplink.
- рядки 32–34: конфігурація сервісу JNDI.
Ми завершили роботу з файлом конфігурації Jboss Ejb3. Він є складним, і багато що залишається незрозумілим. Він був взятий із файлу [ref1]. Проте ми зможемо адаптувати його до іншого SGBD (рядки 12–15 з jboss-config.xml, рядок 24 з persistence.xml). Перехід на Toplink не вдалося здійснити через відсутність прикладів.
3.2.6. Клієнтська програма [InitDB]
Ми приступаємо до написання першого клієнта для описаної вище архітектури:
![]() |
Код [InitDB] виглядає так:
package tests;
...
public class InitDB {
// сервісний шар
private static IService service;
// конструктор
public static void main(String[] args) throws ParseException, NamingException {
// запускається контейнер EJB3 JBoss
// використовуються файли конфігурації ejb3-interceptors-aop.xml та embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Створення власних для додатка бінів
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Розгортання всіх EJBs, знайдених у classpath (повільно, сканує все)
// EJB3StandaloneBootstrap.scanClasspath();
// розгортаються всі EJB, знайдені в classpath додатка
EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
// Ініціалізується контекст JNDI. Використовується файл jndi.properties
InitialContext initialContext = new InitialContext();
// Створюється екземпляр сервісного рівня
service = (IService) initialContext.lookup("Service/local");
// База даних очищується
clean();
// заповнюємо її
fill();
// візуальна перевірка
dumpPersonnes();
// зупинка контейнера EJB
EJB3StandaloneBootstrap.shutdown();
}
// виведення вмісту таблиці
private static void dumpPersonnes() {
System.out.format("[personnes]-------------------------------------------------------------------%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// заповнення таблиці
public static void fill() throws ParseException {
// створення осіб
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// збереження даних
service.saveArray(new Personne[] { p1, p2 });
}
// видалення елементів з таблиці
public static void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
}
- Спосіб запуску контейнера Jboss Ejb3 було знайдено у [ref1].
- рядок 13: контейнер запущено. [EJB3StandaloneBootstrap] — це клас контейнера.
- рядок 16: одиниця розгортання, налаштована [jboss-config.xml], розгортається в контейнері: встановлюються менеджер JTA, джерело даних, пул з’єднань, кеш Hibernate, служба JNDI.
- рядок 22: контейнеру надсилається запит на сканування папки bin проекту Eclipse з метою пошуку EJB. EJB з рівнів [service] та [dao] будуть знайдені та управлятимуться контейнером.
- рядок 25: ініціалізується контекст JNDI. Він знадобиться нам для пошуку EJB.
- рядок 28: EJB, що відповідає класу [Service] шару [service], запитується у службі JNDI. До EJB можна отримати доступ локально (local) або через мережу (remote). Тут ім’я «Service/local» шуканого EJB позначає клас [Service] з шару [service] для локального доступу.
- Тепер додаток розгорнуто, і ми маємо посилання на рівень [service]. Ми перебуваємо в тій самій ситуації, що й після 11-го рядка нижче в коді [InitDB] версії Spring. Отже, в обох версіях ми бачимо однаковий код.
public class InitDB {
// сервісний шар
private static IService service;
// конструктор
public static void main(String[] args) throws ParseException {
// конфігурація додатка
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// сервісний рівень
service = (IService) ctx.getBean("service");
// очищення бази даних
clean();
// заповнення
fill();
// візуальна перевірка
dumpPersonnes();
}
...
- рядок 36 (Jboss Ejb3): зупиняємо контейнер Ejb3.
Виконання [InitDB] дає такі результати:
Читачеві пропонується ознайомитися з цими логами. У них міститься цікава інформація про те, що робить контейнер Ejb3.
3.2.7. Одиничні тести [TestNG]
Код програми [TestNG] такий:
package tests;
...
public class TestNG {
// сервісний рівень
private IService service = null;
@BeforeClass
public void init() throws NamingException, ParseException {
// журнал
log("init");
// запускаємо контейнер EJB3 JBoss
// використовуються файли конфігурації ejb3-interceptors-aop.xml та embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Створення власних для додатка бінів
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Розгортання всіх EJBs, знайдених у classpath (повільно, сканує все)
// EJB3StandaloneBootstrap.scanClasspath();
// розгортаються всі EJB, знайдені в classpath додатка
EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
// Ініціалізується контекст JNDI. Використовується файл jndi.properties
InitialContext initialContext = new InitialContext();
// Створюється екземпляр сервісного рівня
service = (IService) initialContext.lookup("Service/local");
// очищення бази даних
clean();
// заповнюємо її
fill();
// візуальна перевірка
dumpPersonnes();
}
@AfterClass
public void terminate() {
// журнал
log("terminate");
// завершення роботи контейнера EJB
EJB3StandaloneBootstrap.shutdown();
}
@BeforeMethod
public void setUp() throws ParseException {
...
}
...
}
- Метод init (рядки 10–37), який слугує для налаштування середовища, необхідного для тестування, повторює код, пояснений раніше в [InitDB].
- Метод `terminate` (рядки 40–45), який виконується наприкінці тестів (за наявності анотації @AfterClass), зупиняє контейнер Ejb3 (рядок 44).
- Все інше залишається таким самим, як і у версії Spring.
Тести проходять успішно:

3.2.8. Змінити на SGBD
![]() |
Щоб змінити SGBD, достатньо замінити вміст папки [META-INF] [2] на вміст папки SGBD у папці [conf] [1]. Розглянемо приклад сервера SQL:
Файл [persistence.xml] має такий вигляд:
<persistence 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" version="1.0">
<persistence-unit name="jpa">
<!-- постачальником JPA є Hibernate -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<!-- DataSource JTA, що керується середовищем Java EE5 -->
<jta-data-source>java:/datasource</jta-data-source>
<properties>
<!-- пошук сутностей шару JBA -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- журнали SQL Hibernate
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- тип, що підтримується SGBD -->
<property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
<!-- відновлення всіх таблиць (drop+create) під час розгортання модуля персистентності -->
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Змінився лише один рядок:
- рядок 24: діалект SQL, який має використовувати Hibernate
Файл [jboss-config.xml] із сервера SQL виглядає так:
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
xmlns="urn:jboss:bean-deployer:2.0">
<!-- фабрика DataSource -->
<bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
<!-- ім'я JNDI для DataSource -->
<property name="jndiName">java:/datasource</property>
<!-- керована база даних -->
<property name="driverClass">com.microsoft.sqlserver.jdbc.SQLServerDriver</property>
<property name="connectionURL">jdbc:sqlserver://localhost\\SQLEXPRESS:1246;databaseName=jpa</property>
<property name="userName">jpa</property>
<property name="password">jpa</property>
<!-- властивості пулу з'єднань -->
...
</bean>
</deployment>
Змінено лише рядки 12–15: вони визначають параметри нового з’єднання JDBC.
Читачеві пропонується повторити тести, описані для файлу MySQL5, з іншими файлами SGBD.
3.2.9. Зміна реалізації JPA
Як зазначалося вище, ми не знайшли прикладів використання контейнера Jboss Ejb3 з Toplink. На сьогодні (червень 2007 року) я досі не знаю, чи можлива така конфігурація.
3.3. Інші приклади
Підсумуємо, що було зроблено з сутністю Personne. Ми побудували три архітектури для проведення одних і тих самих тестів:
1 — реалізація на базі Spring / Hibernate
![]() |
2 — реалізація на базі Spring / Toplink
![]() |
3 — реалізація на базі Jboss Ejb3 / Hibernate
![]() |
Приклади в цьому посібнику охоплюють ці три архітектури разом з іншими сутностями, розглянутими в першій частині посібника:
Категорія — Стаття
![]() |
- у [1]: версія Spring / Hibernate
- у [2]: версія Spring / Toplink
- у [3]: версія Jboss Ejb3 / Hibernate
Особа — Адреса — Діяльність
![]() |
- у [1]: версія Spring / Hibernate
- у [2]: версія Spring / Toplink
- у [3]: версія Jboss Ejb3 / Hibernate
Ці приклади не містять нічого нового щодо архітектури. Вони просто розміщені в контексті, де є кілька сутностей, які потрібно керувати, з відносинами «один до багатьох» або «багато до багатьох» між ними, чого не було в прикладах із сутністю Personne.
3.4. Приклад 3: Spring / JPA у веб-додатку
3.4.1. Огляд
Тут ми повертаємося до додатка, представленого в наступному документі:
[ref4]: Основи веб-розробки MVC на Java [http://tahe.developpez.com/java/baseswebmvc/].
Цей документ присвячений основам веб-розробки MVC на Java. Щоб зрозуміти наведений нижче приклад, читач повинен володіти цими знаннями. Веб-додаток використовуватиме сервер Tomcat. Його встановлення та використання в середовищі Eclipse описано в розділі 5.3.
Додаток було розроблено з використанням шару [dao], що базується на інструменті Ibatis / SqlMap [http://ibatis.apache.org/], який забезпечував реляційно-об’єктний міст. Ми просто замінюємо Ibatis на JPA. Архітектура додатка буде такою:
![]() |
Веб-додаток, який ми будемо писати, дозволить керувати групою осіб за допомогою чотирьох операцій:
- перелік осіб у групі
- додавання особи до групи
- редагування особи в групі
- видалення особи з групи
Ці чотири основні операції відповідають операціям з таблицею бази даних. На наступних скріншотах показано сторінки, які додаток відображає користувачеві.
![]() |
![]() |
![]() |
![]() |
![]() |
3.4.2. Проект Eclipse
Проект Eclipse для цього додатка виглядає так:
![]() |
- у [1]: веб-проект. Це проект Eclipse типу [Dynamic Web Project] [2]. Його можна знайти у файлі [4] у папці [3] з прикладами до підручника. Його потрібно імпортувати.
![]() |
- у [5]: вихідні коди та конфігурація шарів [service, dao, jpa]. Ми зберігаємо результати роботи [dao, entites, service] з проєкту Eclipse [hibernate-spring-personnes-metier-dao], розглянутого в розділі 3.1.1. Ми розробляємо лише шар [web], представлений тут пакетом [web]. Крім того, ми зберігаємо конфігураційні файли [persistence.xml, spring-config.xml] цього проєкту з тією лише відмінністю, що будемо використовувати Postgres SGBD, що зумовлює такі зміни у [spring-config.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
...
<property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
...
</property>
...
</bean>
<!-- джерело даних DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql:jpa" />
<property name="username" value="jpa" />
<property name="password" value="jpa" />
</bean>
....
</beans>
Рядки 8 та 16–19 було адаптовано до Postgres.
- У [6]: папка [WebContent] містить сторінки JSP цього проєкту, а також необхідні бібліотеки. Останні представлені у [8]
- програма може використовуватися з різними SGBD. Для цього достатньо змінити файл [spring-config.xml]. Папка [conf] [7] містить файл [spring-config.xml], адаптований для різних SGBD.
3.4.3. Рівень [web]
Наш додаток має таку багаторівневу архітектуру:
![]() |
Рівень [web] надаватиме користувачеві екрани, що дозволять йому керувати групою осіб:
- список осіб у групі
- додавання особи до групи
- редагування особи в групі
- видалення особи з групи
Для цього вона буде спиратися на шар [service], який, у свою чергу, буде звертатися до шару [dao]. Ми вже розглядали екрани, що обслуговуються шаром [web] (розділ 3.4.1). Для опису веб-шару ми послідовно розглянемо:
- її конфігурацію
- її подання
- її контролер
- деякі тести
3.4.3.1. Конфігурація веб-додатку
Повернемося до архітектури проєкту Eclipse:
![]() | ![]() |
- у пакеті [web] міститься контролер веб-додатку: клас [Application].
- Сторінки JSP / JSTL додатка знаходяться в [WEB-INF/vues].
- Папка [WEB-INF/lib] містить сторонні архіви, необхідні для роботи додатка. Їх можна переглянути в папці [Web App Libraries].
[web.xml]
Файл [web.xml] — це файл, який використовується веб-сервером для завантаження програми. Його вміст такий:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>spring-jpa-hibernate-personnes-crud</display-name>
<!-- ServletPersonne -->
<servlet>
<servlet-name>personnes</servlet-name>
<servlet-class>web.Application</servlet-class>
<init-param>
<param-name>urlEdit</param-name>
<param-value>/WEB-INF/vues/edit.jsp</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/WEB-INF/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlList</param-name>
<param-value>/WEB-INF/vues/list.jsp</param-value>
</init-param>
</servlet>
<!-- зіставлення ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- файли головних сторінок -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Сторінка несподіваної помилки -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- рядки 23–26: URL-адреси [/do/*] оброблятиме сервлет [personnes]
- рядки 7–8: сервлет [personnes] є екземпляром класу [Application], який ми будемо створювати.
- рядки 9–20: визначають три параметри [urlList, urlEdit, urlErreurs], що ідентифікують URL-адреси сторінок JSP для переглядів [list, edit, erreurs].
- рядки 28–30: додаток має сторінку входу за замовчуванням [index.jsp], яка знаходиться в кореневій папці веб-додатка.
- рядки 32–35: у додатка є сторінка помилок за замовчуванням, яка відображається, коли веб-сервер отримує виняток, що не обробляється додатком.
- рядок 37: тег <exception-type> вказує тип винятку, що обробляється директивою <error-page>; у даному випадку це тип [java.lang.Exception] та його похідні, тобто всі винятки.
- рядок 38: тег <location> вказує на сторінку JSP, яку слід відобразити, коли виникає виняток типу, визначеного тегом <exception-type>. Інформація про виняток, що виник, доступна на цій сторінці в об’єкті з іменем exception, якщо на сторінці є директива:
<%@ page isErrorPage="true" %>
- (продовження)
- якщо <exception-type> вказує тип T1, і виняток типу T2, що не є похідним від T1, передається до веб-сервера, останній надсилає клієнту власну сторінку винятку, яка зазвичай є малозручною. Звідси й корисність тегу <error-page> у файлі [web.xml].
[index.jsp]
Ця сторінка відображається, якщо користувач безпосередньо запитує контекст додатка, не вказавши URL-адресу, c.a.d. тут [/spring-jpa-hibernate-personnes-crud]. Її вміст такий:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/do/list"/>
[index.jsp] перенаправляє (рядок 4) клієнта на URL-адресу [/do/list]. Ця URL-адреса відображає список осіб у групі.
3.4.3.2. Сторінки JSP / JSTL додатка
Вигляд [list.jsp]
Він слугує для відображення списку осіб:

Його код такий:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<c:if test="${erreurs!=null}">
<h3>Les erreurs suivantes se sont produites :</h3>
<ul>
<c:forEach items="${erreurs}" var="erreur">
<li><c:out value="${erreur}"/></li>
</c:forEach>
</ul>
<hr>
</c:if>
<h2>Liste des personnes</h2>
<table border="1">
<tr>
<th>Id</th>
<th>Version</th>
<th>Prénom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</th>
<th>Nombre d'enfants</th>
<th></th>
</tr>
<c:forEach var="personne" items="${personnes}">
<tr>
<td><c:out value="${personne.id}"/></td>
<td><c:out value="${personne.version}"/></td>
<td><c:out value="${personne.prenom}"/></td>
<td><c:out value="${personne.nom}"/></td>
<td><dt:format pattern="dd/MM/yyyy">${personne.datenaissance.time}</dt:format></td>
<td><c:out value="${personne.marie}"/></td>
<td><c:out value="${personne.nbenfants}"/></td>
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
- Цей вигляд отримує два елементи у своїй моделі:
- елемент [personnes], пов’язаний з об’єктом типу [List], що містить об’єкти типу [Personne]: список осіб.
- опціональний елемент [erreurs], пов’язаний з об’єктом типу [List], що містить об’єкти типу [String]: список повідомлень про помилки.
- рядки 31–43: виконується перебір списку ${personnes} для відображення масиву HTML, що містить осіб із групи.
- рядок 40: URL-адреса, на яку вказує посилання [Modifier], налаштовується за допомогою поля [id] поточної особи, щоб контролер, пов’язаний з URL-адресою [/do/edit], знав, яку особу потрібно змінити.
- рядок 41: те саме робиться для посилання [Supprimer].
- рядок 37: щоб відобразити дату народження особи у форматі JJ/MM/AAAA, використовується тег <dt> із бібліотеки тегів [DateTime] проекту Apache [Jakarta Taglibs]:

Файл опису цієї бібліотеки тегів визначено у рядку 3.
- рядок 46: посилання [Ajout] для додавання нової особи має цільовим URL-адресу [/do/edit], так само як і посилання [Modifier] у рядку 40. Саме значення -1 параметра [id] вказує на те, що мова йде про додавання, а не про редагування.
- рядки 10–18: якщо елемент ${erreurs} є в шаблоні, то виводяться повідомлення про помилки, що містяться в ньому.
Вигляд [edit.jsp]
Він слугує для відображення форми додавання нової особи або редагування існуючої особи:
![]() |
Код подання [edit.jsp] такий:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="../ressources/standard.jpg">
<h2>Ajout/Modification d'une personne</h2>
<c:if test="${erreurEdit!=''}">
<h3>Echec de la mise à jour :</h3>
L'erreur suivante s'est produite : ${erreurEdit}
<hr>
</c:if>
<form method="post" action="<c:url value="/do/validate"/>">
<table border="1">
<tr>
<td>Id</td>
<td>${id}</td>
</tr>
<tr>
<td>Version</td>
<td>${version}</td>
</tr>
<tr>
<td>Prénom</td>
<td>
<input type="text" value="${prenom}" name="prenom" size="20">
</td>
<td>${erreurPrenom}</td>
</tr>
<tr>
<td>Nom</td>
<td>
<input type="text" value="${nom}" name="nom" size="20">
</td>
<td>${erreurNom}</td>
</tr>
<tr>
<td>Date de naissance (JJ/MM/AAAA)</td>
<td>
<input type="text" value="${datenaissance}" name="datenaissance">
</td>
<td>${erreurDateNaissance}</td>
</tr>
<tr>
<td>Marié</td>
<td>
<c:choose>
<c:when test="${marie}">
<input type="radio" name="marie" value="true" checked>Oui
<input type="radio" name="marie" value="false">Non
</c:when>
<c:otherwise>
<input type="radio" name="marie" value="true">Oui
<input type="radio" name="marie" value="false" checked>Non
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<td>Nombre d'enfants</td>
<td>
<input type="text" value="${nbenfants}" name="nbenfants">
</td>
<td>${erreurNbEnfants}</td>
</tr>
</table>
<br>
<input type="hidden" value="${id}" name="id">
<input type="hidden" value="${version}" name="version">
<input type="submit" value="Valider">
<a href="<c:url value="/do/list"/>">Annuler</a>
</form>
</body>
</html>
Цей вигляд містить форму для додавання нової особи або оновлення існуючої. Надалі, для спрощення викладу, ми будемо використовувати єдиний термін [mise à jour]. Кнопка [Valider] (рядок 73) викликає POST форми за URL-адресою [/do/validate] (рядок 16). Якщо POST завершується з помилкою, знову відображається сторінка [edit.jsp] із зазначенням помилки (або помилок), що сталися; в іншому випадку відображається сторінка [list.jsp].
- Вигляд [edit.jsp], що відображається як на GET, так і на POST, який завершився невдало, отримує у свою модель такі елементи:
атрибут | GET | POST |
ідентифікатор особи, що оновлено | те саме | |
його версія | те саме | |
його ім'я | введене ім'я | |
його прізвище | введене прізвище | |
дата народження | введена дата народження | |
його сімейний стан | введений сімейний стан | |
кількість дітей | введена кількість дітей | |
поле порожнє | повідомлення про помилку, що вказує на невдале додавання або зміни під час виконання POST, викликаного кнопкою [Envoyer]. Поле залишається порожнім, якщо помилки не було. | |
порожнє | вказує на неправильне ім’я — у протилежному випадку поле порожнє | |
порожнє | вказує на помилкове прізвище – у разі відсутності значення | |
порожнє | позначає неправильну дату народження – у протилежному випадку порожнє | |
порожнє | вказує на неправильну кількість дітей – у протилежному випадку залишається порожнім |
- рядки 11–15: якщо виконання POST у формі завершиться з помилкою, буде отримано [erreurEdit!=''] і відобразиться повідомлення про помилку.
- рядок 16: форма буде відправлена за URL-адресою [/do/validate]
- рядок 20: відображається елемент шаблону [id]
- рядок 24: відображається елемент [version] шаблону
- рядки 26–32: введення імені особи:
- під час початкового відображення форми (GET), ${ім'я} відображає поточне значення поля [prenom] оновленого об'єкта [Personne], а ${erreurPrenom} є порожнім.
- у разі помилки після POST знову відображається введене значення ${prenom}, а також можливе повідомлення про помилку ${erreurPrenom}
- рядки 33–39: введення прізвища особи
- рядки 40–46: введення дати народження особи
- рядки 47–61: введення сімейного стану особи за допомогою перемикача. Для визначення, який із двох перемикачів слід позначити, використовується значення поля [marie] об’єкта [Personne].
- рядки 62–68: введення кількості дітей особи
- рядок 71: приховане поле HTML з іменем [id], значенням якого є поле [id] особи, дані якої оновлюються; -1 — для додавання, інше значення — для редагування.
- рядок 72: приховане поле HTML з іменем [version], значення якого дорівнює значенню поля [id] особи, що оновлюється.
- рядок 73: кнопка [Valider] типу [Submit] у формі
- рядок 74: посилання, що дозволяє повернутися до списку осіб. Воно отримало назву [Annuler], оскільки дозволяє покинути форму без її підтвердження.
Вигляд [exception.jsp]
Вона служить для відображення сторінки, що повідомляє про виникнення винятку, який не обробляється додатком і який був переданий на веб-сервер.
Наприклад, видалимо особу, якої немає в групі:
![]() |
Код подання [exception.jsp] такий:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>
<%
response.setStatus(200);
%>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<h2>MVC - personnes</h2>
L'exception suivante s'est produite :
<%= exception.getMessage()%>
<br><br>
<a href="<c:url value="/do/list"/>">Retour à la liste</a>
</body>
</html>
- Цей вигляд отримує у своїй моделі елемент [exception], який є винятком, перехопленим веб-сервером. Щоб цей елемент був включений веб-сервером до моделі сторінки JSP, на сторінці має бути визначений тег у рядку 3.
- рядок 6: код стану відповіді HTTP встановлюється на 200. Це перший заголовок відповіді HTTP. Код 200 означає для клієнта, що його запит було виконано. Зазвичай у відповідь сервера вбудовується документ HTML. Так і є в даному випадку. Якщо не встановити код стану відповіді HTTP на 200, він матиме тут значення 500, що означає, що сталася помилка. Дійсно, веб-сервер, перехопивши некероване виключення, розцінює цю ситуацію як аномальну та сигналізує про це кодом 500. Реакція на код 500 у відповіді HTTP відрізняється залежно від браузера: Firefox відображає документ HTML, який може супроводжувати цю відповідь, тоді як IE ігнорує цей документ і відображає власну сторінку. Саме з цієї причини ми замінили код 500 на код 200.
- рядок 16: відображається текст винятку
- рядок 18: користувачеві пропонується посилання для повернення до списку осіб
Вигляд [erreurs.jsp]
Вона призначена для відображення сторінки, що повідомляє про помилки ініціалізації додатка c.a.d та про помилки, виявлені під час виконання методу [init] сервлета контролера. Це може бути, наприклад, відсутність параметра у файлі [web.xml], як показано у прикладі нижче:

Код сторінки [erreurs.jsp] такий:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li>${erreur}</li>
</c:forEach>
</ul>
</body>
</html>
Сторінка отримує у свій шаблон елемент [erreurs], який є об’єктом типу [ArrayList], що містить об’єкти [String], які, у свою чергу, є повідомленнями про помилки. Вони відображаються за допомогою циклу у рядках 13–15.
3.4.3.3. Контролер додатка
Контролер [Application] визначено в пакеті [web]:

Структу та ініціалізація контролера
Структура контролера [Application] така:
package web;
...
@SuppressWarnings("serial")
public class Application extends HttpServlet {
// параметри екземпляра
private String urlErreurs = null;
private ArrayList erreursInitialisation = new ArrayList<String>();
private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
private Map params = new HashMap<String, String>();
// служба
private IService service = null;
// ініціалізація
@SuppressWarnings("unchecked")
public void init() throws ServletException {
// отримано параметри ініціалізації сервлета
ServletConfig config = getServletConfig();
// обробка інших параметрів ініціалізації
String valeur = null;
for (int i = 0; i < paramètres.length; i++) {
// значення параметра
valeur = config.getInitParameter(paramètres[i]);
// параметр присутній?
if (valeur == null) {
// фіксується помилка
erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
} else {
// зберігаємо значення параметра
params.put(paramètres[i], valeur);
}
}
// URL-адреса подання [erreurs] обробляється окремо
urlErreurs = config.getInitParameter("urlErreurs");
if (urlErreurs == null)
throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
// конфігурація додатка
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// сервісний рівень
service = (IService) ctx.getBean("service");
// очищення бази даних
clean();
// заповнення бази даних
try {
fill();
} catch (ParseException e) {
throw new ServletException(e);
}
}
// заповнення таблиці
public void fill() throws ParseException {
// створення осіб
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// збереження
service.saveArray(new Personne[] { p1, p2 });
}
// видалення елементів з таблиці
public void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
// GET
@SuppressWarnings("unchecked")
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
...
}
// перегляд списку осіб
private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// редагування / додавання особи
private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// видалення особи
private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// підтвердження редагування / додавання особи
public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// перегляд попередньо заповненої форми
private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
...
}
// відправлення
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// передача управління GET
doGet(request, response);
}
}
- рядки 21–34: отримуємо очікувані параметри з файлу [web.xml].
- рядки 37–39: параметр [urlErreurs] має бути обов’язково присутнім, оскільки він вказує URL-адресу подання [erreurs], здатного відображати можливі помилки ініціалізації. Якщо його немає, додаток переривається шляхом запуску [ServletException] (рядок 39). Цей виняток буде передано на веб-сервер і оброблено тегом <error-page> у файлі [web.xml]. Отже, відображається сторінка [exception.jsp]:

Наведене вище посилання [Retour à la liste] не працює. Його використання дасть той самий результат, доки додаток не буде змінено та перезавантажено. Воно корисне для інших типів винятків, як ми вже бачили.
- рядки 40–43: використовують файл конфігурації Spring для отримання посилання на рівень [service]. Після ініціалізації контролера його методи отримують посилання [service] на рівень [service] (рядок 15), яке вони використовуватимуть для виконання дій, запитуваних користувачем. Ці дії будуть перехоплені методом [doGet], який передасть їх на обробку спеціальному методу контролера:
Url | Метод HTTP | метод контролера |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
Метод [doGet]
Цей метод призначений для спрямування обробки дій, запитуваних користувачем, до відповідного методу. Його код такий:
// GET
@SuppressWarnings("unchecked")
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// перевіряється, як пройшла ініціалізація сервлета
if (erreursInitialisation.size() != 0) {
// передаємо управління сторінці помилок
request.setAttribute("erreurs", erreursInitialisation);
getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
// кінець
return;
}
// отримуємо метод відправлення запиту
String méthode = request.getMethod().toLowerCase();
// отримуємо дію, яку потрібно виконати
String action = request.getPathInfo();
// дія?
if (action == null) {
action = "/list";
}
// виконання дії
if (méthode.equals("get") && action.equals("/list")) {
// список осіб
doListPersonnes(request, response);
return;
}
if (méthode.equals("get") && action.equals("/delete")) {
// видалення особи
doDeletePersonne(request, response);
return;
}
if (méthode.equals("get") && action.equals("/edit")) {
// відображення форми додавання/редагування особи
doEditPersonne(request, response);
return;
}
if (méthode.equals("post") && action.equals("/validate")) {
// підтвердження форми додавання/редагування особи
doValidatePersonne(request, response);
return;
}
// інші випадки
doListPersonnes(request, response);
}
- рядки 7–13: перевіряється, чи список помилок ініціалізації порожній. Якщо це не так, відображається вікно [erreurs(erreurs)], яке повідомить про помилку або помилки.
- рядок 15: отримується метод [get] або [post], який клієнт використав для формування запиту.
- рядок 17: отримуємо значення параметра [action] із запиту.
- рядки 23–27: обробка запиту [GET /do/list], який запитує список осіб.
- рядки 28–32: обробка запиту [GET /do/delete], який вимагає видалення особи.
- рядки 33–37: обробка запиту [GET /do/edit], який вимагає форми для оновлення даних особи.
- рядки 38–42: обробка запиту [POST /do/validate], що вимагає підтвердження оновлених даних особи.
- рядок 44: якщо запитувана дія не є однією з п’яти попередніх, то виконується обробка, як для [GET /do/list].
Метод [doListPersonnes]
Цей метод обробляє запит [GET /do/list], який вимагає переліку осіб:

Його код такий:
// відображення списку осіб
private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// шаблон подання [list]
request.setAttribute("personnes", service.getAll());
// відображення подання [list]
getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
- рядок 4: у шарі [service] запитується список осіб групи, який поміщається в модель під ключем «особи».
- рядок 6: відображається вигляд [list.jsp], описаний у параграфі 3.4.3.2.
Метод [doDeletePersonne]
Цей метод обробляє запит [GET /do/delete?id=XX], який вимагає видалення особи з id=XX. URL-адреса [/do/delete?id=XX] відповідає посиланням [Supprimer] у поданні [list.jsp]:

код якого такий:
...
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
...
<c:forEach var="personne" items="${personnes}">
<tr>
...
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
У рядку 12 ми бачимо URL-адресу [/do/delete?id=XX] посилання [Supprimer]. Метод [doDeletePersonne], який повинен обробити цей URL-адресу, повинен видалити особу з id=XX, а потім відобразити новий список осіб у групі. Його код такий:
// видалення особи
private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// отримання ідентифікатора особи
int id = Integer.parseInt(request.getParameter("id"));
// видалення особи
service.deleteOne(id);
// перенаправлення до списку осіб
response.sendRedirect("list");
}
- рядок 4: оброблюваний URL має вигляд [/do/delete?id=XX]. З параметра [id] отримуємо значення [XX].
- рядок 6: до шару [service] надсилається запит на видалення особи з отриманим ідентифікатором. Ми не проводимо жодної перевірки. Якщо особа, яку ми намагаємося видалити, не існує, шар [dao] генерує виняток, який передається вгору шаром [service]. Ми також не обробляємо це тут, у контролері. Отже, виняток буде передано до веб-сервера, який, згідно з налаштуваннями, відобразить сторінку [exception.jsp], описану в параграфі 3.4.3.2:

- рядок 9: якщо видалення відбулося (винятку немає), клієнту пропонується перейти за відносним URL-адресою [list]. Оскільки щойно оброблено сторінку [/do/delete], URL-адреса перенаправлення буде [/do/list]. Отже, браузер буде змушений виконати запит [GET /do/list], що призведе до відображення списку осіб.
Метод [doEditPersonne]
Цей метод обробляє запит [GET /do/edit?id=XX], який запитує форму оновлення особи з id=XX. URL-адреса [/do/edit?id=XX] є адресою посилань [Modifier] та [Ajout] у поданні [list.jsp]:

код якого такий:
...
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
...
<c:forEach var="personne" items="${personnes}">
<tr>
...
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
У рядку 11 ми бачимо URL-адресу [/do/edit?id=XX] посилання [Modifier], а в рядку 17 — URL-адресу [/do/edit?id=-1] посилання [Ajout]. Метод [doEditPersonne] повинен відобразити форму редагування особи з id=XX або, якщо це додавання, — порожню форму.
![]() |
- у [1] вище — форма додавання, а в [2] — форма редагування.
Код методу [doEditPersonne] такий:
// редагування / додавання особи
private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// отримуємо ідентифікатор особи
int id = Integer.parseInt(request.getParameter("id"));
// додати чи змінити?
Personne personne = null;
if (id != -1) {
// редагування — отримуємо особу для редагування
personne = service.getOne(id);
request.setAttribute("id", personne.getId());
request.setAttribute("version", personne.getVersion());
} else {
// додавання — створюється порожня особа
personne = new Personne();
request.setAttribute("id", -1);
request.setAttribute("version", -1);
}
// додаємо об’єкт [Personne] до сесії користувача
request.getSession().setAttribute("personne", personne);
// та в шаблон подання [edit]
request.setAttribute("erreurEdit", "");
request.setAttribute("prenom", personne.getPrenom());
request.setAttribute("nom", personne.getNom());
Date dateNaissance = personne.getDatenaissance();
if (dateNaissance != null) {
request.setAttribute("datenaissance", new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance));
} else {
request.setAttribute("datenaissance", "");
}
request.setAttribute("marie", personne.isMarie());
request.setAttribute("nbenfants", personne.getNbenfants());
// відображення подання [edit]
getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
}
- GET має за ціль URL-адресу типу [/do/edit?id=XX]. У рядку 4 ми отримуємо значення [id]. Далі можливі два випадки:
- id відмінний від -1. Тоді це редагування, і потрібно відобразити форму, попередньо заповнену даними особи, яку потрібно редагувати. У рядку 9 ця особа запитується на рівні [service].
- id дорівнює -1. У цьому випадку йдеться про додавання, і потрібно відобразити порожню форму. Для цього у рядку 14 створюється порожня особа.
- В обох випадках ініціалізуються елементи [id, version] шаблону сторінки [edit.jsp], описаного в параграфі 3.4.3.2.
- Отриманий об’єкт [Personne] розміщується в шаблоні сторінки [edit.jsp]. Цей шаблон містить такі елементи: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants]. Ці елементи ініціалізуються у рядках 19–31, за винятком тих, значенням яких є порожній рядок [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Відомо, що за їхньої відсутності у шаблоні бібліотека JSTL відображатиме порожній рядок як їхнє значення. Хоча елемент [erreurEdit] також має значенням порожній рядок, він все ж ініціалізується, оскільки на сторінці [edit.jsp] виконується перевірка його значення.
- Після того як шаблон готовий, управління передається на сторінку [edit.jsp], рядок 33, яка згенерує представлення [edit].
Метод [doValidatePersonne]
Цей метод обробляє запит [POST /do/validate], який перевіряє форму оновлення. Цей POST запускається кнопкою [Valider]:

Нагадаємо елементи введення даних у формі HTML, показаній на малюнку вище:
<form method="post" action="<c:url value="/do/validate"/>">
...
<input type="text" value="${nom}" name="nom" size="20">
...
<input type="text" value="${datenaissance}" name="datenaissance">
...
<c:choose>
<c:when test="${marie}">
<input type="radio" name="marie" value="true" checked>Oui
<input type="radio" name="marie" value="false">Non
</c:when>
<c:otherwise>
<input type="radio" name="marie" value="true">Oui
<input type="radio" name="marie" value="false" checked>Non
</c:otherwise>
</c:choose>
...
<input type="text" value="${nbenfants}" name="nbenfants">
...
<input type="hidden" value="${id}" name="id">
<input type="hidden" value="${version}" name="version">
<input type="submit" value="Valider">
<a href="<c:url value="/do/list"/>">Annuler</a>
</form>
Запит POST містить параметри [prenom, nom, datenaissance, marie, nbenfants, id] і надсилається на URL-адресу [/do/validate] (рядок 1). Він обробляється наступним методом [doValidatePersonne]:
// підтвердження зміни / додавання особи
public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// отримання відправлених елементів
boolean formulaireErroné = false;
boolean erreur;
// ім'я
String prenom = request.getParameter("prenom").trim();
// ім'я є дійсним?
if (prenom.length() == 0) {
// фіксуємо помилку
request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
formulaireErroné = true;
}
// прізвище
String nom = request.getParameter("nom").trim();
// ім'я правильне?
if (nom.length() == 0) {
// позначено помилку
request.setAttribute("erreurNom", "Le nom est obligatoire");
formulaireErroné = true;
}
// дата народження
Date datenaissance = null;
try {
datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
} catch (ParseException e) {
// зафіксовано помилку
request.setAttribute("erreurDateNaissance", "Date incorrecte");
formulaireErroné = true;
}
// сімейний стан
boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
// кількість дітей
int nbenfants = 0;
erreur = false;
try {
nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
if (nbenfants < 0) {
erreur = true;
}
} catch (NumberFormatException ex) {
// позначено помилку
erreur = true;
}
// чи кількість дітей вказана неправильно?
if (erreur) {
// повідомляється про помилку
request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
formulaireErroné = true;
}
// ідентифікатор особи
int id = Integer.parseInt(request.getParameter("id"));
// чи є помилки у формі?
if (formulaireErroné) {
// форма відображається знову з повідомленнями про помилки
showFormulaire(request, response, "");
// завершено
return;
}
// форма правильна — оновлюємо дані особи, які були збережені в сесії
// даними, надісланими клієнтом
Personne personne = (Personne)request.getSession().getAttribute("personne");
personne.setDatenaissance(datenaissance);
personne.setMarie(marie);
personne.setNbenfants(nbenfants);
personne.setNom(nom);
personne.setPrenom(prenom);
// збереження даних
try {
if (id == -1) {
// створення
service.saveOne(personne);
} else {
// оновлення
service.updateOne(personne);
}
} catch (DaoException ex) {
// повторно відображається форма з повідомленням про помилку
showFormulaire(request, response, ex.getMessage());
// завершено
return;
}
// перенаправлення до списку осіб
response.sendRedirect("list");
}
// відображення попередньо заповненої форми
private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
// готується шаблон подання [edit]
request.setAttribute("erreurEdit", erreurEdit);
request.setAttribute("id", request.getParameter("id"));
request.setAttribute("version", request.getParameter("version"));
request.setAttribute("prenom", request.getParameter("prenom").trim());
request.setAttribute("nom", request.getParameter("nom").trim());
request.setAttribute("datenaissance", request.getParameter("datenaissance").trim());
request.setAttribute("marie", request.getParameter("marie"));
request.setAttribute("nbenfants", request.getParameter("nbenfants").trim());
// відображення подання [edit]
getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
}
- рядки 7–13: отримується параметр [prenom] із запиту POST і перевіряється його правильність. Якщо він виявляється некоректним, елемент [erreurPrenom] ініціалізується з повідомленням про помилку та розміщується в атрибутах запиту.
- рядки 15–21: аналогічні дії виконуються для параметра [nom]
- рядки 23–30: аналогічним чином обробляється параметр [datenaissance]
- рядок 32: отримується параметр [marie]. Ми не перевіряємо його правильність, оскільки, як правило, він походить від значення перемикача. Однак ніщо не заважає програмі створити [POST /.../do/validate] разом із вигаданим параметром [marie]. Тому нам слід перевірити правильність цього параметра. Тут ми покладаємося на нашу систему обробки винятків, яка викликає відображення сторінки [exception.jsp], якщо контролер не обробляє їх самостійно. Отже, якщо перетворення параметра [marie] у логічне значення завершиться невдало у рядку 32, виникне виняток, що призведе до відправлення сторінки [exception.jsp] клієнту. Такий механізм нас влаштовує.
- рядки 34–50: отримуємо параметр [nbenfants] і перевіряємо його значення.
- рядок 52: отримуємо параметр [id], не перевіряючи його значення
- рядки 54–59: якщо форма містить помилки, її знову відображають разом із попередньо сформованими повідомленнями про помилки
- рядки 62–67: якщо форма є дійсною, створюється новий об’єкт [Personne] з елементами форми
- рядки 69–82: дані про особу зберігаються. Збереження може завершитися невдачею. У багатокористувацькому середовищі особа, яку потрібно змінити, могла бути видалена або вже змінена кимось іншим. У цьому випадку рівень [dao] викличе виняток, який ми обробляємо тут.
- рядок 84: якщо винятку не сталося, клієнта перенаправляють на URL-адресу [/do/list], щоб показати йому новий стан групи.
- рядок 79: якщо під час збереження сталося виключення, ми знову запитуємо перезавантаження початкової форми, передаючи їй повідомлення про помилку виключення (3-й параметр).
Метод [showFormulaire] (рядки 88–97) формує шаблон, необхідний для сторінки [edit.jsp], із введеними значеннями (request.getParameter(" ... ")). Нагадаємо, що повідомлення про помилки вже були вставлені в шаблон методом [doValidatePersonne]. Сторінка [edit.jsp] відображається у рядку 99.
3.4.4. Тестування веб-додатку
У розділі 3.4.1 було наведено низку тестів. Пропонуємо читачеві повторити їх. Тут ми наводимо інші знімки екрана, що ілюструють випадки конфліктів доступу до даних у багатокористувацькому середовищі:
[Firefox] буде браузером користувача U1. Останній запитує URL-адресу [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

[IE7] — це браузер користувача U2. Він запитує той самий URL:

Користувач U1 переходить до редагування запису про особу [p2]:

Користувач U2 робить те саме:

Користувач U1 вносить зміни та підтверджує:
![]() |
Користувач U2 робить те саме:
![]() |
Користувач U2 повертається до списку осіб за посиланням [Retour à la liste] у формі:

Він знаходить особу [Lemarchand] у тому вигляді, в якому її змінив U1 (заміжня, 2 дітей). Номер версії p2 змінився. Тепер U2 видаляє [p2]:
![]() |
U1 як і раніше має власний список і хоче знову змінити [p2]:
![]() |
U1 використовує посилання [Retour à la liste], щоб дізнатися, про що йдеться:

Він виявляє, що [p2] дійсно більше не входить до списку...
3.4.5. Версія 2
Ми трохи змінюємо попередню версію, щоб використовувати архіви шарів [service, dao, jpa], а не їхні вихідні коди:
![]() |
- у [1]: новий проєкт Eclipse. Зверніть увагу на зникнення пакетів [service, dao, entites]. Вони були інкапсульовані в архів [service-dao-jpa-personne.jar] [2], розміщений у [WEB-INF/lib].
- Папка проекту знаходиться в [4]. Її потрібно імпортувати.
Більше нічого робити не потрібно. Коли запускається новий веб-додаток і запитується список осіб, отримується така відповідь:
![]() |
Hibernate не знаходить сутності [Personne]. Щоб вирішити цю проблему, необхідно явно оголосити в [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="jpa" transaction-type="RESOURCE_LOCAL">
<class>entites.Personne</class>
</persistence-unit>
</persistence>
- рядок 7: оголошено суть Personne.
Після цього виняток зникає:
![]() |
3.4.6. Змінити реалізацію JPA
![]() |
- на [1]: новий проект Eclipse
- на [2]: бібліотеки Toplink замінили бібліотеки Hibernate
- папка проєкту знаходиться в [4]. Її потрібно імпортувати.
Зміна реалізації JPA передбачає лише кілька змін у файлі [spring-config.xml]. Більше нічого не змінюється. Зміни, внесені до файлу [spring-config.xml], пояснено в розділі 3.1.9:
<?xml version="1.0" encoding="UTF-8"?>
<!-- JVM потрібно запустити з аргументом -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar
(à remplacer par le chemin exact de spring-agent.jar)-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
...
<property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
...
</bean>
...
</beans>
Для переходу з Hibernate на Toplink потрібно змінити лише кілька рядків:
- рядок 11: реалізація JPA тепер виконується Toplink
- рядок 13: властивість [databasePlatform] має інше значення, ніж у Hibernate: ім’я класу, властивого Toplink. Де знайти це ім’я, пояснено в розділі 2.1.15.2.
Ось і все. Варто відзначити, наскільки легко за допомогою Spring можна змінити SGBD або реалізацію JPA. Проте ми ще не зовсім закінчили. Під час виконання додатка виникає виняток:
![]() |
Це та сама проблема, про яку йшлося в розділі 3.1.9. Її можна вирішити, запустивши JVM за допомогою агента Spring. Для цього потрібно змінити конфігурацію запуску Tomcat:
![]() |
- на [1]: було обрано опцію [Run / Run...] для зміни конфігурації Tomcat
- на [2]: ми вибрали вкладку [Arguments]
- в [3]: додано параметр -javaagent, як описано в розділі 3.1.9.
Після цього можна отримати список осіб:

3.5. Інші приклади
Ми хотіли б показати приклад веб-сторінки, де контейнер Spring замінено на контейнер Jboss Ejb3, розглянутий у розділі 3.2:
![]() |
- на [1]: проект Eclipse
- у [3]: його розташування у папці з прикладами. Ми імпортуємо його.
Ми взяли конфігурацію [jboss-config.xml, persistence.xml], описану в розділі 3.2, а потім змінили метод [init] контролера [Application.java] наступним чином:
// init
@SuppressWarnings("unchecked")
public void init() throws ServletException {
try {
// отримуються параметри ініціалізації сервлета
ServletConfig config = getServletConfig();
// обробка інших параметрів ініціалізації
String valeur = null;
for (int i = 0; i < paramètres.length; i++) {
// значення параметра
valeur = config.getInitParameter(paramètres[i]);
// параметр присутній?
if (valeur == null) {
// фіксується помилка
erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
} else {
// зберігаємо значення параметра
params.put(paramètres[i], valeur);
}
}
// URL-адреса подання [erreurs] обробляється окремо
urlErreurs = config.getInitParameter("urlErreurs");
if (urlErreurs == null)
throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
// конфігурація додатка
// запускається контейнер EJB3 JBoss
// використовуються файли конфігурації ejb3-interceptors-aop.xml та embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Створення власних для додатка бінів
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// розгортаються всі EJB, знайдені в classpath додатка
//EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
EJB3StandaloneBootstrap.scanClasspath();
// Ініціалізується контекст JNDI. Використовується файл jndi.properties
InitialContext initialContext = new InitialContext();
// створення екземпляра сервісного шару
service = (IService) initialContext.lookup("Service/local");
// очищення бази даних
clean();
// заповнюємо її
fill();
} catch (Exception e) {
throw new ServletException(e);
}
}
- рядки 28–38: запускається контейнер Ejb3. Він замінює контейнер Spring.
- рядок 41: запитуємо посилання на рівень [service] додатка.
На перший погляд, це єдині зміни, які потрібно внести. Під час виконання з’являється така помилка:
![]() |
Мені не вдалося зрозуміти, у чому саме полягала проблема. Виняток, про який повідомляє Tomcat, здається, вказує на те, що об’єкт із назвою «TransactionManager» було запитано у службі JNDI, а та його не розпізнала. Я залишаю читачам можливість самостійно знайти рішення цієї проблеми. Якщо рішення буде знайдено, його буде додано до цього документа.





























































