Skip to content

4. Веб-сервіс J2EE для зустрічей

Повернемося до архітектури додатка, який ми будемо створювати:

У цьому розділі ми розглянемо створення веб-сервісу J2EE [1], що виконується на сервері Sun / Glassfish.

4.1. База даних

База даних, яку ми назвемо [dbrdvmedecins] , є базою даних MySQL5 із чотирма таблицями:

Image

4.1.1. Таблиця [MEDECINS]

Вона містить інформацію про лікарів, які обслуговуються додатком [RdvMedecins].

  • ID: номер, що ідентифікує лікаря — первинний ключ таблиці
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 щоразу, коли до рядка вносяться зміни.
  • NOM: прізвище лікаря
  • PRENOM: його ім’я
  • TITRE: його/її титул (пані, пані, пан)

4.1.2. Таблиця [CLIENTS]

Пацієнти різних лікарів заносяться до таблиці [CLIENTS]:

  • ID: номер, що ідентифікує пацієнта — первинний ключ таблиці
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 кожного разу, коли до рядка вносяться зміни.
  • NOM: ім’я клієнта
  • PRENOM: його ім’я
  • TITRE: титул (пані, пані, пан)

4.1.3. Таблиця [CRENEAUX]

У ній перелічено часові проміжки, у яких можливі RV:

  • ID: номер, що ідентифікує часовий проміжок — первинний ключ таблиці (рядок 8)
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 щоразу, коли до рядка вносяться зміни.
  • ID_MEDECIN: номер, що ідентифікує лікаря, якому належить цей часовий проміжок — зовнішній ключ у стовпці MEDECINS (ID).
  • HDEBUT: час початку часового проміжку
  • MDEBUT: хвилини початку часового проміжку
  • HFIN: година закінчення часового проміжку
  • MFIN: хвилини закінчення інтервалу

Другий рядок таблиці [CRENEAUX] (див. [1] вище) вказує, наприклад, що слот № 2 починається о 8:20 і закінчується о 8:40 та належить лікарю № 1 (пані Марі PELISSIER).

4.1.4. Таблиця [RV]

У ній наведено перелік RV, призначених кожному лікарю:

  • ID: номер, що однозначно ідентифікує RV — первинний ключ
  • JOUR: день RV
  • ID_CRENEAU: часовий проміжок для RV — зовнішній ключ до поля [ID] у таблиці [CRENEAUX] — визначає як часовий проміжок, так і відповідного лікаря.
  • ID_CLIENT: номер клієнта, для якого зроблено бронювання — зовнішній ключ до поля [ID] таблиці [CLIENTS]

Ця таблиця має обмеження унікальності ( ) для значень з’єднаних стовпців (JOUR, ID_CRENEAU):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Якщо рядок таблиці [RV] має значення (JOUR1, ID_CRENEAU1) для стовпців (JOUR, ID_CRENEAU), це значення не може зустрічатися більше ніде. Інакше це означало б, що одночасно було зареєстровано два записи RV для одного й того самого лікаря. З точки зору програмування на Java драйвер JDBC бази даних запускає SQLException, коли трапляється такий випадок.

Рядок id, що дорівнює 3 (див. [1] вище), означає, що 23.08.2006 було заброньовано RV для слоту № 20 та клієнта № 4. З таблиці [CRENEAUX] ми дізнаємося, що слот № 20 відповідає часовому проміжку 16:20–16:40 і належить лікарю № 1 (пані Марі PELISSIER). З таблиці [CLIENTS] випливає, що клієнт № 4 — це пані Бріжит BISTROU.

4.2. Створення бази даних

Створіть базу даних MySql [dbrdvmedecins] за допомогою інструменту на ваш вибір. Для створення таблиць та їх заповнення можна використати скрипт [createbd.sql], який вам буде надано. Його вміст такий:

