Skip to content

4. Usługa internetowa J2EE dotycząca spotkań

Wróćmy do architektury tworzonej aplikacji:

W tej części skupimy się na tworzeniu serwisu internetowego J2EE [1] działającego na serwerze Sun / Glassfish.

4.1. Baza danych

Baza danych, którą nazwiemy [dbrdvmedecins] , jest bazą danych MySQL5 zawierającą cztery tabele:

Image

4.1.1. Tabela [MEDECINS]

Zawiera informacje o lekarzach obsługiwanych przez aplikację [RdvMedecins].

  • ID: numer identyfikacyjny lekarza – klucz główny tabeli
  • VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
  • NOM: nazwisko lekarza
  • PRENOM: jego imię
  • TITRE: tytuł (panna, pani, pan)

4.1.2. Tabela [CLIENTS]

Pacjenci poszczególnych lekarzy są zapisani w tabeli [CLIENTS]:

  • ID: numer identyfikacyjny pacjenta – klucz główny tabeli
  • VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
  • NOM: nazwisko klienta
  • PRENOM: imię klienta
  • TITRE: tytuł (panna, pani, pan)

4.1.3. Tabela [CRENEAUX]

Zawiera listę przedziałów czasowych, w których możliwe są operacje RV:

  • ID: numer identyfikujący przedział czasowy – klucz główny tabeli (wiersz 8)
  • VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
  • ID_MEDECIN: numer identyfikujący lekarza, do którego należy ten przedział czasowy – klucz obcy w kolumnie MEDECINS (ID).
  • HDEBUT: godzina rozpoczęcia terminu
  • MDEBUT: minuty początku przedziału czasowego
  • HFIN: godzina zakończenia przedziału czasowego
  • MFIN: minuty zakończenia przedziału czasowego

Drugi wiersz tabeli [CRENEAUX] (por. [1] powyżej) wskazuje na przykład, że przedział nr 2 rozpoczyna się o godz. 8:20 i kończy o godz. 8:40 oraz należy do lekarza nr 1 (pani Marie PELISSIER).

4.1.4. Tabela [RV]

Zawiera listę RV przypisanych każdemu lekarzowi:

  • ID: numer jednoznacznie identyfikujący RV – klucz główny
  • JOUR: dzień RV
  • ID_CRENEAU: przedział czasowy dla RV – klucz obcy w polu [ID] tabeli [CRENEAUX] – określa zarówno przedział czasowy, jak i danego lekarza.
  • ID_CLIENT: numer klienta, dla którego dokonano rezerwacji – klucz obcy w polu [ID] tabeli [CLIENTS]

Ta tabela posiada , która wymusza unikalność wartości połączonych kolumn (JOUR, ID_CRENEAU):

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

Jeśli wiersz w tabeli [RV] ma wartość (JOUR1, ID_CRENEAU1) w kolumnach (JOUR, ID_CRENEAU), to ta wartość nie może występować nigdzie indziej. W przeciwnym razie oznaczałoby to, że dwa rekordy o wartości RV zostały zarejestrowane w tym samym czasie dla tego samego lekarza. Z punktu widzenia programowania w Javie sterownik bazy danych o wartości JDBC uruchamia sterownik o wartości SQLException, gdy wystąpi taka sytuacja.

Wiersz o wartości 3 dla id (por. [1] powyżej) oznacza, że rezerwacja o numerze RV została dokonana na przedział czasowy nr 20 dla klienta nr 4 w dniu 23.08.2006 r. Z tabeli [CRENEAUX] wynika, że przedział nr 20 odpowiada przedziałowi czasowemu 16:20–16:40 i należy do lekarza nr 1 (pani Marie PELISSIER). Z tabeli [CLIENTS] wynika, że klient nr 4 to panna Brigitte BISTROU.

4.2. Tworzenie bazy danych

Utwórz bazę danych o nazwie MySql [dbrdvmedecins] za pomocą wybranego narzędzia. Do utworzenia tabel i wypełnienia ich danymi można wykorzystać skrypt [createbd.sql], który zostanie Państwu dostarczony. Jego treść jest następująca:

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. Elementy architektury po stronie serwera

Wróćmy do architektury tworzonej aplikacji:

Po stronie serwera aplikacja będzie składać się z:

  1. warstwy JPA umożliwiającej pracę z BD za pomocą obiektów
  1. z EJB odpowiedzialnego za zarządzanie operacjami z warstwą JPA
  2. z serwisu internetowego odpowiedzialnego za udostępnianie zdalnym klientom interfejsu EJB w postaci serwisu internetowego.

Elementy (b) i (c) implementują warstwę [dao] przedstawioną na poprzednim schemacie. Wiadomo, że aplikacja może uzyskać dostęp do zdalnego EJB za pośrednictwem protokołów RMI i JNDI. W praktyce ogranicza to klientów do klientów Java. Usługa internetowa wykorzystuje znormalizowany protokół komunikacyjny, który jest implementowany przez różne języki programowania: .NET, PHP, C++, ... Właśnie to chcemy tutaj pokazać, korzystając z klienta .NET.

Krótkie wprowadzenie do usług internetowych można znaleźć w kursie [ref1], akapit 14, strona 109.

Usługę internetową można zaimplementować na dwa sposoby:

  • za pomocą klasy opatrzonej adnotacją @WebService, która działa w kontenerze internetowym
  • poprzez komponent EJB opatrzony adnotacją @WebService, który działa w kontenerze EJB

W tym przypadku zastosujemy pierwsze rozwiązanie:

W kursie [ref1], w akapicie 14 na stronie 109, znajduje się przykład wykorzystujący drugie rozwiązanie.

4.4. Konfiguracja serwera Glassfish z em Hibernate

W zależności od wersji serwer Glassfish V2 dostarczany wraz z NetBeans może nie zawierać bibliotek Hibernate, których wymaga warstwa JPA / Hibernate. Jeśli w dalszej części samouczka okaże się, że GlassFish nie oferuje implementacji JPA/Hibernate lub że podczas wdrażania usług pojawi się wyjątek wskazujący, że nie znaleziono bibliotek Hibernate, należy dodać te biblioteki do folderu [<glassfish>/domains/domain1/lib/ext], a następnie ponownie uruchomić serwer GlassFish:

  • w [1], folder <glassfish>/.../lib/ext
  • do [2], biblioteki Hibernate oraz kilka sterowników JDBC
  • na [3], sterownik JDBC z MySQL

Biblioteki Hibernate znajdują się w pliku ZIP dołączonym do samouczka.

4.5. Narzędzia do automatycznego generowania w NetBeans

Wróćmy do architektury, którą musimy zbudować:

Za pomocą NetBeans można automatycznie wygenerować warstwę [JPA] oraz warstwę [Ejb], która kontroluje dostęp do wygenerowanych encji JPA. Warto zapoznać się z tymi metodami automatycznego generowania, ponieważ wygenerowany kod dostarcza cennych wskazówek dotyczących sposobu pisania encji JPA lub kodu EJB, który z nich korzysta.

Poniżej opisujemy niektóre z tych narzędzi do automatycznego generowania. Aby zrozumieć wygenerowany kod, należy posiadać solidną wiedzę na temat encji JPA, [ref1] oraz EJB i [ref2].