create table CLIENTS (
        ID bigint not null auto_increment,
        VERSION integer not null,
        TITRE varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(30) not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table CRENEAUX (
        ID bigint not null auto_increment,
        VERSION integer not null,
        HDEBUT integer not null,
        MDEBUT integer not null,
        HFIN integer not null,
        MFIN integer not null,
        ID_MEDECIN bigint not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table MEDECINS (
        ID bigint not null auto_increment,
        VERSION integer not null,
        TITRE varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(30) not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table RV (
        ID bigint not null auto_increment,
        JOUR date not null,
        ID_CLIENT bigint not null,
        ID_CRENEAU bigint not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    alter table CRENEAUX 
        add index FK9BD7A197FE16862 (ID_MEDECIN), 
        add constraint FK9BD7A197FE16862 
        foreign key (ID_MEDECIN) 
        references MEDECINS (ID);

    alter table RV 
        add index FKA4494D97AD2 (ID_CLIENT), 
        add constraint FKA4494D97AD2 
        foreign key (ID_CLIENT) 
        references CLIENTS (ID);

    alter table RV 
        add index FKA441A673246 (ID_CRENEAU), 
        add constraint FKA441A673246 
        foreign key (ID_CRENEAU) 
        references CRENEAUX (ID);

INSERT INTO CLIENTS ( VERSION, NOM, PRENOM, TITRE) VALUES (1, 'MARTIN', 'Jules', 'Mr');
...

INSERT INTO MEDECINS ( VERSION, NOM, PRENOM, TITRE) VALUES (1, 'PELISSIER', 'Marie', 'Mme');
...

INSERT INTO CRENEAUX ( VERSION, ID_MEDECIN, HDEBUT, MDEBUT, HFIN, MFIN) VALUES (1, 1, 8, 0, 8, 20);
...

INSERT INTO RV ( JOUR, ID_CRENEAU, ID_CLIENT) VALUES ('2006-08-22', 1, 2);
...

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

COMMIT WORK;

4.3. Елементи архітектури на стороні сервера

Повернемося до архітектури додатка, який потрібно створити:

На стороні сервера додаток складатиметься з:

  1. з JPA-шару, що дозволяє працювати з BD за допомогою об’єктів
  1. EJB, що відповідає за управління операціями з шаром JPA
  2. веб-сервісу, що відповідає за надання віддаленим клієнтам інтерфейсу EJB у вигляді веб-сервісу.

Елементи (b) та (c) реалізують шар [dao], зображений на попередній схемі. Відомо, що додаток може отримати доступ до віддаленого EJB через протоколи RMI та JNDI. На практиці це обмежує коло клієнтів лише клієнтами на Java. Веб-сервіс використовує стандартизований протокол зв’язку, який реалізують різні мови програмування: .NET, PHP, C++ тощо. Саме це ми й хочемо продемонструвати тут, використовуючи клієнт .NET.

Короткий вступ до веб-сервісів можна знайти в курсі [ref1], параграф 14, сторінка 109.

Веб-сервіс можна реалізувати двома способами:

  • за допомогою класу з анотацією @WebService, який виконується у веб-контейнері
  • за допомогою EJB, анотованого @WebService, що виконується у контейнері EJB

Тут ми використаємо перший варіант:

У курсі [ref1], у параграфі 14 на сторінці 109, наведено приклад із використанням другого рішення.

4.4. Налаштування Hibernate для сервера Glassfish у

Залежно від версії, сервер Glassfish V2, що постачається разом із NetBeans, може не містити бібліотек Hibernate, необхідних для роботи шару JPA/Hibernate. Якщо під час проходження цього посібника ви виявите, що Glassfish не пропонує реалізацію JPA/Hibernate або під час розгортання служб з’являється виняток про те, що бібліотеки Hibernate не знайдено, вам слід додати ці бібліотеки до папки [<glassfish>/domains/domain1/lib/ext], а потім перезапустити сервер Glassfish:

  • у [1], папку <glassfish>/.../lib/ext
  • у [2] — бібліотеки Hibernate та кілька драйверів JDBC
  • на [3], драйвер JDBC з MySQL

Бібліотеки Hibernate містяться в архіві, що додається до цього посібника.

4.5. Інструменти автоматичного генерування NetBeans

Повернемося до архітектури, яку нам потрібно побудувати:

За допомогою NetBeans можна автоматично згенерувати шар [JPA] та шар [Ejb], який контролює доступ до згенерованих сутностей JPA. Цікаво ознайомитися з цими методами автоматичного генерування, оскільки згенерований код дає цінні підказки щодо того, як писати сутності JPA або EJB-код, який їх використовує.

Тепер ми опишемо деякі з цих інструментів автоматичного генерування. Щоб зрозуміти згенерований код, потрібно мати хороші знання про сутності JPA, [ref1], а також EJB і [ref2].

Створення з’єднання NetBeans із базою даних

  • запустіть SGBD та MySQL 5, щоб BD став доступним
  • Створити з’єднання NetBeans із базою даних [dbrdvmedecins]
  • на вкладці [Files], у гілці [Databases] [1], вибрати драйвер JDBC MySQL [2]
  • потім виберіть опцію [3] «Connect Using», що дозволяє створити з’єднання з базою даних MySQL
  • у [4] введіть необхідні дані
  • потім підтвердіть у [5]
  • у [6] з’єднання створено. Тут відображаються чотири таблиці підключеної бази даних.

Створення проекту EJB

  • в [1], створіть нову програму, модуль EJB
  • у [2], виберіть категорію [Java EE], а в [3] — тип [EJB Module]
  • у [4] виберіть папку для проекту, а в [5] надайте йому ім’я — потім завершіть роботу майстра
  • у [6] — згенерований проєкт

Додавання ресурсу JDBC на сервер Glassfish

Ми додамо ресурс JDBC на сервер Glassfish.

  • на вкладці [Services] запустіть сервер Glassfish [2, 3]
  • у вкладці [Projects] клацніть правою кнопкою миші на проекті Ejb і в [5] виберіть опцію [New / Other], що дозволяє додати елемент до проекту.

Image

  • у [6], виберіть категорію [Glassfish], а в [7] вкажіть, що ви хочете створити ресурс JDBC, вибравши тип [JDBC Resource]
  • у полі [8] вкажіть, що цей ресурс JDBC використовуватиме власний пул з’єднань
  • у [9], надайте ім’я ресурсу JDBC
  • у [10] перейти до наступного кроку
  • в [11] визначаються характеристики пулу з'єднань ресурсу JDBC
  • у [12], надайте ім’я пулу з’єднань
  • у [13], виберіть раніше створене з'єднання NetBeans [dbrdvmedecins]
  • у [14] перейдіть до наступного кроку
  • у [15], зазвичай на цій сторінці нічого змінювати не потрібно. Властивості з’єднання з базою даних MySQL [dbrdvmedecins] були скопійовані з властивостей раніше створеного з’єднання NetBeans [dbrdvmedecins]
  • у [16], перейдіть до наступного кроку
  • у [17], залиште запропоновані значення за замовчуванням
  • у [18], завершіть роботу майстра. Він створить файл [sun-resources.xml] [19], вміст якого є таким:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
  <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins" object-type="user" pool-name="dbrdvmedecinsPool">
    <description/>
  </jdbc-resource>
  <jdbc-connection-pool ...">
    <property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins"/>
    <property name="User" value="root"/>
    <property name="Password" value="()"/>
  </jdbc-connection-pool>
</resources>

Вищезазначений файл містить всю інформацію, введену в майстрі, у форматі XML. Він буде використаний NetBeans для запиту до сервера GlassFish щодо створення ресурсу «jdbc/dbrdvmedecins», визначеного у рядку 4.

Створення модуля збереження даних

Модуль збереження даних [persistence.xml] налаштовує рівень JPA: він вказує використовувану реалізацію JPA (Toplink, Hibernate тощо) та налаштовує її.

  • у [1] клацніть правою кнопкою миші на проекті Ejb і виберіть [New / Other] у [2]
  • в [3], виберіть категорію [Persistence], а потім в [4] вкажіть, що ви хочете створити одиницю збереження JPA
  • у [5] надайте ім’я створеному модулю збереження
  • в [6], виберіть [Hibernate] як реалізацію JPA
  • у [7], вибрати щойно створений ресурс Glassfish «jdbc/dbrdvmedecins»
  • у [8] вкажіть, що під час інстанціювання шару JPA з базою даних не слід виконувати жодних дій
  • завершіть роботу майстра
  • у [9] — файл [persistence.xml], створений майстром

Його вміст такий:

1
2
3
4
5
6
7
8
9
<?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="serveur-ejb-dao-jpa-hibernate-generePU" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties/>
  </persistence-unit>
</persistence>

Знову ж таки, у форматі XML він повторює інформацію, вказану в майстрі. Цього файлу недостатньо для роботи з базою даних MySQL5 «dbrdvmedecins». Нам потрібно вказати Hibernate, який тип SGBD слід обробляти. Це буде зроблено пізніше.

Створення сутностей JPA

 
  • у [1] клацніть правою кнопкою миші на проекті, а в [2] виберіть опцію [New / Other]
  • у [3] виберіть категорію [Persistence], а потім у [4] вкажіть, що ви хочете створити об’єкти JPA на основі існуючої бази даних.
  • у [5] виберіть джерело JDBC «jdbc/dbrdvmedecins», яке ми створили
  • у [6] чотири таблиці пов’язаної бази даних
  • у [7,8] включіть їх усі у процес генерації сутностей JPA
  • у [9], продовжити роботу з майстром
  • у [10], сутності JPA, які будуть згенеровані
  • в [11], надати ім’я пакету для об’єктів JPA
  • в [12], вибрати тип Java, який буде інкапсулювати списки об’єктів, що повертаються шаром JPA
  • завершіть роботу майстра
  • у [13] — чотири згенеровані сутності JPA, по одній для кожної таблиці бази даних.

Ось, наприклад, код сутності [Rv], яка представляє рядок таблиці [rv] у базі [dbrdvmedecins].

package jpa;
...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  @Basic(optional = false)
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneaux idCreneau;
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Clients idClient;

  public Rv() {
  }

...
}

Створення EJB-шару для доступу до сутностей JPA

  • у [1] клацніть правою кнопкою миші на проекті, а в [2] виберіть опцію [New / Other]
  • у [3], вибрати категорію [Persistence], а потім у [4] — тип [Session Beans for Entity Classes]
  • в [5] раніше створені об’єкти JPA відображаються
  • у [6], виділіть їх усі
  • у [7] — вони були виділені
  • у [8], продовжити роботу з майстром
  • у [9], надайте ім’я пакету EJB, які будуть згенеровані
  • в [10], вказати, що EJB мають реалізовувати як локальний, так і віддалений інтерфейс
  • завершити роботу майстра
  • у [11] — згенеровані EJB

Ось, наприклад, код EJB, який керує доступом до сутності [Rv], а отже, до таблиці [rv] у базі даних [dbrdvmedecins]:

package ejb;
...
@Stateless
public class RvFacade implements RvFacadeLocal, RvFacadeRemote {
  @PersistenceContext
  private EntityManager em;

  public void create(Rv rv) {
    em.persist(rv);
  }

  public void edit(Rv rv) {
    em.merge(rv);
  }

  public void remove(Rv rv) {
    em.remove(em.merge(rv));
  }

  public Rv find(Object id) {
    return em.find(Rv.class, id);
  }

  public List<Rv> findAll() {
    return em.createQuery("select object(o) from Rv as o").getResultList();
  }

}

Як уже зазначалося, автоматичне генерування коду може бути дуже корисним для запуску проєкту та ознайомлення з сутностями JPA та EJB. Далі ми перепишемо шари JPA та EJB, використовуючи власний код, але читач знайде в них інформацію, яку ми щойно розглянули під час автоматичного генерування шарів.

4.6. Проєкт NetBeans для модуля EJB

Ми створюємо новий порожній модуль EJB (див. розділ 4.5):

 
  • пакет [rdvmedecins.entites] об’єднує сутності шару JPA
  • пакет [rdvmedecins.dao] реалізує EJB шару [dao]
  • пакет [rdvmedecins.exceptions] реалізує клас винятків, специфічний для даного додатка

Далі ми припускаємо, що читач виконав усі кроки, описані в розділі 4.5. Деякі з них йому доведеться повторити.

4.6.1. Налаштування шару JPA

Нагадаємо архітектуру нашого клієнт-серверного додатка:

Проєкт NetBeans:

 

Рівень [JPA] налаштовується за допомогою файлів [persistence.xml] та [sun-resources.xml], наведених вище. Ці два файли генеруються майстрами, з якими ми вже знайомилися:

  • створення файлу [sun-resources.xml] було описано в розділі 4.5.
  • створення файлу [persistence.xml] описано в розділі 4.5.

Створений файл [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="dbrdvmedecins" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <properties>
       <!-- Діалект -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>
  • рядок 3: тип транзакцій — JTA: транзакції будуть оброблятися контейнером Ejb3 сервера Glassfish
  • рядок 4: використовується реалізація JPA / Hibernate. Для цього бібліотека Hibernate була додана до сервера Glassfish (див. параграф 4.4).
  • рядок 5: джерело даних JTA, яке використовується рівнем JPA, має ім’я JNDI «jdbc/dbrdvmedecins».
  • рядок 8: цей рядок не генерується автоматично. Його потрібно додати вручну. Він вказує Hibernate, що використовується SGBD — це MySQL5.

Джерело даних «jdbc/dbrdvmedecins» налаштовується у такому файлі [sun-resources.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
  <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins" object-type="user" pool-name="dbrdvmedecinsPool">
    <description/>
  </jdbc-resource>
  <jdbc-connection-pool ...>
    <property name="URL" value="jdbc:mysql://localhost/dbrdvmedecins"/>
    <property name="User" value="root"/>
    <property name="Password" value="()"/>
  </jdbc-connection-pool>
</resources>
  • рядки 8–10: параметри JDBC джерела даних (URL бази даних, ім’я користувача та пароль). База даних MySQL dbrdvmedecins — це та, що описана в розділі 4.1.
  • рядок 7: параметри пулу з’єднань, пов’язаного з цим джерелом даних

4.6.2. Елементи шару JPA

Нагадаємо архітектуру нашого клієнт-серверного додатка:

Проєкт NetBeans:

Пакет [rdvmedecins.entites] реалізує рівень [Jpa].

У розділі 4.5 ми розглянули, як автоматично генерувати сутності JPA для додатка. Тут ми не будемо використовувати цю техніку, а визначимо сутності самостійно. Однак вони будуть містити значну частину коду, згенерованого в розділі 4.5. У цьому випадку ми хочемо, щоб сутності [Medecin] та [Client] були дочірніми класами класу [Personne].

Клас Personne використовується для представлення лікарів та клієнтів:

package rdvmedecins.entites;
...
@MappedSuperclass
public class Personne implements Serializable {
   // характеристики особи

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Version
  @Column(name = "VERSION", nullable = false)
  private Integer version;

  @Column(name = "TITRE", length = 5, nullable = false)
  private String titre;
  @Column(name = "NOM", length = 30, nullable = false)
  private String nom;
  @Column(name = "PRENOM", length = 30, nullable = false)
  private String prenom;

   // конструктор за замовчуванням
  public Personne() {
  }

   // виробник із параметрами
  public Personne(String titre, String nom, String prenom) {
     // використовуються сетери
...
  }

   // конструктор з копіюванням
  public Personne(Personne personne) {
     // використання сеттерів
 ...
  }

   // toString
  @Override
  public String toString() {
    return "[" + titre + "," + prenom + "," + nom + "]";
  }

// геттери та сеттери
....
}
  • рядок 3: зверніть увагу, що клас [Personne] сам по собі не є сутністю (@Entity). Він буде батьківським класом для сутностей. Анотація @MappedSuperClass позначає цю ситуацію.

Ентітет [Client] інкапсулює рядки таблиці [clients]. Він походить від попереднього класу [Personne]:

package rdvmedecins.entites;
....
@Entity
@Table(name = "CLIENTS")
public class Client extends Personne implements Serializable {

   // конструктор за замовчуванням
  public Client() {
  }

   // конструктор з параметрами
  public Client(String titre, String nom, String prenom) {
     // батьківський об’єкт
    super(titre, nom, prenom);
  }

   // конструктор з копіюванням
  public Client(Client client) {
     // батьківський об’єкт
    super(client);
  }
}
  • рядок 3: клас [Client] є сутністю JPA
  • рядок 4: вона пов’язана з таблицею [clients]
  • рядок 5: вона походить від класу [Personne]

Ентітет [Medecin], який інкапсулює рядки таблиці [medecins], побудований за тим самим зразком:

package rdvmedecins.entites;
...
@Entity
@Table(name = "MEDECINS")
public class Medecin extends Personne implements Serializable {

   // конструктор за замовчуванням
  public Medecin() {
  }

   // конструктор з параметрами
  public Medecin(String titre, String nom, String prenom) {
     // батьківський
    super(titre, nom, prenom);
  }

   // конструктор з копіюванням
  public Medecin(Medecin medecin) {
     // батьківський
    super(medecin);
  }
}

Об’єкт [Creneau] інкапсулює рядки таблиці [creneaux]:

package rdvmedecins.entites;
....
@Entity
@Table(name = "CRENEAUX")
public class Creneau implements Serializable {

   // характеристики інтервалу RV
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Version
  @Column(name = "VERSION", nullable = false)
  private Integer version;
  @ManyToOne
  @JoinColumn(name = "ID_MEDECIN", nullable = false)
  private Medecin medecin;
  @Column(name = "HDEBUT", nullable = false)
  private Integer hdebut;
  @Column(name = "MDEBUT", nullable = false)
  private Integer mdebut;
  @Column(name = "HFIN", nullable = false)
  private Integer hfin;
  @Column(name = "MFIN", nullable = false)
  private Integer mfin;

   // виробник за замовчуванням
  public Creneau() {

  }

   // конструктор з параметрами
  public Creneau(Medecin medecin, Integer hDebut,Integer mDebut, Integer hFin, Integer mFin) {
     // використовуються сетери
...
  }

   // конструктор з копіюванням
  public Creneau(Creneau creneau) {
     // використовуються сетери
...
  }

   // toString
  @Override
  public String toString() {
    return "[" + getId() + "," + getVersion() + "," + getMedecin() + "," + getHdebut() + ":" + getMdebut() + "," + getHfin() + ":" + getMfin() + "]";
  }

   // сетери — геттери
...
}
  • рядки 15–17 моделюють відношення «один до багатьох», яке існує між таблицею [creneaux] та таблицею [medecins] у базі даних.

Суть [Rv] інкапсулює рядки таблиці [rv]:

package rdvmedecins.entites;
...
@Entity
@Table(name = "RV")
public class Rv implements Serializable {
   // характеристики

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Column(name = "JOUR", nullable = false)
  @Temporal(TemporalType.DATE)
  private Date jour;
  @ManyToOne
  @JoinColumn(name = "ID_CLIENT", nullable = false)
  private Client client;
  @ManyToOne
  @JoinColumn(name = "ID_CRENEAU", nullable = false)
  private Creneau creneau;

   // конструктор за замовчуванням
  public Rv() {
  }

   // конструктор з параметрами
  public Rv(Date jour, Client client, Creneau creneau) {
     // використовуємо сетери
...
  }

   // конструктор з копіюванням
  public Rv(Rv rv) {
     // через сетери
...
  }

   // toString
  @Override
  public String toString() {
    return "[" + getId() + "," + new SimpleDateFormat("dd/MM/yyyy").format(getJour()) + "," + getClient() + "," + getCreneau() + "]";
  }

// геттери та сеттери
...
}
  • рядки 15–17 моделюють відношення «один до багатьох», що існує між таблицею [rv] та таблицею [clients] у базі даних, а рядки 18–20 — відношення «один до багатьох», що існує між таблицею [rv] та таблицею [creneaux]

4.6.3. Клас винятків

Клас винятків [RdvMedecinsException] додатка має такий вигляд:

package rdvmedecins.exceptions;

import javax.ejb.ApplicationException;

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

  private static final long serialVersionUID = 1L;

   // приватні поля
  private int code = 0;

   // конструктори
  public RdvMedecinsException() {
    super();
  }

  public RdvMedecinsException(String message) {
    super(message);
  }

  public RdvMedecinsException(String message, Throwable cause) {
    super(message, cause);
  }

  public RdvMedecinsException(Throwable cause) {
    super(cause);
  }

  public RdvMedecinsException(String message, int code) {
    super(message);
    setCode(code);
  }

  public RdvMedecinsException(Throwable cause, int code) {
    super(cause);
    setCode(code);
  }

  public RdvMedecinsException(String message, Throwable cause, int code) {
    super(message, cause);
    setCode(code);
  }

   // геттери — сеттери
...
}
  • рядок 6: клас походить від класу [RuntimeException]. Тому компілятор не вимагає обробляти його за допомогою блоків try / catch.
  • рядок 5: анотація @ApplicationException гарантує, що виняток не буде «поглинутий» винятком типу [EjbException].

Щоб зрозуміти анотацію @ApplicationException, повернемося до архітектури, що використовується на стороні сервера:

Виняток типу [RdvMedecinsException] буде згенеровано методами EJB шару [dao] всередині контейнера EJB3 і перехоплено ним. Без анотації @ApplicationException контейнер EJB3 інкапсулює виникле виключення у виключення типу [EjbException] і перекидає його. Можливо, ви не бажаєте такого інкапсулювання і хочете, щоб з контейнера Ejb3 виходило виключення типу [RdvMedecinsException]. Саме це дозволяє зробити анотація @ApplicationException. Крім того, атрибут (rollback=true) цієї анотації вказує контейнеру EJB3, що якщо виняток типу [RdvMedecinsException] виникає всередині методу, що виконується в рамках транзакції з SGBD, то цю транзакцію слід скасувати. З технічної точки зору це називається «rollback» транзакції.

4.6.4. EJB шару [dao]

Java-інтерфейс [IDao] шару [dao] має такий вигляд:

package rdvmedecins.dao;
...
public interface IDao {

   // список клієнтів
  public List<Client> getAllClients();
   // список лікарів
  public List<Medecin> getAllMedecins();
   // список часових слотів лікаря
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // список прийомів лікаря на певний день
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // знайти клієнта за його ідентифікаційним номером
  public Client getClientById(Long id);
   // пошук клієнта за його ідентифікатором
  public Medecin getMedecinById(Long id);
   // знайти прийом за його ідентифікатором
  public Rv getRvById(Long id);
   // знайти часовий проміжок за його ідентифікатором
  public Creneau getCreneauById(Long id);
   // додати RV
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // видалити RV
  public void supprimerRv(Rv rv);
}

Локальний інтерфейс [IDaoLocal] EJB просто успадковує попередній інтерфейс [IDao]:

1
2
3
4
5
6
7
package rdvmedecins.dao;

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{
}

Те саме стосується віддаленого інтерфейсу [IDaoRemote]:

1
2
3
4
5
6
7
package rdvmedecins.dao;

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao {
}

EJB [DaoJpa] реалізує обидва інтерфейси — локальний і віддалений:

1
2
3
4
5
6
7
package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {
...
}
  • у рядку 3 вказано, що віддалений EJB має назву «rdvmedecins.dao»
  • у рядку 4 вказано, що всі методи EJB виконуються в рамках транзакції, яку керує контейнер EJB3.
  • у рядку 5 показано, що EJB реалізує локальний та віддалений інтерфейси.

Повний код EJB виглядає так:

package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {

  @PersistenceContext
  private EntityManager em;

   // перелік клієнтів
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select c from Client c").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }

   // перелік лікарів
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select m from Medecin m").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }

   // перелік часових проміжків для певного лікаря
   // лікар: лікар
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    try {
      return em.createQuery("select c from Creneau c join c.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }

   // список прийомів певного лікаря в певний день
   // medecin: лікар
   // день: день
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    try {
      return em.createQuery("select rv from Rv rv join rv.creneau c join c.medecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", new SimpleDateFormat("yyyy:MM:dd").parse(jour)).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
  }

   // додавання прийому
   // день: день прийому
   // інтервал: часовий інтервал прийому
   // клієнт: клієнт, для якого призначено зустріч
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(new SimpleDateFormat("yyyy:MM:dd").parse(jour), client, creneau);
      em.persist(rv);
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

   // видалення зустрічі
   // зустріч: скасована зустріч
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

   // отримати інформацію про певного клієнта
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 7);
    }
  }

   // отримати даних лікаря
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 8);
    }
  }

   // отримати певний запис на прийом
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 9);
    }
  }

   // отримати певний час прийому
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 10);
    }
  }
}
  • рядок 8: об’єкт EntityManager, який керує доступом до контексту персистентності. Під час інстанціювання класу це поле буде ініціалізовано контейнером EJB завдяки анотації @PersistenceContext у рядку 7.
  • рядок 15: запит JPQL, який повертає всі рядки таблиці [clients] у вигляді списку об’єктів [Client].
  • рядок 22: аналогічний запит для лікарів
  • рядок 32: запит JPQL, що виконує з'єднання таблиць [creneaux] та [medecins]. Він параметризується за ідентифікатором лікаря.
  • рядок 43: запит JPQL, що виконує з'єднання між таблицями [rv], [creneaux] та [medecins] і має два параметри: ідентифікатор лікаря та день прийому.
  • рядки 55–57: створення запису про прийом та його збереження в базі даних.
  • рядок 67: видалення запису про прийом із бази даних.
  • рядок 76: виконує запит SELECT до бази даних для пошуку певного клієнта
  • рядок 85: те саме для лікаря
  • рядок 94: те саме для запису на прийом
  • рядок 103: те саме для часового проміжку
  • усі операції з контекстом збереження даних (em) у рядку 9 можуть спричинити проблему з базою даних. Тому всі вони обгорнуті блоком try/catch. Можливе виключення інкапсульовано у «внутрішнє» виключення RdvMedecinsException.