Tworzenie połączenia NetBeans z bazą danych

  • uruchom SGBD i MySQL 5, aby BD był dostępny
  • utwórz połączenie NetBeans z bazą danych [dbrdvmedecins]
  • w zakładce [Files], w gałęzi [Databases] [1], wybrać sterownik JDBC MySQL [2]
  • następnie należy wybrać opcję [3] „Connect Using”, która umożliwia nawiązanie połączenia z bazą danych MySQL
  • w [4] należy podać wymagane informacje
  • a następnie zatwierdzić w [5]
  • w [6] połączenie zostało utworzone. Widoczne są cztery tabele podłączonej bazy danych.

Tworzenie projektu EJB

  • w [1], utworzyć nową aplikację, moduł EJB
  • w [2], wybierz kategorię [Java EE], a w [3] typ [EJB Module]
  • w [4] wybierz folder dla projektu, a w [5] nadaj mu nazwę – następnie zakończ pracę kreatora
  • w [6] wygenerowany projekt

Dodanie zasobu JDBC do serwera Glassfish

Dodamy zasób JDBC do serwera Glassfish.

  • w zakładce [Services] uruchom serwer Glassfish [2, 3]
  • w zakładce [Projects] kliknij prawym przyciskiem myszy na projekt Ejb i w [5] wybierz opcję [New / Other] umożliwiającą dodanie elementu do projektu.

Image

  • w [6], wybrać kategorię [Glassfish], a następnie w [7] wskazać, że chcemy utworzyć zasób JDBC, wybierając typ [JDBC Resource]
  • w polu [8] należy wskazać, że zasób JDBC będzie korzystał z własnej puli połączeń
  • w [9], nadać nazwę zasobowi JDBC
  • w [10], przejść do następnego etapu
  • w [11] definiuje się właściwości puli połączeń zasobu JDBC
  • w [12], nadać nazwę puli połączeń
  • w [13], wybierz utworzone wcześniej połączenie NetBeans [dbrdvmedecins]
  • w [14], przejść do następnego kroku
  • w [15] – zazwyczaj nie ma potrzeby wprowadzania żadnych zmian na tej stronie. Właściwości połączenia z bazą danych MySQL [dbrdvmedecins] zostały pobrane z właściwości wcześniej utworzonego połączenia NetBeans o nazwie [dbrdvmedecins]
  • w [16], przejdź do następnego kroku
  • na [17], zachowaj proponowane wartości domyślne
  • w [18], zakończ pracę kreatora. Kreator utworzy plik [sun-resources.xml] [19], którego zawartość jest następująca:
<?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>

Powyższy plik zawiera wszystkie informacje wprowadzone w kreatorze w formacie XML. Zostanie on wykorzystany przez IDE NetBeans do wysłania żądania do serwera GlassFish o utworzenie zasobu „jdbc/dbrdvmedecins” zdefiniowanego w wierszu 4.

Tworzenie jednostki trwałości

Jednostka trwałości [persistence.xml] konfiguruje warstwę JPA: wskazuje używaną implementację JPA (Toplink, Hibernate itp.) i dokonuje jej konfiguracji.

  • w [1] kliknij prawym przyciskiem myszy projekt EJB i wybierz [New / Other] w [2]
  • w [3], wybierz kategorię [Persistence], a następnie w [4], wskaż, że chcesz utworzyć jednostkę trwałości JPA
  • w [5], nadać nazwę utworzonej jednostce trwałości
  • w [6], wybierz [Hibernate] jako implementację JPA
  • w [7], wybierz właśnie utworzony zasób Glassfish „jdbc/dbrdvmedecins”
  • w [8] należy wskazać, że podczas instancjonowania warstwy JPA nie należy wykonywać żadnych operacji na bazie danych
  • zakończyć pracę kreatora
  • w [9] plik [persistence.xml] utworzony przez kreatora

Jego zawartość jest następująca:

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>

Ponownie, w formacie XML zawiera informacje podane w kreatorze. Ten plik nie wystarcza do pracy z bazą danych MySQL5 „dbrdvmedecins”. Musielibyśmy wskazać Hibernate, jaki typ pliku SGBD ma być obsługiwany. Zostanie to zrobione w późniejszym terminie.

Tworzenie encji JPA

 
  • w [1] kliknij prawym przyciskiem myszy na projekt, a następnie w [2] wybierz opcję [New / Other]
  • w [3], wybierz kategorię [Persistence], a następnie w [4], zaznacz, że chcesz utworzyć obiekty JPA na podstawie istniejącej bazy danych.
  • w [5] należy wybrać źródło JDBC „jdbc/dbrdvmedecins”, które utworzyliśmy
  • w [6], cztery tabele powiązanej bazy danych
  • w [7,8], uwzględnij je wszystkie podczas generowania encji JPA
  • w [9], kontynuować pracę z kreatorem
  • w [10], jednostki JPA, które zostaną wygenerowane
  • w [11], nadać nazwę pakietowi elementów JPA
  • w [12], wybierz typ Java, który będzie enkapsulował listy obiektów zwracanych przez warstwę JPA
  • zakończyć pracę kreatora
  • w [13], cztery wygenerowane encje JPA, po jednej dla każdej tabeli bazy danych.

Oto na przykład kod encji [Rv], która reprezentuje wiersz tabeli [rv] w bazie [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() {
  }

...
}

Tworzenie warstwy EJB zapewniającej dostęp do encji JPA

  • w [1], kliknij prawym przyciskiem myszy na projekt, a następnie w [2] wybierz opcję [New / Other]
  • w [3], wybierz kategorię [Persistence], a następnie w [4] typ [Session Beans for Entity Classes]
  • w [5], wcześniej utworzone jednostki JPA są wyświetlane
  • w [6], zaznaczyć je wszystkie
  • w [7] – zostały zaznaczone
  • w pliku [8], kontynuuj pracę z kreatorem
  • w [9], nadać nazwę pakietowi EJB, które zostaną wygenerowane
  • w [10], należy wskazać, że komponenty EJB muszą implementować zarówno interfejs lokalny, jak i zdalny
  • zakończyć pracę kreatora
  • w [11], wygenerowane komponenty EJB

Oto na przykład kod EJB, który zarządza dostępem do encji [Rv], a więc do tabeli [rv] w bazie danych [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();
  }

}

Jak już wspomniano, automatyczne generowanie kodu może być bardzo przydatne na początku projektu oraz w celu zapoznania się z encjami typu JPA i EJB. W dalszej części przepiszemy warstwy JPA i EJB, wykorzystując nasz własny kod, jednak czytelnik odnajdzie w nich informacje, które właśnie omówiliśmy w kontekście automatycznego generowania warstw.

4.6. Projekt NetBeans modułu EJB

Tworzymy nowy, pusty moduł EJB (patrz punkt 4.5):

 
  • pakiet [rdvmedecins.entites] zawiera encje warstwy JPA
  • pakiet [rdvmedecins.dao] implementuje EJB warstwy [dao]
  • pakiet [rdvmedecins.exceptions] implementuje klasę wyjątku specyficzną dla aplikacji