Після компіляції модуль EJB створює файл .jar з іменем :

4.7. Розгортання EJB шару [dao] за допомогою NetBeans

NetBeans дозволяє легко розгорнути раніше створений EJB на сервері GlassFish.

  • У властивостях проекту EJB перевірте параметри виконання [1].
  • У [2] вкажіть ім’я сервера, на якому буде розгорнуто EJB
  • на вкладці [Services] [3] запускаємо його [4].
  • у [5] — сервер Glassfish після запуску. На ньому ще немає модуля EJB.
  • Запустіть сервер MySQL і переконайтеся, що база даних [dbrdvmedecins] працює. Для цього можна скористатися підключенням NetBeans, створеним у розділі 4.5.
  • На вкладці [Projects] [6] розгортаємо модуль EJB [7]: необхідно, щоб SGBD MySQL5 був запущений, щоб ресурс JDBC «jdbc/dbrdvmedecins», який використовується EJB, був доступний.
  • У [8] розгорнутий EJB відображається в дереві сервера GlassFish
  • У [9] розгорнутий EJB видаляється
  • у [10] EJB більше не відображається в дереві сервера GlassFish.

4.8. Розгортання EJB з рівня [dao] за допомогою GlassFish

Тут ми покажемо, як розгорнути EJB на сервері GlassFish із його архіву .jar.

  • Запустіть сервер MySQL і переконайтеся, що база даних [dbrdvmedecins] працює. Для цього можна скористатися підключенням NetBeans, створеним у розділі 4.5.