W dalszej części zakładamy, że czytelnik wykonał wszystkie czynności opisane w punkcie 4.5. Niektóre z nich będzie musiał powtórzyć.

4.6.1. Konfiguracja warstwy JPA

Przypomnijmy architekturę naszej aplikacji klient-serwer:

Projekt NetBeans:

 

Warstwa [JPA] jest konfigurowana za pomocą powyższych plików [persistence.xml] i [sun-resources.xml]. Oba te pliki są generowane przez znane już kreatory:

  • proces generowania pliku [sun-resources.xml] został opisany w paragrafie 4.5.
  • Sposób generowania pliku [persistence.xml] opisano w paragrafie 4.5.

Wygenerowany plik [persistence.xml] należy zmodyfikować w następujący sposób:

<?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>
       <!-- Dialekt -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>
  • wiersz 3: typ transakcji to JTA: transakcje będą zarządzane przez kontener EJB3 serwera GlassFish
  • wiersz 4: stosowana jest implementacja JPA / Hibernate. W tym celu biblioteka Hibernate została dodana do serwera GlassFish (patrz punkt 4.4).
  • wiersz 5: źródło danych JTA wykorzystywane przez warstwę JPA nosi nazwę JNDI „jdbc/dbrdvmedecins”.
  • wiersz 8: ten wiersz nie jest generowany automatycznie. Należy go dodać ręcznie. Informuje on bibliotekę Hibernate, że wykorzystywanym źródłem danych SGBD jest MySQL5.

Źródło danych „jdbc/dbrdvmedecins” jest skonfigurowane w następującym pliku [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>
  • wiersze 8–10: parametry JDBC źródła danych (adres URL bazy danych, nazwa użytkownika i hasło). Baza danych MySQL dbrdvmedecins jest tą samą, która została opisana w punkcie 4.1.
  • wiersz 7: parametry puli połączeń powiązanej z tym źródłem danych

4.6.2. Elementy warstwy JPA

Przypomnijmy architekturę naszej aplikacji klient-serwer:

Projekt NetBeans:

Pakiet [rdvmedecins.entites] implementuje warstwę [Jpa].

W rozdziale 4.5 omówiliśmy, jak automatycznie wygenerować encje JPA w aplikacji. W tym przypadku nie skorzystamy z tej techniki, lecz sami zdefiniujemy encje. Będą one jednak w znacznej mierze oparte na kodzie wygenerowanym w rozdziale 4.5. W tym przypadku chcemy, aby encje [Medecin] i [Client] były klasami potomnymi klasy [Personne].

Klasa Personne służy do reprezentowania lekarzy i klientów:

package rdvmedecins.entites;
...
@MappedSuperclass
public class Personne implements Serializable {
   // cechy osoby

  @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;

   // domyślny konstruktor
  public Personne() {
  }

   // konstruktor z parametrami
  public Personne(String titre, String nom, String prenom) {
     // korzystamy z metod ustawiających
...
  }

   // konstruktor z kopiowaniem
  public Personne(Personne personne) {
     // przekazywanie przez metody ustawiające
 ...
  }

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

// metody pobierające i ustawiające
....
}
  • wiersz 3: należy zauważyć, że klasa [Personne] sama w sobie nie jest encją (@Entity). Będzie ona klasą nadrzędną dla encji. Adnotacja @MappedSuperClass wskazuje na tę sytuację.

Entyteta [Client] zawiera wiersze tabeli [clients]. Wywodzi się ona z poprzedniej klasy [Personne]:

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

   // konstruktor domyślny
  public Client() {
  }

   // konstruktor z parametrami
  public Client(String titre, String nom, String prenom) {
     // klasa nadrzędna
    super(titre, nom, prenom);
  }

   // konstruktor kopiujący
  public Client(Client client) {
     // klasa nadrzędna
    super(client);
  }
}
  • wiersz 3: klasa [Client] jest encją JPA
  • wiersz 4: jest ona powiązana z tabelą [clients]
  • wiersz 5: wywodzi się z klasy [Personne]

Entyteta [Medecin], która hermetyzuje wiersze tabeli [medecins], jest zbudowana według tego samego wzorca:

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

   // konstruktor domyślny
  public Medecin() {
  }

   // konstruktor z parametrami
  public Medecin(String titre, String nom, String prenom) {
     // nadrzędny
    super(titre, nom, prenom);
  }

   // konstruktor z kopiowaniem
  public Medecin(Medecin medecin) {
     // nadrzędny
    super(medecin);
  }
}

Entyteta [Creneau] zawiera wiersze tabeli [creneaux]:

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

   // charakterystyka przedziału czasowego 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;

   // domyślny producent
  public Creneau() {

  }

   // konstruktor z parametrami
  public Creneau(Medecin medecin, Integer hDebut,Integer mDebut, Integer hFin, Integer mFin) {
     // przechodzi się przez metody ustawiające
...
  }

   // konstruktor z kopiowaniem
  public Creneau(Creneau creneau) {
     // przekazywanie przez metody ustawiające
...
  }

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

   // metody ustawiające i pobierające
...
}
  • wiersze 15–17 modelują relację „jeden do wielu”, która istnieje między tabelą [creneaux] a tabelą [medecins] w bazie danych.

Entyteta [Rv] zawiera wiersze z tabeli [rv]:

package rdvmedecins.entites;
...
@Entity
@Table(name = "RV")
public class Rv implements Serializable {
   // właściwości

  @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;

   // konstruktor domyślny
  public Rv() {
  }

   // konstruktor z parametrami
  public Rv(Date jour, Client client, Creneau creneau) {
     // korzystamy z metod ustawiających
...
  }

   // konstruktor z kopiowaniem
  public Rv(Rv rv) {
     // przekazywanie przez metody ustawiające
...
  }

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

// metody pobierające i ustawiające
...
}
  • wiersze 15–17 modelują relację „jeden do wielu”, która istnieje między tabelą [rv] a tabelą [clients] w bazie danych, a wiersze 18–20 relację „jeden do wielu”, która istnieje między tabelą [rv] a tabelą [creneaux]

4.6.3. Klasa wyjątku

Klasa wyjątku [RdvMedecinsException] w aplikacji ma następującą postać:

package rdvmedecins.exceptions;

import javax.ejb.ApplicationException;

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

  private static final long serialVersionUID = 1L;

   // pola prywatne
  private int code = 0;

   // konstruktory
  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);
  }

   // metody pobierające i ustawiające
...
}
  • wiersz 6: klasa ta wywodzi się z klasy [RuntimeException]. Kompilator nie wymusza zatem obsługi tej klasy za pomocą instrukcji try / catch.
  • wiersz 5: adnotacja @ApplicationException sprawia, że wyjątek nie zostanie „przejęty” przez wyjątek typu [EjbException].

Aby zrozumieć adnotację @ApplicationException, wróćmy do architektury stosowanej po stronie serwera:

Wyjątek typu [RdvMedecinsException] zostanie wygenerowany przez metody EJB warstwy [dao] wewnątrz kontenera EJB3 i przechwycony przez ten kontener. Bez adnotacji @ApplicationException kontener EJB3 hermetyzuje wystąpiony wyjątek w wyjątku typu [EjbException] i ponownie go zgłasza. Można nie chcieć takiego enkapsulowania i pozwolić, aby z kontenera Ejb3 wyszedł wyjątek typu [RdvMedecinsException]. Umożliwia to adnotacja @ApplicationException. Ponadto atrybut (rollback=true) tej adnotacji informuje kontener EJB3, że jeśli wyjątek typu [RdvMedecinsException] wystąpi wewnątrz metody wykonywanej w ramach transakcji z SGBD, transakcja ta musi zostać cofnięta. W terminologii technicznej nazywa się to wykonaniem operacji rollback na transakcji.

4.6.4. Komponent EJB warstwy [dao]

Interfejs Java [IDao] warstwy [dao] wygląda następująco:

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

   // lista klientów
  public List<Client> getAllClients();
   // lista lekarzy
  public List<Medecin> getAllMedecins();
   // lista terminów wizyt u lekarza
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // lista wizyt u lekarza w danym dniu
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // znalezienie klienta na podstawie jego identyfikatora
  public Client getClientById(Long id);
   // znalezienie pacjenta na podstawie jego identyfikatora
  public Medecin getMedecinById(Long id);
   // znalezienie wizyty na podstawie jej identyfikatora
  public Rv getRvById(Long id);
   // znalezienie przedziału czasowego na podstawie identyfikatora
  public Creneau getCreneauById(Long id);
   // dodaj RV
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // usunięcie RV
  public void supprimerRv(Rv rv);
}

Lokalny interfejs [IDaoLocal] dla EJB jest po prostu pochodną poprzedniego interfejsu [IDao]:

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

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{
}

To samo dotyczy interfejsu zdalnego [IDaoRemote]:

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

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao {
}

EJB o nazwie [DaoJpa] implementuje oba interfejsy, zarówno lokalny, jak i zdalny:

1
2
3
4
5
6
7
package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {
...
}
  • wiersz 3 wskazuje, że zdalny EJB nosi nazwę „rdvmedecins.dao”
  • wiersz 4 wskazuje, że wszystkie metody EJB są wykonywane w ramach transakcji zarządzanej przez kontener EJB3.
  • wiersz 5 pokazuje, że komponent EJB implementuje interfejsy lokalny i zdalny.

Pełny kod EJB wygląda następująco:

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

  @PersistenceContext
  private EntityManager em;

   // lista klientów
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select c from Client c").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }

   // lista lekarzy
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select m from Medecin m").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }

   // lista terminów wizyt u danego lekarza
   // lekarz: dany lekarz
  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);
    }
  }

   // lista wizyt u danego lekarza w danym dniu
   // lekarz: dany lekarz
   // dzień: dzień
  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);
    }
  }

   // dodanie wizyty
   // dzień: dzień wizyty
   // przedział czasowy: przedział czasowy wizyty
   // klient: klient, dla którego umówiono wizytę
  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);
    }
  }

   // usunięcie terminu spotkania
   // spotkanie: usunięte spotkanie
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

   // pobranie danego klienta
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 7);
    }
  }

   // pobranie określonego lekarza
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 8);
    }
  }

   // pobranie określonej wizyty
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 9);
    }
  }

   // pobierz określony termin
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 10);
    }
  }
}
  • wiersz 8: obiekt EntityManager, który zarządza dostępem do kontekstu trwałości. Podczas instancjonowania klasy pole to zostanie zainicjowane przez kontener EJB dzięki adnotacji @PersistenceContext w wierszu 7.
  • wiersz 15: zapytanie JPQL, które zwraca wszystkie wiersze tabeli [clients] w postaci listy obiektów [Client].
  • wiersz 22: analogiczne zapytanie dotyczące lekarzy
  • wiersz 32: zapytanie JPQL wykonujące połączenie tabel [creneaux] i [medecins]. Jest ono filtrowane według identyfikatora lekarza.
  • wiersz 43: zapytanie JPQL wykonujące połączenie tabel [rv], [creneaux] i [medecins] oraz posiadające dwa parametry: identyfikator lekarza i dzień wizyty.
  • wiersze 55–57: utworzenie wizyty, a następnie zapisanie jej w bazie danych.
  • wiersz 67: usunięcie wizyty z bazy danych.
  • wiersz 76: wykonuje zapytanie SELECT w bazie danych w celu znalezienia określonego klienta
  • wiersz 85: to samo dla lekarza
  • wiersz 94: to samo dla wizyty
  • wiersz 103: to samo dla przedziału czasowego
  • Wszystkie operacje z wykorzystaniem kontekstu trwałości em z linii 9 mogą napotkać problem z bazą danych. Dlatego wszystkie są otoczone blokiem try / catch. Ewentualny wyjątek jest zamknięty w „własnym” wyjątku RdvMedecinsException.

Po skompilowaniu moduł EJB tworzy plik .jar o nazwie „ ”:

4.7. Wdrażanie modułu EJB warstwy [dao] za pomocą NetBeans

NetBeans umożliwia proste wdrożenie utworzonego wcześniej modułu EJB na serwerze GlassFish.

  • W właściwościach projektu EJB należy sprawdzić opcje uruchamiania [1].
  • W polu [2] należy wpisać nazwę serwera, na którym ma zostać wdrożony komponent EJB
  • w zakładce [Services] [3] uruchamia się go [4].
  • w [5], serwer Glassfish po uruchomieniu. Nie ma jeszcze modułu EJB.
  • Uruchom serwer MySQL i upewnij się, że baza danych [dbrdvmedecins] jest dostępna. W tym celu możesz skorzystać z połączenia NetBeans utworzonego w punkcie 4.5.
  • W zakładce [Projects] [6] wdrażamy moduł EJB [7]: aby zasób JDBC „jdbc/dbrdvmedecins” wykorzystywany przez EJB był dostępny, należy uruchomić SGBD MySQL5.
  • W [8] wdrożony komponent EJB pojawia się w drzewie katalogów serwera GlassFish
  • W [9] usuwa się wdrożony komponent EJB
  • w [10] EJB nie pojawia się już w drzewie katalogów serwera GlassFish.

4.8. Wdrażanie komponentu EJB z warstwy [dao] za pomocą Glassfish

Poniżej pokazujemy, jak wdrożyć komponent EJB na serwerze GlassFish z archiwum .jar.

  • Uruchom serwer MySQL i upewnij się, że baza danych [dbrdvmedecins] jest dostępna. W tym celu możesz skorzystać z połączenia NetBeans utworzonego w punkcie 4.5.