Нагадаємо конфігурацію JPA модуля EJB, який буде розгорнуто. Ця конфігурація налаштовується у файлі [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="dbrdvmedecins" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <properties>
       <!-- Діалект -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>

Рядок 5 вказує, що шар Jpa використовує джерело даних JTA, c.a.d, яке управляється контейнером Ejb3 і має назву «jdbc/dbrdvmedecins».

У розділі 4.5 ми розглянули, як створити цей ресурс JDBC за допомогою NetBeans. Тут ми покажемо, як це зробити безпосередньо в GlassFish. Ми дотримуємося процедури, описаної в розділі 13.1.2, на сторінці 79 [ref1].

Спочатку видалимо ресурс, щоб мати змогу створити його заново. Зробимо це за допомогою NetBeans:

  • у [1], ресурси JDBC сервера Glassfish
  • на [2], ресурс «jdbc/dbrdvmedecins» нашого EJB
  • у [3], пул з’єднань цього ресурсу JDBC
  • в [4] видаляється пул з'єднань. Це призведе до видалення всіх ресурсів JDBC, які його використовують, а отже, і ресурсу «jdbc/dbrdvmedecins».
  • У [5] та [6] ресурс JDBC та пул з’єднань були знищені.

Тепер ми використовуємо консоль адміністрування сервера Glassfish для створення ресурсу JDBC та розгортання EJB.

  • на вкладці [services] [1] у NetBeans, запустіть сервер GlassFish [2], а потім перейдіть [3] до його консолі адміністрування
  • за адресою [4], увійдіть як адміністратор (пароль: adminadmin, якщо ви не змінювали його під час встановлення або пізніше).
  • в [5] виберіть гілку [Connection Pools] ресурсів Glassfish
  • у [6] створіть новий пул з'єднань. Нагадуємо, що пул з'єднань — це технічний засіб для обмеження кількості відкриттів/закриттів з'єднань із SGBD. Під час запуску сервера N (число, визначене в конфігурації) з’єднань відкриваються з SGBD. Ці відкриті з’єднання потім надаються EJB, які їх запитують для виконання операції з SGBD. Як тільки операція завершується, EJB повертає з’єднання до пулу. З’єднання ніколи не закривається. Воно спільно використовується різними потоками, які звертаються до SGBD
  • у [7], надайте ім’я пулу
  • в [8], клас, що моделює джерело даних, — це клас [javax.sql.DataSource]
  • в [9], SGBD, що містить джерело даних, тут — MySQl.
  • у [10] перейдіть до наступного кроку
  • у [11] атрибут «Connection Validation Required» забезпечує те, що перед наданням з’єднання пул перевіряє, чи воно працює. Якщо це не так, він створює нове. Це дозволяє додатку продовжувати працювати після тимчасового розриву з’єднання з SGBD. Під час перерви жодне з'єднання не є доступним, і клієнту надсилаються винятки. Коли збій закінчується, клієнти, які продовжують запитувати з’єднання, знову їх отримують: завдяки атрибуту «Connection Validation Required» усі з’єднання пулу будуть відтворені. Без цього атрибуту пул констатував би, що початкові з’єднання було розірвано, але не намагався б створити нові.
  • У [12] задається рівень ізоляції «Read Committed» для транзакцій. Цей рівень гарантує, що транзакція T2 не зможе зчитувати дані, змінені транзакцією T1, доки остання не буде повністю завершена.
  • У транзакції [13] вимагається, щоб усі транзакції використовували рівень ізоляції, зазначений у транзакції [12]
  • у [14] та [15] вкажіть URL-адресу BD, з’єднання з яким обслуговує пул
  • у [16], користувачем буде root
  • у [17] додайте властивість
  • у [18] додайте властивість «Password» із значенням () у [19]. Хоча на скріншоті [19] цього не показано, для позначення порожнього пароля не слід вказувати порожній рядок, а саме () (відкриваюча дужка, закриваюча дужка). Якщо користувач root у вашому SGBD MySQL має непорожній пароль, вкажіть цей пароль.
  • у [20], завершіть роботу майстра створення пулу з’єднань для бази даних MySQL [dbrdvmedecins].
  • у [21] пул створено. Клацніть на його посилання.
  • у [22] кнопка [Ping] дозволяє створити з’єднання з базою даних [dbrdvmedecins]
  • у [23], якщо все гаразд, з’явиться повідомлення про те, що з’єднання встановлено

Після створення пулу з’єднань можна створити ресурс JDBC:

  • у [1], вибираємо гілку [JDBC Resources] у дереві об’єктів сервера
  • у [2] створюємо новий ресурс JDBC
  • у [3] надаємо ім’я ресурсу JDBC. Воно має відповідати імені, використаному у файлі [persistence.xml]:
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
  • у [4] вказується пул з'єднань, який має використовувати новий ресурс JDBC: той, що щойно було створено
  • у файлі [5] завершуємо роботу майстра створення
  • у [6] новий ресурс JDBC

Тепер, коли ресурс JDBC створено, можна розгорнути архів jar EJB:

  • у [1], виберіть гілку [Enterprise Applications]
  • у [2], за допомогою кнопки [Deploy] вкажіть, що ви хочете розгорнути нову програму
  • у [3] вкажіть, що додаток є модулем EJB
  • у [4] виберіть JAR-файл EJB [serveur-ejb-dao-jpa-hibernate.jar], який вам надали для TP.
  • у [5] ви можете змінити назву модуля EJB, якщо бажаєте
  • у [6] завершіть роботу майстра розгортання модуля EJB
  • У [7] було розгорнуто модуль EJB. Тепер ним можна користуватися.

4.9. Тестування EJB шару [dao]

Тепер, коли EJB шару [dao] нашого додатка розгорнуто, ми можемо його протестувати. Ми зробимо це за допомогою такого Java-клієнта:

Клас [MainTestsDaoRemote] [1] є тестовим класом JUnit 4. Бібліотеки в [2] складаються, з одного боку, з:

  • jar-файлу EJB шару [dao] [3] (див. розділ 4.6.4).
  • бібліотек GlassFish [4], необхідних для віддалених клієнтів EJB.

Тестовий клас має такий вигляд:

package dao;
...
public class MainTestsDaoRemote {

   // перевірений рівень [dao]
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
     // ініціалізація середовища JNDI
    InitialContext initialContext = new InitialContext();
     // інстанціювання шару DAO
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
     // дані тесту
    String jour = "2006:08:23";
     // відображення клієнтів
    List<Client> clients = null;
    try {
      clients = dao.getAllClients();
      display("Liste des clients :", clients);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // відображення лікарів
    List<Medecin> medecins = null;
    try {
      medecins = dao.getAllMedecins();
      display("Liste des médecins :", medecins);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // відображення вільних слотів лікаря
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = null;
    try {
      creneaux = dao.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // список прийомів лікаря на певний день
    try {
      display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // додати RV
    Rv rv = null;
    Creneau creneau = creneaux.get(2);
    Client client = clients.get(0);
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    try {
      rv = dao.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // додати RV у той самий час того самого дня
     // повинно викликати виняток
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    try {
      rv = dao.ajouterRv(jour, creneau, client);
      System.out.println("Rv ajouté");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // видалити RV
    System.out.println("Suppression du Rv ajouté");
    try {
      dao.supprimerRv(rv);
      System.out.println("Rv supprimé");
      display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
  }

   // допоміжна функція — відображає елементи колекції
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • рядок 13: зверніть увагу на створення екземпляра проксі віддаленого EJB. Використовується його ім’я JNDI «rdvmedecins.dao».
  • Тестові методи використовують методи, надані EJB (див. розділ 4.6.4).

Якщо все гаразд, тести мають пройти успішно:

 

Тепер, коли EJB шару [dao] працює, можна перейти до його публічного оприлюднення через веб-сервіс.

4.10. Веб-сервіс шару [dao]

Для короткого вступу до поняття веб-сервісу див. параграф 14, сторінка 111 у [ref1].

Повернемося до архітектури сервера нашого клієнт-серверного додатка:

Вище ми розглядаємо веб-сервіс рівня [dao]. Єдина роль цього сервісу полягає в тому, щоб зробити інтерфейс EJB рівня [dao] доступним для багатоплатформних клієнтів, здатних взаємодіяти з веб-сервісом.

Нагадаємо, що існує два способи реалізації веб-сервісу:

  • за допомогою класу з анотацією @WebService, який виконується у веб-контейнері
  • за допомогою EJB, анотованого @WebService, що виконується у контейнері EJB

Тут ми використовуємо перший варіант. У NetBeans IDE нам потрібно створити корпоративний проєкт із двома модулями:

  • модуль EJB, який виконуватиметься в контейнері EJB: EJB рівня [dao].
  • веб-модуль, який виконуватиметься у веб-контейнері: веб-сервіс, який ми зараз створюємо.

Ми будемо створювати цей корпоративний проєкт двома способами.

4.10.1. Проєкт NetBeans — версія 1

Спочатку створюємо проект NetBeans типу «Web Application»:

  • у [1] створюємо новий проєкт у категорії «Java Web» [2] типу «Веб-додаток» [3].
  • у [4] надаємо проекту ім’я, а у [5] вказуємо папку, в якій його слід згенерувати
  • у [6] вказується сервер додатків, який буде виконувати веб-додаток
  • у [7] задається контекст додатка
  • у [8] підтверджуємо конфігурацію проєкту.
  • у [9] — згенерований проект. Веб-сервіс, який ми створюємо, використовуватиме EJB з попереднього проєкту [10]. Тому йому потрібно вказати посилання на .jar-файл модуля EJB [10].
  • У [11] додаємо проект NetBeans до бібліотек веб-проекту [12]
  • у [13] вибираємо папку модуля EJB у файловій системі та підтверджуємо вибір.
  • у [14] модуль EJB було додано до бібліотек веб-проєкту.

У [15] ми реалізуємо веб-сервіс за допомогою наступного класу [WsDaoJpa]:

package rdvmedecins.ws;
...
@WebService()
public class WsDaoJpa implements IDao {

  @EJB
  private IDaoLocal dao;

   // список клієнтів
  @WebMethod
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

   // список лікарів
  @WebMethod
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

   // список часових проміжків для певного лікаря
   // medecin: лікар
  @WebMethod
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

   // список прийомів певного лікаря в певний день
   // medecin: лікар
   // день: день
  @WebMethod
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

   // додавання прийому
   // день: день прийому
   // інтервал: часовий інтервал прийому
   // клієнт: клієнт, для якого призначено зустріч
  @WebMethod
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

   // видалення зустрічі
   // зустріч: скасована зустріч
  @WebMethod
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

   // отримати дані про певного клієнта
  @WebMethod
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

   // отримати даних лікаря
  @WebMethod
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

   // отримати певний запис на прийом
  @WebMethod
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

   // отримати певний час прийому
  @WebMethod
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
}
  • у рядку 4 клас [WsdaoJpa] реалізує інтерфейс [IDao]. Нагадаємо, що цей інтерфейс визначено в архіві EJB шару [dao] у такому вигляді:
package rdvmedecins.dao;
...
public interface IDao {

   // список клієнтів
  public List<Client> getAllClients();
   // список лікарів
  public List<Medecin> getAllMedecins();
   // перелік часових проміжків лікаря
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // перелік прийомів лікаря на певний день
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // знайти клієнта за його ідентифікаційним номером
  public Client getClientById(Long id);
   // пошук клієнта за його ідентифікатором
  public Medecin getMedecinById(Long id);
   // знайти прийом за його ідентифікатором
  public Rv getRvById(Long id);
   // знайти часовий проміжок за його ідентифікатором
  public Creneau getCreneauById(Long id);
   // додати RV
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // видалити RV
  public void supprimerRv(Rv rv);
}
  • рядок 3: анотація @WebService перетворює клас [WsDaoJpa] на веб-сервіс.
  • рядки 6–7: посилання на EJB шару [dao] буде вставлено сервером додатків у поле рядка 7. Нагадаємо, що таким чином завжди вводиться локальна реалізація (у даному випадку — IDaoLocal). Це введення можливе, оскільки веб-сервіс виконується в тій самій JVM, що й EJB.
  • Усі методи веб-сервісу позначені анотацією @WebMethod, щоб зробити їх видимими для віддалених клієнтів. Метод, не позначений анотацією @WebMethod, був би внутрішнім для веб-сервісу і невидимим для віддалених клієнтів. Кожен метод M веб-сервісу просто викликає відповідний метод M EJB, введений у рядку 7.

Створення цього веб-сервісу відображається новою гілкою в проєкті NetBeans:

У [1] ми бачимо веб-сервіс WsDaoJpa, а в [2] — методи, які він надає віддаленим клієнтам.

Нагадаємо архітектуру веб-сервісу, що розробляється:

Компоненти веб-сервісу, який ми збираємося розгорнути, такі:

  • [1]: веб-модуль, який ми щойно створили
  • [2]: модуль EJB, який ми створили на попередньому етапі і від якого залежить веб-сервіс

Щоб розгорнути їх разом, потрібно об’єднати обидва модулі в так званий «корпоративний» проєкт NetBeans:

У [1] створюємо новий корпоративний проєкт [2, 3].

  • У [4,5] надаємо проекту ім’я та вказуємо папку для його створення
  • у [6] вибирають сервер додатків, на якому буде розгорнуто корпоративний додаток
  • у [7] зазначено, що корпоративний проєкт може мати три компоненти: веб-додаток, модуль EJB, клієнтський додаток. У цьому випадку проєкт створюється без жодних компонентів. Їх буде додано пізніше.
  • у [8] — щойно створений корпоративний додаток.
  • у [9] клацніть правою кнопкою миші на [Java EE Modules] і додайте новий модуль
  • у [10] відображаються лише ті модулі NetBeans, які наразі відкриті в IDE. Тут ми вибираємо веб-модуль [serveur-webservice-1-ejb-dao-jpa-hibernate] та модуль EJB [serveur-ejb-dao-jpa-hibernate], які ми створили.
  • У [11] — обидва модулі, додані до корпоративного проєкту.

Залишилося розгорнути цей корпоративний додаток на сервері Glassfish. Далі потрібно запустити SGBD та MySQL, щоб забезпечити доступ до джерела даних JDBC «jdbc/dbrdvmedecins», яке використовує модуль EJB.

  • у [1] запускається сервер Glassfish
  • якщо модуль EJB [serveur-ejb-dao-jpa-hibernate] розгорнуто, його вивантажують [2]
  • у [3] розгортається корпоративний додаток
  • у [4] — вона розгорнута. Бачимо, що вона містить обидва модулі: Web та EJB.

4.10.2. Проєкт NetBeans — версія 2

Тепер ми покажемо, як розгорнути веб-сервіс, коли у нас немає вихідного коду модуля EJB, а є лише його архів .jar.

Новий проект NetBeans для веб-сервісу матиме такий вигляд:

Серед важливих елементів проєкту слід виділити такі:

  • [1]: веб-сервіс реалізовано за допомогою проекту NetBeans типу [Web Application].
  • [2]: веб-сервіс реалізовано за допомогою класу [WsDaoJpa], який ми вже розглядали
  • [3]: архів EJB шару [dao], який дозволяє класу [WsDaoJpa] отримати доступ до визначень різних класів, інтерфейсів та сутностей шарів [dao] та [jpa].

Далі ми створюємо корпоративний проєкт, необхідний для розгортання веб-сервісу:

  • [1], створюємо корпоративний додаток [ea-rdvmedecins], спочатку без жодного модуля.
  • у [2] додаємо попередній веб-модуль [serveur-webservice-ejb-dao-jpa-hibernate]
  • у [3] — результат.

У такому вигляді корпоративний додаток [ea-rdvmedecins] не можна розгорнути на сервері Glassfish із NetBeans. Виникає помилка. Тому потрібно вручну розгорнути архів EAR додатка [ea-rdvmedecins]:

  • архів [ea-rdvmedecins.ear] знаходиться в папці [dist] [2] на вкладці [Files] у NetBeans.
  • У цьому архіві [3] містяться два елементи корпоративного додатка:
  • архів EJB [serveur-ejb-dao-jpa-hibernate]. Цей архів присутній, оскільки він входив до складу бібліотек, на які посилається веб-сервіс.
  • архів веб-сервісу [serveur-webservice- ejb-dao-jpa-hibernate].
  • архів [ea-rdvmedecins.ear] складається з простого Build [4] корпоративного додатка.
  • У [5] операція розгортання завершується з помилкою.

Щоб розгорнути архів [ea-rdvmedecins.ear] корпоративного додатка, ми діємо так, як було показано під час розгортання архіву EJB [serveur-ejb-dao-jpa-hibernate.jar] у розділі 4.2. Ми знову використовуємо веб-клієнт адміністрування сервера GlassFish. Ми не повторюємо кроки, що вже були описані.

Спочатку «вивантажимо» корпоративний додаток, розгорнутий у розділі 4.10.1:

  • [1]: виберіть гілку [Enterprise Applications] сервера Glassfish
  • у [2] виберіть корпоративний додаток, який потрібно вивантажити, а потім у [3] вивантажте його
  • у [4] корпоративний додаток було вивантажено
  • у [1] виберіть гілку [Enterprise Applications] сервера Glassfish
  • у [2], розгорніть новий корпоративний додаток
  • у [3] виберіть тип [Enterprise Application]
  • у [4] вкажіть файл .ear проекту NetBeans [ea-rdvmedecins]
  • у [5] розгорніть цей архів
  • в [6], додаток було розгорнуто
  • у [7], веб-сервіс [WsDaoJpa] з’являється у гілці [Web Services] сервера Glassfish. Його потрібно вибрати.
  • У [8] міститься різноманітна інформація про веб-сервіс. Найцікавішою для клієнта є інформація [9]: URI веб-сервісу.
  • У [10] можна протестувати веб-сервіс
  • У [11] наведено URI веб-сервісу , до якого додано параметр ?tester. Цей URI відкриває тестову сторінку. Усі методи (@WebMethod), що надаються веб-сервісом, відображаються та можуть бути протестовані. Тут ми тестуємо метод [13], який запитує список клієнтів.
  • У [14] ми показуємо лише частину сторінки відповіді. Але можна побачити, що метод getAllClients дійсно повернув список клієнтів. Знімок екрана показує, що відповідь надсилається у форматі XML.

Веб-сервіс повністю описується файлом XML, який називається файлом WSDL:

  • у [1] у веб-інструменті адміністрування сервера Glassfish виберіть веб-сервіс [WsDaoJpa]
  • на [2], перейдіть за посиланням [View WSDL]
  • на [3]: URI файлу WSDL. Цю інформацію важливо знати. Вона необхідна для налаштування клієнтів цього веб-сервісу.
  • [4] — опис веб-сервісу XML. Ми не будемо коментувати цей складний вміст.

4.10.3. Тестування веб-сервісу JUnit

Ми створюємо проект NetBeans, щоб «відтворити» тести, які вже виконувалися з клієнтом EJB, але цього разу з клієнтом для нещодавно розгорнутого веб-сервісу. Тут ми дотримуємося підходу, аналогічного тому, що описано в параграфі 14.2.1, на сторінці 115 документа [ref1].

  • у [1] — класичний Java-проект
  • у [2] — тестовий клас
  • у [3] клієнт використовує архів EJB для доступу до визначень інтерфейсу шару [dao] та сутностей JPA. Нагадаємо, що цей архів знаходиться у підпапці [dist] папки модуля EJB.

Для доступу до віддаленого веб-сервісу необхідно згенерувати проксі-класи:

На наведеному вище схематичному зображенні шар [2] [C=Client] взаємодіє з шаром [1] [S=Serveur]. Щоб взаємодіяти з рівнем [S], клієнт [C] повинен встановити мережеве з’єднання з рівнем [S] і взаємодіяти з ним за певним протоколом. Мережеві з’єднання мають ідентифікатори TCP, а транспортний протокол — HTTP. Рівень [S], що представляє веб-сервіс, реалізовано за допомогою Java-сервлета, який виконується на сервері Glassfish. Ми не писали цей сервлет. Його генерація автоматизована Glassfish на основі анотацій @Webservice та @WebMethod класу [WsDaoJpa], який ми написали. Так само ми автоматизуємо генерацію клієнтського рівня [C]. Іноді шар [C] називають проксі-шаром віддаленого веб-сервісу, оскільки термін proxy позначає проміжний елемент у програмному ланцюжку. У даному випадку проксі C є посередником між клієнтом, який ми збираємося написати, та веб-сервісом, який ми розгорнули.

У NetBeans 6.5 проксі C можна згенерувати таким чином (для подальших дій веб-сервіс має бути активним на сервері GlassFish):

  • у [1] додати новий елемент до проекту Java
  • у [2] виберіть гілку [Web services]
  • у [3] виберіть [Web Service Client]
  • у [4] вкажіть URI файлу WSDL веб-сервісу. Цей URI наведено в розділі 4.10.2.
  • у полі [5] залиште значення за замовчуванням [JAX-WS]. Іншим можливим значенням є [JAX-RPC]
  • після підтвердження роботи майстра створення проксі веб-сервісу проект NetBeans було доповнено гілкою [Web Service References] [6]. Ця гілка відображає методи, що надаються віддаленим веб-сервісом.
  • на вкладці [Files] [7] було додано вихідний код Java [8]. Він відповідає згенерованому проксі на мові C.
  • У [9] наведено код одного з класів. З нього видно, що [10] ці класи було розміщено в пакеті [rdvmedecins.ws]. Ми не будемо коментувати код цих класів, який знову є досить складним.

Для Java-клієнта, який ми зараз створюємо, згенерований проксі-код на C слугує посередником. Щоб отримати доступ до методу M віддаленого веб-сервісу, Java-клієнт викликає метод M проксі-коду на C. Таким чином, Java-клієнт викликає локальні методи (які виконуються в тій самій JVM), і ці локальні виклики прозоро для нього перетворюються на віддалені виклики.

Залишається з’ясувати, як викликати методи M проксі-класу на мові C. Повернімося до нашого тестового класу JUnit:

У [1] тестовий клас [MainTestsDaoRemote] — це той самий клас, що вже використовувався під час тестування EJB шару [dao]:

package dao;
...
public class MainTestsDaoRemote {

   // протестований шар [dao]
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
  }

  @Test
  public void test1() {
...
  }
}
  • у рядку [13] тест test1 збережено без змін.
  • у рядку [9] вміст методу [init] було видалено.

На даному етапі у проєкті є помилки, оскільки тестовий метод [test1] використовує сутності [Client], [Medecin], [Creneau], [Rv], які більше не знаходяться в тих самих пакетах, що й раніше. Вони знаходяться в пакеті згенерованого проксі-коду на мові C. Видаляємо відповідні інструкції import і перегенеруємо їх за допомогою операції «Fix Imports».

Повернемося до коду тестового класу [MainTestsDaoRemote]:

package dao;
...

public class MainTestsDaoRemote {

   // протестовано шар [dao]
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
}

Метод [init] у рядку 10 повинен ініціалізувати посилання на шар [dao] у рядку 7. Нам потрібно знати, як використовувати згенерований проксі-код C у нашому коді. NetBeans допомагає нам у цьому.

  • у [1] виберіть метод [getAllClients] веб-сервісу та, за допомогою миші, перетягніть цей метод у метод [init] тестового класу.

Отримуємо результат [2]. Цей шаблон коду показує, як використовувати згенерований проксі-код на C:

1
2
3
4
5
6
7
8
9
    try { // Виклик операції веб-сервісу
      rdvmedecins.ws.WsDaoJpaService service = new rdvmedecins.ws.WsDaoJpaService();
      rdvmedecins.ws.WsDaoJpa port = service.getWsDaoJpaPort();
       // TODO обробка результату тут
      java.util.List<rdvmedecins.ws.Client> result = port.getAllClients();
      System.out.println("Result = "+result);
    } catch (Exception ex) {
       // TODO обробка користувацьких винятків тут
}
  • рядок [5] показує, що метод [getAllClients] є методом об’єкта типу [WsDaoJpa], визначеного у рядку 3. Тип [WsDaoJpa] — це інтерфейс, що містить ті самі методи, що й ті, які надає віддалений веб-сервіс.
  • У рядку [3] об’єкт [WsDaoJpa port] отримується з іншого об’єкта типу [WsDaoJpaService], визначеного в рядку 2. Тип [WsDaoJpaService] представляє проксі на мові C, згенерований локально.
  • Доступ до віддаленої веб-служби може завершитися невдачею, тому весь код обгорнуто блоком try/catch.
  • Об’єкти проксі-сервера на мові C містяться в пакеті [rdvmedecins.ws]

Зрозумівши цей код, можна побачити, що локальний ідентифікатор віддаленого веб-сервісу можна отримати за допомогою такого коду:

WsDaoJpa dao=new WsDaoJpaService().getWsDaoJpaPort();

Тоді код тестового класу JUnit виглядає так:

package dao;

import rdvmedecins.ws.Client;
import rdvmedecins.ws.Creneau;
import rdvmedecins.ws.Medecin;
import rdvmedecins.ws.Rv;
import rdvmedecins.ws.WsDaoJpa;
import rdvmedecins.ws.WsDaoJpaService;
...

public class MainTestsDaoRemote {

   // протестовано рівень [dao]
  private static WsDaoJpa dao;

  @BeforeClass
  public static void init(){
    dao=new WsDaoJpaService().getWsDaoJpaPort();
  }

  @Test
  public void test1() {
...
  }

   // допоміжний метод — відображає елементи колекції
  private static void display(String message, List elements) {
 ...
  }
}

Тепер ми готові до тестування:

У [1] виконується тест JUnit. У [2] він проходить успішно. Якщо подивитися на вивід у консолі NetBeans, можна побачити такі рядки:

Liste des clients :
rdvmedecins.ws.Client@1982fc1
rdvmedecins.ws.Client@676437
rdvmedecins.ws.Client@1e4853f
rdvmedecins.ws.Client@1e808ca

На стороні сервера суть [Client] має метод toString, який відображає різні поля об’єкта типу [Client]. Під час автоматичного генерування проксі-коду на C об’єкти створюються в проксі-коді на C, але лише з приватними полями та відповідними методами get / set. Таким чином, метод toString не був згенерований в об’єкті [Client] проксі-коду на C. Це пояснює попереднє відображення. Це ніяк не впливає на результат тесту JUnit: він пройшов успішно. Відтепер вважатимемо, що веб-сервіс працює.