Przypomnijmy konfigurację JPA modułu EJB, który ma zostać wdrożony. Konfiguracja ta znajduje się w pliku [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>
       <!-- Dialekt -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>

Wiersz 5 wskazuje, że warstwa JPA korzysta ze źródła danych JTA, c.a.d, zarządzanego przez kontener EJB3 o nazwie „jdbc/dbrdvmedecins”.

W rozdziale 4.5 omówiliśmy, jak utworzyć ten zasób JDBC w programie NetBeans. Tutaj pokazujemy, jak zrobić to bezpośrednio w GlassFish. Postępujemy zgodnie z procedurą opisaną w rozdziale 13.1.2, na stronie 79 dokumentacji [ref1].

Zaczynamy od usunięcia zasobu, aby móc go ponownie utworzyć. Robimy to z poziomu NetBeans:

  • w pliku [1] zasoby JDBC serwera GlassFish
  • na [2], zasób „jdbc/dbrdvmedecins” naszego EJB
  • na [3], pula połączeń tego zasobu JDBC
  • w [4] usuwamy pulę połączeń. Spowoduje to usunięcie wszystkich zasobów JDBC, które z niej korzystają, a więc zasobu „jdbc/dbrdvmedecins”.
  • W plikach [5] i [6] zasób JDBC oraz pula połączeń zostały usunięte.

Teraz używamy konsoli administracyjnej serwera GlassFish do utworzenia zasobu JDBC i wdrożenia komponentu EJB.

  • w zakładce [services] [1] w NetBeans, uruchom serwer GlassFish [2], a następnie przejdź do jego konsoli administracyjnej
  • pod adresem [4], zaloguj się jako administrator (hasło: adminadmin, jeśli nie zmieniłeś go podczas instalacji ani później).
  • w [5], wybierz gałąź [Connection Pools] zasobów Glassfish
  • w [6], utwórz nową pulę połączeń. Przypominamy, że pula połączeń to technika służąca do ograniczenia liczby otwierania i zamykania połączeń z SGBD. Po uruchomieniu serwera N — liczba określona w konfiguracji — połączeń zostaje otwartych z SGBD. Te otwarte połączenia są następnie udostępniane komponentom EJB, które je wykorzystują do wykonania operacji z SGBD. Gdy operacja zostanie zakończona, komponent EJB zwraca połączenie do puli. Połączenie nigdy nie jest zamykane. Jest ono współdzielone między różnymi wątkami, które uzyskują dostęp do obiektu SGBD
  • w [7], nadaj nazwę puli
  • w [8], klasą modelującą źródło danych jest klasa [javax.sql.DataSource]
  • w [9], a SGBD, który przechowuje źródło danych, to tutaj MySQl.
  • w [10], przejdź do następnego etapu
  • w [11] atrybut „Connection Validation Required” powoduje, że przed przyznaniem połączenia pula sprawdza, czy jest ono operacyjne. Jeśli tak nie jest, tworzy nowe. Pozwala to aplikacji na dalsze działanie po chwilowej przerwie w połączeniu z SGBD. W trakcie przerwy żadne połączenie nie jest dostępne, a do klienta zgłaszane są wyjątki. Gdy przerwa dobiegnie końca, klienci, którzy nadal żądają połączeń, otrzymują je ponownie: dzięki atrybutowi „Connection Validation Required” wszystkie połączenia w puli zostaną odtworzone. Bez tego atrybutu pula stwierdziłaby, że początkowe połączenia zostały przerwane, ale nie próbowałaby tworzyć nowych.
  • W [12] wymagany jest poziom izolacji „Read Committed” dla transakcji. Poziom ten gwarantuje, że transakcja T2 nie może odczytać danych zmodyfikowanych przez transakcję T1, dopóki ta ostatnia nie zostanie całkowicie zakończona.
  • W transakcji [13] żąda się, aby wszystkie transakcje korzystały z poziomu izolacji określonego w transakcji [12]
  • w [14] i [15] należy określić adres URL transakcji BD, której połączenia są zarządzane przez pulę
  • w [16], użytkownikiem będzie root
  • w [17] dodaj właściwość
  • w [18] dodaj właściwość „Password” o wartości () w [19]. Chociaż zrzut ekranu [19] tego nie pokazuje, nie należy wpisywać pustego ciągu znaków, lecz () (nawias otwierający, nawias zamykający), aby oznaczyć puste hasło. Jeśli użytkownik root z bazy SGBD MySQL ma hasło inne niż puste, należy wprowadzić to hasło.
  • w [20], zakończ pracę kreatora tworzenia puli połączeń dla bazy MySQL [dbrdvmedecins].
  • w [21] – pula została utworzona. Należy kliknąć jej link.
  • w [22] przycisk [Ping] umożliwia nawiązanie połączenia z bazą danych [dbrdvmedecins]
  • w [23], jeśli wszystko przebiegło pomyślnie, pojawi się komunikat informujący o pomyślnym nawiązaniu połączenia

Po utworzeniu puli połączeń można utworzyć zasób JDBC:

  • w [1], wybieramy gałąź [JDBC Resources] z drzewa obiektów serwera
  • w [2] tworzymy nowy zasób JDBC
  • w [3] nadajemy nazwę zasobowi JDBC. Musi ona odpowiadać nazwie użytej w pliku [persistence.xml]:
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
  • w pliku [4] określa się pulę połączeń, z której ma korzystać nowy zasób JDBC: tę, którą właśnie utworzono
  • w pliku [5] kończymy pracę z kreatorem
  • w [6] nowy zasób JDBC

Teraz, gdy zasób JDBC został utworzony, można wdrożyć archiwum jar komponentu EJB:

  • w [1], wybierz gałąź [Enterprise Applications]
  • w [2], za pomocą przycisku [Deploy], wskaż, że chcesz wdrożyć nową aplikację
  • w [3], zaznacz, że aplikacja jest modułem EJB
  • w [4] wybierz plik JAR modułu EJB [serveur-ejb-dao-jpa-hibernate.jar], który został Ci przekazany dla TP.
  • w [5] można zmienić nazwę modułu EJB, jeśli jest taka potrzeba
  • w [6] zakończ pracę kreatora wdrażania modułu EJB
  • W module [7] wdrożono moduł EJB. Można z niego teraz korzystać.

4.9. Testy modułu EJB warstwy [dao]

Teraz, gdy moduł EJB warstwy [dao] naszej aplikacji został wdrożony, możemy go przetestować. Zrobimy to za pomocą następującego klienta Java:

Klasa [MainTestsDaoRemote] [1] jest klasą testową JUnit 4. Biblioteki w [2] składają się z jednej strony z:

  • pliku JAR EJB warstwy [dao] [3] (patrz punkt 4.6.4).
  • bibliotek GlassFish [4] niezbędnych dla zdalnych klientów EJB.

Klasa testowa ma następującą postać:

package dao;
...
public class MainTestsDaoRemote {

   // warstwa [dao] przetestowana
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
     // inicjalizacja środowiska JNDI
    InitialContext initialContext = new InitialContext();
     // instancjonowanie warstwy DAO
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
     // dane testowe
    String jour = "2006:08:23";
     // wyświetlanie klientów
    List<Client> clients = null;
    try {
      clients = dao.getAllClients();
      display("Liste des clients :", clients);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // wyświetlanie lekarzy
    List<Medecin> medecins = null;
    try {
      medecins = dao.getAllMedecins();
      display("Liste des médecins :", medecins);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // wyświetlanie terminów wizyt u lekarza
    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);
    }
     // lista wizyt u lekarza w danym dniu
    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);
    }
     // dodaj 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);
    }
     // dodaj RV w tym samym terminie tego samego dnia
     // powinno wywołać wyjątek
    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);
    }
     // usuń 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);
    }
  }

   // metoda pomocnicza – wyświetla elementy kolekcji
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • wiersz 13: należy zwrócić uwagę na instancjonowanie proxy zdalnego EJB. Używa się jego nazwy JNDI „rdvmedecins.dao”.
  • Metody testowe wykorzystują metody udostępniane przez EJB (patrz punkt 4.6.4).

Jeśli wszystko przebiegnie pomyślnie, testy powinny zakończyć się powodzeniem:

 

Teraz, gdy EJB warstwy [dao] działa poprawnie, można przejść do jego publicznego udostępnienia za pośrednictwem serwisu internetowego.

4.10. Usługa internetowa warstwy [dao]

Krótkie wprowadzenie do pojęcia serwisu internetowego można znaleźć w paragrafie 14, na stronie 111 dokumentu [ref1].

Wróćmy do architektury serwera naszej aplikacji klient-serwer:

W powyższym przykładzie skupiamy się na usłudze internetowej warstwy [dao]. Jedynym zadaniem tej usługi jest udostępnienie interfejsu EJB warstwy [dao] klientom korzystającym z różnych platform, którzy są w stanie komunikować się z usługą internetową.

Przypomnijmy, że istnieją dwa sposoby implementacji usługi internetowej:

  • za pomocą klasy opatrzonej adnotacją @WebService, która działa w kontenerze internetowym
  • poprzez komponent EJB opatrzony adnotacją @WebService, który działa w kontenerze EJB

W tym przypadku stosujemy pierwsze rozwiązanie. W środowisku NetBeans IDE musimy utworzyć projekt korporacyjny zawierający dwa moduły:

  • moduł EJB, który będzie działał w kontenerze EJB: EJB warstwy [dao].
  • moduł internetowy, który będzie działał w kontenerze internetowym: usługa internetowa, którą właśnie tworzymy.

Zrealizujemy ten projekt korporacyjny na dwa sposoby.

4.10.1. Projekt NetBeans – wersja 1

Najpierw tworzymy projekt NetBeans typu „Web Application”:

  • w [1] tworzymy nowy projekt w kategorii „Java Web” [2] typu „Web Application” [3].
  • w [4] nadajemy nazwę projektowi, a w [5] określamy folder, w którym ma zostać wygenerowany
  • w pliku [6] określa się serwer aplikacji, który będzie uruchamiał aplikację internetową
  • w pliku [7] określa się kontekst aplikacji
  • w pliku [8] zatwierdza się konfigurację projektu.
  • w [9] – wygenerowany projekt. Tworzona przez nas usługa internetowa będzie korzystać z pliku EJB z poprzedniego projektu [10]. W związku z tym musi ona odwoływać się do pliku .jar modułu EJB [10].
  • W projekcie [11] dodajemy projekt NetBeans do bibliotek projektu internetowego [12]
  • w [13] należy wybrać folder modułu EJB w systemie plików i zatwierdzić.
  • w [14] moduł EJB został dodany do bibliotek projektu internetowego.

W pliku [15] implementujemy usługę internetową za pomocą następującej klasy [WsDaoJpa]:

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

  @EJB
  private IDaoLocal dao;

   // lista klientów
  @WebMethod
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

   // lista lekarzy
  @WebMethod
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

   // lista terminów wizyt u danego lekarza
   // lekarz: dany lekarz
  @WebMethod
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

   // lista wizyt u danego lekarza w danym dniu
   // lekarz: dany lekarz
   // dzień: dzień
  @WebMethod
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

   // dodanie wizyty
   // dzień: dzień wizyty
   // przedział czasowy: przedział czasowy wizyty
   // pacjent: pacjent, dla którego umówiono wizytę
  @WebMethod
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

   // usunięcie terminu spotkania
   // spotkanie: usunięte spotkanie
  @WebMethod
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

   // pobranie danego klienta
  @WebMethod
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

   // pobranie danych określonego lekarza
  @WebMethod
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

   // pobranie określonej wizyty
  @WebMethod
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

   // pobierz określony termin
  @WebMethod
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
}
  • W wierszu 4 klasa [WsdaoJpa] implementuje interfejs [IDao]. Przypomnijmy, że interfejs ten jest zdefiniowany w archiwum EJB warstwy [dao] w następującej postaci:
package rdvmedecins.dao;
...
public interface IDao {

   // lista klientów
  public List<Client> getAllClients();
   // lista lekarzy
  public List<Medecin> getAllMedecins();
   // lista terminów u lekarza
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // lista wizyt u lekarza w danym dniu
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // znalezienie klienta na podstawie jego identyfikatora
  public Client getClientById(Long id);
   // znalezienie klienta na podstawie jego identyfikatora
  public Medecin getMedecinById(Long id);
   // znalezienie wizyty na podstawie jej identyfikatora
  public Rv getRvById(Long id);
   // znalezienie przedziału czasowego na podstawie identyfikatora
  public Creneau getCreneauById(Long id);
   // dodaj RV
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // usunięcie RV
  public void supprimerRv(Rv rv);
}
  • wiersz 3: adnotacja @WebService sprawia, że klasa [WsDaoJpa] staje się usługą internetową.
  • wiersze 6–7: odwołanie do EJB warstwy [dao] zostanie wstrzyknięte przez serwer aplikacji do pola w wierszu 7. Należy pamiętać, że w ten sposób zawsze wstrzykiwana jest implementacja lokalna (w tym przypadku IDaoLocal). Wstrzyknięcie to jest możliwe, ponieważ usługa internetowa działa w tej samej maszynie JVM co komponent EJB.
  • Wszystkie metody serwisu internetowego są oznaczone adnotacją @WebMethod, aby były widoczne dla klientów zdalnych. Metoda nieoznaczona adnotacją @WebMethod byłaby metodą wewnętrzną serwisu internetowego i nie byłaby widoczna dla klientów zdalnych. Każda metoda M serwisu internetowego ogranicza się do wywołania odpowiadającej jej metody M z EJB wstrzykniętego w linii 7.

Utworzenie tej usługi internetowej znajduje odzwierciedlenie w nowej gałęzi projektu NetBeans:

W [1] widoczna jest usługa internetowa WsDaoJpa, a w [2] metody, które udostępnia ona klientom zdalnym.

Przypomnijmy architekturę tworzonej usługi internetowej:

Komponenty serwisu internetowego, które zamierzamy wdrożyć, to:

  • [1]: moduł internetowy, który właśnie stworzyliśmy
  • [2]: moduł EJB, który stworzyliśmy na poprzednim etapie i od którego zależy usługa internetowa

Aby wdrożyć je razem, należy połączyć oba moduły w projekcie NetBeans określanym jako „korporacyjny”:

W projekcie [1] tworzymy nowy projekt korporacyjny o nazwie [2, 3].

  • w [4,5] nadajemy nazwę projektowi i określamy katalog, w którym zostanie utworzony
  • w [6] wybiera się serwer aplikacji, na którym zostanie wdrożona aplikacja korporacyjna
  • w [7]: projekt korporacyjny może składać się z trzech komponentów: aplikacji internetowej, modułu EJB oraz aplikacji klienckiej. W tym przypadku projekt jest tworzony bez żadnych komponentów. Zostaną one dodane w późniejszym terminie.
  • w [8] – nowo utworzona aplikacja korporacyjna.
  • w [9], kliknij prawym przyciskiem myszy na [Java EE Modules] i dodaj nowy moduł
  • w [10] wyświetlane są wyłącznie moduły NetBeans aktualnie otwarte w IDE. W tym miejscu wybieramy moduł internetowy [serveur-webservice-1-ejb-dao-jpa-hibernate] oraz moduł EJB [serveur-ejb-dao-jpa-hibernate], które stworzyliśmy.
  • W [11] widoczne są oba moduły dodane do projektu korporacyjnego.

Pozostaje nam wdrożyć tę aplikację korporacyjną na serwerze GlassFish. W dalszej kolejności należy uruchomić SGBD i MySQL, aby zapewnić dostęp do źródła danych JDBC „jdbc/dbrdvmedecins” wykorzystywanego przez moduł EJB.

  • w [1] uruchamia się serwer Glassfish
  • jeśli moduł EJB [serveur-ejb-dao-jpa-hibernate] jest wdrożony, należy go wyładować za pomocą [2]
  • na [3], wdraża się aplikację korporacyjną
  • w [4] – aplikacja została wdrożona. Widać, że zawiera ona dwa moduły: Web i EJB.

4.10.2. Projekt NetBeans – wersja 2

Pokażemy teraz, jak wdrożyć usługę internetową, gdy nie dysponujemy kodem źródłowym modułu EJB, a jedynie jego archiwum .jar.

Nowy projekt NetBeans dla serwisu internetowego będzie wyglądał następująco:

Istotne elementy projektu to:

  • [1]: usługa internetowa jest zaimplementowana przez projekt NetBeans typu [Web Application].
  • [2]: usługa internetowa jest zaimplementowana przez omówioną już klasę [WsDaoJpa]
  • [3]: archiwum EJB warstwy [dao], które umożliwia klasie [WsDaoJpa] dostęp do definicji różnych klas, interfejsów i encji warstw [dao] oraz [jpa].

Następnie tworzymy projekt korporacyjny niezbędny do wdrożenia usługi internetowej:

  • [1], tworzymy aplikację korporacyjną [ea-rdvmedecins], początkowo bez żadnego modułu.
  • w [2] dodajemy poprzedni moduł internetowy [serveur-webservice-ejb-dao-jpa-hibernate]
  • W pliku [3] znajduje się wynik.

W obecnej postaci aplikacja korporacyjna [ea-rdvmedecins] nie może zostać wdrożona na serwerze Glassfish z poziomu NetBeans. Pojawia się błąd. Należy zatem ręcznie wdrożyć archiwum EAR aplikacji [ea-rdvmedecins]:

  • Archiwum [ea-rdvmedecins.ear] znajduje się w folderze [dist] [2] w zakładce [Files] programu NetBeans.
  • W tym archiwum [3] znajdują się dwa elementy aplikacji korporacyjnej:
  • archiwum EJB o nazwie [serveur-ejb-dao-jpa-hibernate]. Archiwum to znajduje się w archiwum, ponieważ stanowiło część bibliotek, do których odwołuje się usługa internetowa.
  • archiwum serwisu internetowego [serveur-webservice- ejb-dao-jpa-hibernate].
  • Archiwum [ea-rdvmedecins.ear] składa się z archiwum Build oraz [4] aplikacji korporacyjnej.
  • W przypadku archiwum [5] operacja wdrożenia kończy się niepowodzeniem.

Aby wdrożyć archiwum [ea-rdvmedecins.ear] aplikacji korporacyjnej, postępujemy tak, jak pokazano podczas wdrażania archiwum EJB [serveur-ejb-dao-jpa-hibernate.jar] w punkcie 4.2. Ponownie korzystamy z internetowego klienta administracyjnego serwera GlassFish. Nie powtarzamy już opisanych kroków.

Najpierw „wycofamy” aplikację korporacyjną wdrożoną w punkcie 4.10.1:

  • [1]: wybierz gałąź [Enterprise Applications] serwera Glassfish
  • w [2] wybierz aplikację korporacyjną do odinstalowania, a następnie w [3] odinstaluj ją
  • w [4] aplikacja korporacyjna została wyładowana
  • w [1] wybierz gałąź [Enterprise Applications] serwera Glassfish
  • w [2], wdróż nową aplikację korporacyjną
  • w [3], wybierz typ [Enterprise Application]
  • w [4], wskaż plik .ear projektu NetBeans [ea-rdvmedecins]
  • w [5] wdróż ten archiwum
  • na [6], aplikacja została wdrożona
  • jako [7], usługa internetowa [WsDaoJpa] pojawia się w gałęzi [Web Services] serwera Glassfish. Należy ją wybrać.
  • W [8] dostępne są różne informacje o usłudze internetowej. Najbardziej interesująca dla klienta jest informacja [9]: adres URI usługi internetowej.
  • W [10] można przetestować usługę internetową
  • W [11] znajduje się adres URI usługi internetowej , do którego dodano parametr ?tester. Ten adres URI wyświetla stronę testową. Wszystkie metody (@WebMethod) udostępniane przez usługę internetową są wyświetlane i można je przetestować. W tym przypadku testujemy metodę [13], która pobiera listę klientów.
  • W przypadku [14] przedstawiamy jedynie fragment strony z odpowiedzią. Widać jednak, że metoda getAllClients rzeczywiście zwróciła listę klientów. Zrzut ekranu pokazuje, że wysyła ona swoją odpowiedź w formacie XML.

Usługa internetowa jest w całości opisana w pliku o nazwie XML, zwanym plikiem WSDL:

  • w pliku [1] w internetowym narzędziu administracyjnym serwera Glassfish, wybierz usługę internetową [WsDaoJpa]
  • na [2], kliknij link [View WSDL]
  • na [3]: adres URI pliku WSDL. Jest to ważna informacja, którą należy znać. Jest ona niezbędna do skonfigurowania klientów tej usługi internetowej.
  • w [4] – opis usługi internetowej XML. Nie będziemy komentować tej złożonej treści.

4.10.3. Testy serwisu internetowego JUnit

Tworzymy projekt w NetBeans, aby „odtworzyć” testy przeprowadzone wcześniej z klientem EJB, tym razem z klientem dla niedawno wdrożonej usługi internetowej. Postępujemy tutaj analogicznie do procedury opisanej w paragrafie 14.2.1, na stronie 115 dokumentu [ref1].

  • w [1], klasyczny projekt Java
  • w [2] – klasa testowa
  • w pliku [3] klient korzysta z archiwum EJB, aby uzyskać dostęp do definicji interfejsu warstwy [dao] oraz encji JPA. Przypominamy, że archiwum to znajduje się w podfolderze [dist] w folderze modułu EJB.

Aby uzyskać dostęp do zdalnej usługi internetowej, konieczne jest wygenerowanie klas proxy:

Na powyższym schemacie warstwa [2] [C=Client] komunikuje się z warstwą [1] [S=Serveur]. Aby nawiązać komunikację z warstwą [S], klient [C] musi utworzyć połączenie sieciowe z warstwą [S] i komunikować się z nią zgodnie z określonym protokołem. Połączenia sieciowe to połączenia typu TCP, a protokołem transportowym jest HTTP. Warstwa [S], reprezentująca usługę internetową, jest zaimplementowana przez serwlet Java uruchamiany przez serwer Glassfish. Nie napisaliśmy tej serwletu. Jej generowanie jest zautomatyzowane przez Glassfish na podstawie adnotacji @Webservice i @WebMethod w klasie [WsDaoJpa], którą napisaliśmy. Podobnie zautomatyzujemy generowanie warstwy [C] po stronie klienta. Warstwę [C] nazywa się czasem warstwą proxy zdalnej usługi internetowej, przy czym termin proxy oznacza element pośredniczący w łańcuchu oprogramowania. W tym przypadku proxy C jest pośrednikiem między klientem, który zamierzamy napisać, a usługą internetową, którą wdrożyliśmy.

W programie NetBeans 6.5 serwer proxy C można wygenerować w następujący sposób (aby kontynuować, usługa internetowa musi być aktywna na serwerze GlassFish):

  • w [1], dodaj nowy element do projektu Java
  • w [2], wybierz gałąź [Web services]
  • w [3], wybierz [Web Service Client]
  • w [4] należy podać adres URI pliku WSDL usługi internetowej. Adres ten został przedstawiony w punkcie 4.10.2.
  • w polu [5] należy pozostawić wartość domyślną [JAX-WS]. Inną możliwą wartością jest [JAX-RPC]
  • po zatwierdzeniu kreatora tworzenia proxy serwisu internetowego projekt NetBeans został wzbogacony o gałąź [Web Service References] [6]. Gałąź ta przedstawia metody udostępniane przez zdalny serwis internetowy.
  • w zakładce [Files] [7] dodano kod źródłowy w języku Java [8]. Odpowiada on wygenerowanemu proxy w języku C.
  • W pliku [9] znajduje się kod jednej z klas. Widać tam, że zostały one umieszczone w pakiecie [rdvmedecins.ws]. Nie będziemy komentować kodu tych klas, który ponownie jest dość złożony.

W przypadku klienta Java, który właśnie tworzymy, wygenerowany proxy C pełni rolę pośrednika. Aby uzyskać dostęp do metody M zdalnej usługi internetowej, klient Java wywołuje metodę M proxy C. W ten sposób klient Java wywołuje metody lokalne (wykonywane w tej samej maszynie JVM), a wywołania te są dla niego w sposób przezroczysty przekształcane na wywołania zdalne.

Pozostaje nam jeszcze dowiedzieć się, jak wywoływać metody M proxy w języku C. Wróćmy do naszej klasy testowej JUnit:

W klasie [1] klasa testowa [MainTestsDaoRemote] jest tą samą, która została już wykorzystana podczas testowania EJB warstwy [dao]:

package dao;
...
public class MainTestsDaoRemote {

   // warstwa [dao] przetestowana
  private static IDaoRemote dao;

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

  @Test
  public void test1() {
...
  }
}
  • W wierszu [13] test test1 pozostaje bez zmian.
  • w wierszu [9] usunięto zawartość metody [init].

Na tym etapie projekt zawiera błędy, ponieważ metoda testowa [test1] wykorzystuje jednostki [Client], [Medecin], [Creneau], [Rv], które nie znajdują się już w tych samych pakietach co wcześniej. Znajdują się one w pakiecie wygenerowanego proxy C. Należy usunąć odpowiednie instrukcje import i wygenerować je ponownie za pomocą operacji „Fix Imports”.

Wróćmy do kodu klasy testowej [MainTestsDaoRemote]:

package dao;
...

public class MainTestsDaoRemote {

   // warstwa [dao] przetestowana
  private static IDaoRemote dao;

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

Metoda [init] w wierszu 10 musi zainicjować odwołanie do warstwy [dao] z wiersza 7. Musimy wiedzieć, jak wykorzystać wygenerowany proxy C w naszym kodzie. NetBeans pomaga nam w tym zadaniu.

  • W [1] należy zaznaczyć metodę [getAllClients] usługi internetowej za pomocą myszy, a następnie przeciągnąć tę metodę i upuścić ją w metodzie [init] klasy testowej.

Otrzymujemy wynik [2]. Ten szkielet kodu pokazuje, jak korzystać z wygenerowanego proxy C:

1
2
3
4
5
6
7
8
9
    try { // Wywołanie operacji usługi internetowej
      rdvmedecins.ws.WsDaoJpaService service = new rdvmedecins.ws.WsDaoJpaService();
      rdvmedecins.ws.WsDaoJpa port = service.getWsDaoJpaPort();
       // TODO przetwarzaj wynik tutaj
      java.util.List<rdvmedecins.ws.Client> result = port.getAllClients();
      System.out.println("Result = "+result);
    } catch (Exception ex) {
       // TODO obsługa niestandardowych wyjątków tutaj
}
  • Wiersz [5] pokazuje, że metoda [getAllClients] jest metodą obiektu typu [WsDaoJpa] zdefiniowanego w wierszu 3. Typ [WsDaoJpa] jest interfejsem zawierającym te same metody, co te udostępniane przez zdalną usługę internetową.
  • W wierszu [3] obiekt [WsDaoJpa port] jest uzyskiwany z innego obiektu typu [WsDaoJpaService] zdefiniowanego w wierszu 2. Typ [WsDaoJpaService] reprezentuje lokalnie wygenerowany proxy w języku C.
  • Dostęp do zdalnej usługi internetowej może się nie powieść, dlatego cały kod jest otoczony blokiem try/catch.
  • Obiekty proxy C znajdują się w pakiecie [rdvmedecins.ws]

Po zrozumieniu tego kodu widać, że lokalny identyfikator zdalnej usługi internetowej można uzyskać za pomocą kodu:

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

Kod klasy testowej JUnit przyjmuje wówczas następującą postać:

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 {

   // warstwa [dao] przetestowana
  private static WsDaoJpa dao;

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

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

   // metoda pomocnicza – wyświetla elementy kolekcji
  private static void display(String message, List elements) {
 ...
  }
}

Jesteśmy teraz gotowi do przeprowadzenia testów:

W [1] uruchamiany jest test JUnit. W [2] test zakończył się powodzeniem. Jeśli przyjrzymy się komunikatom wyświetlanym w konsoli NetBeans, znajdziemy takie wiersze jak poniżej:

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

Po stronie serwera jednostka [Client] posiada metodę toString, która wyświetla różne pola obiektu typu [Client]. Podczas automatycznego generowania proxy C, encje są tworzone w proxy C, ale zawierają wyłącznie pola prywatne wraz z odpowiadającymi im metodami get / set. W związku z tym metoda toString nie została wygenerowana w encji [Client] proxy C. To wyjaśnia poprzedni wynik wyświetlania. Nie ma to jednak wpływu na wynik testu JUnit: test zakończył się powodzeniem. Od tej pory uznamy, że usługa internetowa działa poprawnie.