<

14. Lekkie kontenery, 

wzorzec "dependency injection" i EJB 3.0


Zapoznamy się teraz z koncepcją lekkich kontenerów, wzorcem projektowym "dependency injection" oraz - m.in. opartymi na nich elementami specyfikacji EJB 3.0 oraz Java Persistance API.

Będzie to naturalnie tylko wprowadzenie do rozległej tematyki EJB 3.0, ale za to nastawione praktycznie - będziemy testować przykładowe programy w środowisku pierwszego serwera aplikacji implementującego specyfikację EJB 3.0 - Sun Java Application Server 9.


1. "Loose coupling", wzorzec "dependency injection" i lekkie kontenery

Przy pisaniu programów, szczególnie dużych, składających się z wielu części i działających w środowiskach rozproszonych konieczne jest dbanie o "loose coupling" - czyli słabą współzależność poszczególnych modułów systemu, a nawet fragmentów kodu.

"Loose coupling" jest  właściwością systemu komputerowego, która oznacza, że jego poszczególne moduły mogą być traktowane w dużym stopniu niezależnie i ich kody nie są ze sobą ściśle powiązane



Programowanie w kategoriach interfejsów daje nam pewną niezależność, ale nie całkowitą.
Np. taki kod:

List list = new ArrayList();

zależy od klasy ArrayList i nie spełnia warunku "loose coupling"

Sytuację poprawia zastosowanie fabryk:

List list = ListFactory.createList();

Ten kod nie zależy od konkretnej implementacji listy. Fabryka może dostarczyć dowolnej implementacji (często zależnej od kontekstu), a program nie musi czynić żadnych założeń co do tej implementacji.
Zwróćmy jednak uwagę, że musimy odwołać się do konkretnej fabryki i w tym sensie kod nie jest w pełni niezależny.

O konkretnej implementacji chcielibyśmy przesądzać w trakcie wdrażania aplikacji (a nie pisania kodu). Być może tę konkretną implementację (właściwą dla środowiska, w którym wykonuje się program) zna tylko kontener, w którym program jest wdrażany. Być może w różnych kontenerach implementacje są różne.
Nie powinniśmy więc od tego uzależniać naszego kodu.
Programujmy tylko w kategoriach interfejsów, a zadanie tworzenia i dostarczenia obiektów klas będących konkretnymi implementacjami tych interfejsów odłóżmy do momentu wdrożenia aplikacji i obarczmy nim kontener, w którym aplikacja jest wdrażana.
Jest to zasadniczy sens zastosowania wzorca "dependency injection".

 
Wzorzec "dependency injection" polega na przeniesieniu procesu tworzenia obiektu i łączenia go z referencjami typów interfejsu poza kod programu. Odpowiednie referencje (do obiektów klasy implementującej dany interfejs) są wstrzykiwane w kod programu w fazie wdrożenia lub nawet wykonania przez środowisko zewnętrzne, zazwyczaj kontener, w sposob domyślny lub na podstawie informacji z plików wdrożeniowych.

Ogólnie, iniekcje mogą być dokonywane:

Iniekcja w pole:

List list;  // po iniekcji list wskazuje na obiekt implementujący interfejs List
            // trzeba jeszcze powiedzieć jakoś, że w tym miejscu jest potrzebna iniekcja 
Iniekcja za pośrednictwem konstruktora:

class A {

   private List list;

   public A(List l) {    // iniekcja dokonana za pośrednictwem konstruktora
      list = l;          // konstruktorowi przekazywany jest obiekt klasy implementującej List
   }

}

Iniekcja za pomocą settera:

class A {

   private List list;

   public void setList(List l) {    // iniekcja poprzez setter
      list = l;
   }

}

Wzorzec "dependency injection" od jakiegoś już czasu stosowany jest w tzw. lekkich konenerach.
Należą do nich m.in.
PicoContainer
NanoContainer
Spring

Proszę odwiedzić powyższe linki i przeczytać o tych środowiskach i sposobie realizacji w nich wzorca "dependency injection".

Lekkie kontenery nazywane są lekkimi, ponieważ w przeciwieństwie do kontenerów EJB 2.1 pozwalają na programowanie za pomocą zwykłych klas Javy (POJO - "plain old Java objects") i nie wymagają od programisty używania specyficznych, niejako "technicznych" konstrukcji, związanych z "wnętrznościami" kontenera (np. w rodzaju interfejsu Home, klasy EJBObject itp.). Ta lekkość osiągana jest m.in. za pomocą zastosowania wzorca "dependency injection".
 


Wspomniane "lekkie kontenery" różnie realizują wzorzec "dependency injection". Np. dla PicoContainera trzeba dostarczyć kodów (programików) konfigurujących. W Spring Application Framework mamy pliki konfiguracyjne XML.

Po wprowadzeniu w Javie 1.5 mechanizmu adnotacji pojawiła się również możliwość oznaczania punktów iniekkcji za pomocą opdowiednich adnotacji.
Jest to główny (choć nie jedyny możliwy) mechanizm stosowany w EJB 3.0, która to specyfikacja czyni wreszcie kontenery EJB lekkimi i zmacznie ułatwia programowanie aplikacji J2EE.



2. EJB 3.0 - przegląd

Dotychczasowy model EJB (zob. poprzedni wykład) był:

Specyfikacja EJB 3.0 pozwala na zastosowanie w programowaniu klas POJO. Za pomocą mechanizmu "dependency injection" i z wykorzystaniem adnotacji umożliwia łatwiejsze i bardziej elastyczne (m.in. usuwające zależności od klas kontenerowych i deskryptorów wdrożenia) tworzenie i wdrazanie aplikacji.


EJB 3.0 = POJO + adnotacje + dependency injection



Praktycznie w nowym modelu znikają (można nie używać):

Interfejsy są POJO!
Obiekty (EJB) są POJO!
Te POJO są zarządzane przez kontener (czasem na zasadzie wzorca dependency injection), z dodatkową, niewidoczną dla programisty, autogeneracją niezbędnych łączników, obiektów pośredniczących itp.
Jak ma się to dziać - określają w sposób deklaratywny adnotacje, ew. można też użyć deskryptorów wdrożenia lub stosowac obie te metody równolegle.

Interfejsy biznesowe

W nowym modelu posługujemy się "czystymi" (POJO) interfejsami biznesowymi.
Przywołując omówiony wcześniej przykład kalkulatora podatkowego zamiast nienaturalnego

import javax.ejb.EJBObject;
import java.rmi.RemoteException;
import java.math.*;

public interface Pit extends EJBObject {
    public BigDecimal taxToPay(BigDecimal income) throws RemoteException;
}

możemy teraz pisać:

import java.math.BigDecimal;
import javax.ejb.Remote;

@Remote
public interface Pit {
  public BigDecimal taxToPay(BigDecimal income);
}

Adnotacja @Remote mówi o tym, że interfejs posłuży zdalnym obiektom. Bez niej (lub z użyciem adnotacji @Local)  będziemy mieli obiekty lokalne.

Warto zwrócić uwagę, że to jest prawdziwy interfejs biznesowy, implementowany w klasie biznesowego ziarna. Programowanie odbywa się dalej w kategoriach tego interfejsu, a nie interfejsu Home.

EJB 3.0 - komponenty sesyjne

Sesyjne komponenty EJB:

Zamiast:

import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import java.math.*;


public class PitBean implements SessionBean {

    private BigDecimal taxRate = new BigDecimal("0.14");

    public PitBean() {
    }

    public BigDecimal taxToPay(BigDecimal income) {
        BigDecimal result = income.multiply(taxRate);

        return result.setScale(2, BigDecimal.ROUND_UP);
    }


    // Metoda wołana przy tworzeniu obiektu ziarna
    public void ejbCreate() {
    }

    // Metody interfejsu SessionBean

    public void ejbRemove() {
    }

    public void ejbActivate() {
    }

    public void ejbPassivate() {
    }

    public void setSessionContext(SessionContext sc) {
    }
}

piszemy teraz znacznie krócej i bardziej naturalnie, używając adnotacji @Stateless:

package pit.ejb;
import java.math.BigDecimal;
import javax.ejb.*;


@Stateless
public class PitBean implements Pit {
  private BigDecimal taxRate = new BigDecimal("0.14");

  public PitBean() {
  }

  public BigDecimal taxToPay(BigDecimal income) {
      BigDecimal result =  income.multiply(taxRate);
      return result.setScale(2, BigDecimal.ROUND_UP);

  }

}

Klasa komponentu może implementować od razu dwa interfejsy - zdalny i lokalny.
Ponadto, jeśli nie opatrzymy interfejsu adnotacją @Remote, to o "zdalności" obiektu możemy zdecydowac przy implementacji:

public interface Pit {
//...
}

@Stateless @Remote
public PitBean implements Pit {
// ...
}
Niektóre serwery aplikacji (JBoss, Resin)  potrafią także generować niezbędne interfejsy na podstawie adnotacji @BusinessMethod w klasie komponentu.
Uwaga: autogeneracja interfejsó biznesowych nie jest najlepszym pomysłem i należy tego unikać, bowiem jesteśmy wtedy przywiązani do wygenerowanych nazw.

Po wdrożeniu EJB automatycznie generowane są obiekty posredniczące i klient ma do nich dostęp m.in.  poprzez JNDI. Wyszukiwanie jest uproszczone, nie ma też męczącego rzutowania za pomocą metody narrow(...). Nie trzeba dostarczać żadnych deskryptoró wdrożenia!

Zobaczmy.
Dotychczas musieliśmy pisać tak.
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import java.math.BigDecimal;

public class PitClient {
    public static void main(String[] args) {
        try {
            Context initial = new InitialContext();
            Context myEnv = (Context) initial.lookup("java:comp/env");
            Object objref = myEnv.lookup("ejb/Pit");

            PitHome home =
                (PitHome) PortableRemoteObject.narrow(objref,
                    PitHome.class);

            Pit pit = home.create();

            BigDecimal income = new BigDecimal("100000.00");
            BigDecimal tax = pit.taxToPay(income);

            System.out.println("Podatek wynosi: " + tax);

            System.exit(0);

        } catch (Exception ex) {
            System.err.println("Caught an unexpected exception!");
            ex.printStackTrace();
        }
    }
}
I na dodatek dostarczyć odpowiedniego deskryptora wdrożenia.

Teraz posługujemy się bezpośrednio interfejsem biznesowym (bez interfejsu Home), nie musimy używac metody create() ani też pisać czy generować deskryptorów:

Pit pit;
// .... 

     try {
        InitialContext ic = new InitialContext();
        pit = (Pit) ic.lookup("pit.ejb.Pit");
      } catch (NamingException e) {
        e.printStackTrace();
      }
      BigDecimal income = new BigDecimal("100000.00");
      BigDecimal tax = pit.taxToPay(income);
      System.out.println("Podatek wynosi: " + tax);
Zwróćmy uwagę, że forma nazwy JNDI dla zdalnego obiektu biznesowego jest zależna od serwera aplikacji. W podanym przykładzie pit.ejb.Pit jest nazwą automatycznie generowaną przez Sun Application Server 9 dla typu Pit umieszczonego w pakiecie pit.ejb.

Pojawiają się  głosy, że zastosowanie JNDI nie jest najlepszym rozwiązaniem, bo:
Z poziomu klientów działających w kontenerach WEB oraz kontenerach klienckich można - zamiast JNDI - zastosowac adnotację @EJB. W tym miejscu zadziała mechanizm "dependency injection", a oprogramowanie klienta staje się jeszcze prostsze, a przy tym przenośne pomiędzy różnymi serwerami aplikacji.
Np.
public class PitClient  {

  @EJB
  private static Pit pit;    // iniekcja obiektu zdalnego

  public static void main(String[] args) {
      BigDecimal income = new BigDecimal("100000.00");
      BigDecimal tax = pit.taxToPay(income);
      System.out.println("Podatek wynosi: " + tax);
  }

}
Uwaga: powyżej pole pit musi być statyczne, ponieważ iniekcja następuje podczas wywołania statycznej metody main.

Zarządzanie cyklem życia EJB

W klasycznym EJB do zarządzania cyklem życia komponentów są stosowane metody typu callback (implementacja interfejsu SessionBean).
Nawet jeśli ich nie potrzeba - to musimy dać puste implementacje.
W EJB 3.0 - każda metoda w klasie EJB (POJO) może być typu "life cycle management callback", jeśli opatrzymy ją odpowiednią adnotacją.
Rodzaje adnotacji.
Iniekcje zasobów

Zasoby (takie jak zasoby bazodanowe, konteksty sesji, kolejki JMS) można definiowac za pomocą adnotacji @Resource. Powoduje to "wstrzyknięcie" odpowiednich referencji, pozwala unikać stosowania metody lookup(...) i rzutowania, a także ogranicza lub nawet całkowiecie eliminuje  koniecznośc stosowania deskryptorów wdrożenia.

Przykłady:
@Resource
SessionContext ctx;


@Resource (name="jdbc/SomeDB")
DataSource myDb;

@Resource (name="ConnectionFactory")
QueueConnectionFactory factory;

@Resource (name="queue/A")
Queue queue;

Iniekcje za pośrednictwem setterów

Adnotując metody set (dla pól) uzyskujemy ten sam efekt co przy adnotacji pól. Np.

@Resource(name="jdbc/JakasBD")
public void setDataSource(DataSource ds)
   this.ds = da;
}
To samo dotyczy adnotacji @EJB.

Programowanie aspektowe

W EJB 3.0 dostępna jest prosta forma programowania aspektowego. Dzięki tzw. interceptorom możemy przechwytywać wywołania metod komponentów EJB, sprawdzać lub zmieniać ich argumenty, modyfikowac wyniki.

Można np. adnotować klasę bezstanowego komponentu sesyjnego za pomocą adnotacji @Interceptors, podając klasy odpowiedzialne za przechwytywanie wywołań metod komponentu.

@Stateless
@Interceptors{ (Test.class) }
public class SlesBean implements BusInterf {
//...
}
W klasach tych metody przechwytujące oznaczamy adnotacją @AroundInvoke.
Metody takie - jako argument - powinny mieć referencję do obiektu typu InvocationContext i powinny zwracać wynik typu Object.
public class Test {

  @AroundInvoke
  public Object testArgsAndAddTxtToResult(InvocationContext ic) throws Execption {
  // ...
  }  
Interfejs InvocationContext dostarcza metod manipulacji na argumentach wywołania i otrzymanych wynikach.

EJB 3.0 - komponenty encyjne i Java Persistance API

Dzięki rozbudowanemu Java Persistance API (korzystającego z najlepszych doświadczeń Hibernate, JDO i TopLInk) niezwykle łatwe staje się odzworowanie obiektowo-relacyjne (O/R mapping).

Za pomocą odpowiednich adnotacji komponenty encyjne "od razu" definiują tabele w bazie danych. Np.
@Entity
@Table(name="SCHOOL_STUDENT")   // nazwa tabeliw  bazie danych

public class Student implements Serializable {

  private String id;
  private String name;

  public Student() {
  }

  public Student(String index, String name) {
    this.id = index;
    this.name = name;
  }


  @Id                        // klucz główny
  public String getId() {
    return id;
  }

  public void setId(String index) {
    this.id = index;
  }



  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

}
Serwer aplikacji odwzoruje tabelę SCHOOL_STUDENT z domyślnymi nazwami kolumn ID i NAME na obiekty klasy Student. A nawet - przy odpowiednio ustawionych opcjach - utworzy taką tabelę jeśli jej nie ma. Możemy też dostosować nazwy kolumn za pomocą odpowiednich adnotacji @Column stawianych przed getterami.

Zwróćmy uwagę, że Entity Bean jest tu klasą POJO, może spokojnie dziedziczyć inne klasy, implementować dowolne interfejsy itp. Co więcej - obiekty tej klasy mogą być dostępne po stronie klienta, co w starym modelu nie było możliwe i powodowało konieczność stosowania tzw. Transfer Object (tylko po to, by udostępnić wartości odczytane z bazy). Tutaj wystarczy zapewnić serializację komponentu encyjnego by był on dostępny zdalnie po stronie klienta.

Relacje pomiędzy tabelami wprowadzamy za pomocą adnotacji:
@ManyToMany, @ManyToOne, @OneToOne, @OneToMany
a także z uzyciem połączeń tabel za pośrednictwem adnotacji @JoinTable.
(przykłady będą omówione dalej).

Komponentami encyjnymi zarządza tzw. EntityManager. Zestaw zarządzanych komponentów nazywa się kontekstem persystencji (Persistence Context). Aby uzyskać dostęp do zarządcy stosujemy iniekcję:

@PersistenceContext
EntityManager em;

Obiekty utrwalamy w bazie za pomoca metody persist() EntityManagera, usuwamy za pomocą metody remove(). Na przykład:

@PersistenceContext EntityManager em;
// ...

Student s = new Student("s0001", "Kowalski Jan");
em.persist(s);

// ....
Student p =  em.find(Student.class, "s0001");
em.remove(p);


Uwaga: pominęto obsługę wyjątków

Przy usuwaniu najpierw za pomocą find wyszukujemy w bazie odpowiedni obiekt.
Oczywiście - find ma ogólne zastosowanie - wyszukiwania obiektu po kluczu głównym.

Bardziej zaawansowane sposoby przeszukiwania bazy danych wymagają użycia tzw. Java Persistance Query Language (JPQL). Jest on podobny składniowo do instrukcji SELECT, ale działa na obiektach i pozwala nawigować pomiędzy obiektami. EntityManager - za pomocą metod - createQuery(...) i createNamedQuery(...) pozwala tworzyć i wykonywac dynamiczne i statyczne zapytania.
Np.
public List<Student> findStudents(String name) {
  return em.createQuery(
    "SELECT s FROM Student s WHERE s.name LIKE :studName")
       .setParameter("studName", name)
       .getResultList();
}
Tutaj createQuery() tworzy obiekt typu Query. Jest to zapytanie z parametrem o nazwie studName. Za pomocą metody setParameter (wołanej na rzecz Query) ustalamy wartość parametru. Metoda getResultList() zwraca listę wyników zapytania.

Zapytania nazwane ("namedQueries") są statyczne. Ich zaletą jest to, że podlegają składniowej weryfikacji w fazie wdrożenie. Mogą być także prekompilowane.
Definiujemy je (w klasach komponentó encyjnych) za pomocą adnotacji @NamedQuery lub @NamedQueries np.
@NamedQuery(
  name="getAllStudents",
  query="SELECT s FROM Student s"
)
i używamy za pomocą metody createNamedQuery EntityManagera, np.:

List<Student> result = em.createNamedQuery("getAllStudents").getResultList();

Aby korzystać z EntityManagera musimy dostarczyć plik persistance.xml, który definiuje tzw. jednostkę persystencji (persistence unit). Zwykle będzie to dość prosty plik wdrożeniowy, który określa rodzaj transakcji, nazwę JNDI dla źródła danych oraz ew. dodatkowe właściwości (np. czy tabele mają być generowane automatycznie na podsatwie definiocji komponentów encyjnych). Plik taki oraz jego umiejscowienie zostanie  pokazany w dalszych przykładach.

Więcej informacji o EJB 3.0 i Java Persistane API zobacz:
J2EE 5 Tutorial
EJB 3.0 Specification
Java Persistance API Specification

3. Praktyka EJB 3.0 i Sun Java System Application Server 9

Aby móc praktycznie  wypróbować działanie elementów EJB 3.0 skorzystamy z pierwszego serwera aplikacji, który implementuje finalną specyfikację EJB 3.0. Jest to Sun Javav System Application Server 9. Powstał on dosyć szybko dzięki temu, że kod poprzednich wersji serwera Sun został otwarty i przekazany do projektu GlassFish (zob. o projekcie GlassFish).

Pokażę jednocześnie jak z poziomu Eclipse można w łatwy sposób pisać i wdrażać aplikacje w środowsiku SJAS 9.

Przede wszystkim po to by programy kompilowały się w Eclipse należy dodać odpowiednie JARy z katalogu lib serwrera. Można to zrobić definiując w Workspace bibliotekę, Nazwałme ją j2ee5. Tworząc nowy projekt dodamy ją do niego.

r


 
Będziemy tworzyć prosty sesyjny komponent bezstanowy PitBean - kalkulator pit.
Źródla umieścimy w katalogu src
Źródłowy pakiet nazwiemy pit, a komponent PitBean umieścimy w podpakiecie pit.bean.
Po to by  ten komponent, jak i wolnostojący klient mieli niezależny dostęp do interfejsu biznesowego Pit umieścimy ten interfejs w podpakiecie pit.common. Pakietem klienta będzie pit.client i tam dostarczymy klasy PitClient.
Źródła będą kompilowane do katalogu bin (katalogi odpowiadające pakietom zostaną stworzone automatycznie).
r


Taka organizacja kodu źródłowego przyda się szczególnie w bardziej zaawansowanych przypadkach.
Interfejs biznesowy już znamy. Dla porządku:

package pit.common;

import java.math.BigDecimal;

public interface Pit {
  public BigDecimal taxToPay(BigDecimal income);
}
 
Sam komponent sesyjny także niczym się nie wyróżnia (warto tylko zwrócić uwagę na adnotacje):

package pit.ejb;
import java.math.BigDecimal;
import javax.ejb.*;

import pit.common.*;

@Stateless @Remote
public class PitBean implements Pit {
  private BigDecimal taxRate = new BigDecimal("0.14");

  public PitBean() {
  }

  public BigDecimal taxToPay(BigDecimal income) {
      BigDecimal result =  income.multiply(taxRate);
      return result.setScale(2, BigDecimal.ROUND_UP);
  }

}

Natomiast pisząc program kliencki przewidziano trzy możliwe tryby jego uruchomienia, regulowane przez argument metody main:
Warto w  tym miejscu odnotować, że uruchomienie klienta w kontenerze klienta wymaga zastosowania instrukcji appclient. Dostarcza ona odpowiedniego środowiska, m.in. pozwalającego na dokonanie odpowiedniej iniekcji w miejscu adnotacji @EJB. Poza kontenerem klienta (opcja JAVA_CLIENT)  iniekcja nie jest możliwa i potrzebne jest sięgnięcie do JNDI.
Stąd też w programie klienta pojawia się wywołanie metody init(), która -w  zależności od opcji - odpowiednio inicjuje referencję Pit pit.


import javax.naming.*;
import javax.swing.*;

import pit.common.*;


@SuppressWarnings("serial")
public class PitClient extends JFrame implements ActionListener {

  @EJB
  private static Pit pit;

  private JTextField intf = new JTextField(10);
  private JLabel result = new JLabel();

  public PitClient() {
    this("DEFAULT_CONTEXT");
  }

  public PitClient(String appType) {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    init(appType);
    setTitle("Tax calculator");
    intf.setBorder(BorderFactory.createTitledBorder("Income"));
    result.setPreferredSize(intf.getPreferredSize());
    result.setOpaque(true);
    result.setBackground(Color.WHITE);
    result.setForeground(Color.RED);
    result.setBorder(BorderFactory.createTitledBorder("Tax to pay"));
    setLayout(new FlowLayout());
    JButton calc = new JButton("Calc");
    add(intf);
    add(calc);
    add(result);
    intf.addActionListener(this);
    calc.addActionListener(this);
    pack();
    this.setLocationRelativeTo(null);
    setVisible(true);
  }

  public void actionPerformed(ActionEvent e) {
    result.setText("");
    BigDecimal income = null;
    String in = intf.getText().trim();
    try {
      income = new BigDecimal(in);
    } catch (NumberFormatException exc) {
        JOptionPane.showMessageDialog(null, "Invalid income: " + in);
        return;
    }
    BigDecimal tax = pit.taxToPay(income);
    result.setText(tax.toString());
  }

  private void init(String type) {
    if (type.equals("JAVA_APP")) pit = new pit.ejb.PitBean();
    else if (type.equals("JAVA_CLIENT")) {
      try {
        InitialContext ic = new InitialContext();
        pit = (Pit) ic.lookup("pit.common.Pit");
      } catch (NamingException e) {
        e.printStackTrace();
      }
    }
  }



  public static void main(String[] args) {
    if (args.length >= 1) new PitClient(args[0]);
    else new PitClient();
  }


}
Oczywiście, aplikację nie wystarczy skompilowac, trzeba ją również wdrożyć.
Wdrożenie wymaga:

Będziemy także chcieli stworzyć katalog javaclient, który będzie zawierał klasy potrzebne do działania wolnostojącego klienta (działającego poza kontenerem klienta) oraz odpowiedni skrypt uruchomieniowy (dostarczający odpowiednich bibliotek dla działania poprzez JNDI).

Wszystkie te dzialania, jak również samo wdrożenie oraz uzyskanie z serwera archiwum klienta gotowego do działania w kontenerze klienta zapiszemy jednokrotnie w skrypcie Anta. Będziemy mogli go wykorzystać dla każdej innej aplikacji.

Skrypt - na podstawie właściwości zapisanych w pliku build.props -wykonuje te wszystkie działania. Ponieważ praktycznie dla każdego projektu będziemy mieli tę samą sekwencję działań główny kod skryptu zapiszemy jednokrotnie jako defbuild.xml w katalogu common. Umięscimy tam również wspólne właściwości dla wszystkich projektów. Do każdego projektu będziemy dostarczać takiego samego, krótkiego build.xml, który wywołuje defbuild.xml.

Plik ogólnych właściwości common.properties:
javaee.home= ... tu trzeba podać ścieżkę do instalacji SJAS 9
javaee.server.name=localhost
javaee.server.port=8080
javaee.adminserver.port=4848
javaee.server.username=admin
asadmin=asadmin.bat
Plik specyficznych właściwości projektu - build.props:
app=Pit1                                 // nazwa aplikacji
pack=pit                                 // pakiet
has.ejb=yes                              // czy ma moduł EJB
has.clientjar=yes                        // czy ma JAR klienta
has.javaclient=yes                       // czy zrobić wolnostojącego klienta
has.common=yes                           // czy ma klasy dzielone pomiędzy moduły
main.class=pit.client.PitClient          // nazwa głównej klasy klienta
Generalny skrypt Ant - defbuild.xml

<!-- Konfiguracja wlasciwosci  -->
<property file="../common/common.properties"/>
<property file="build.props"/>

<target name="prepare" depends="init">
         <mkdir dir="dist"/>
         <delete>
           <fileset dir="dist" includes="**/*.jar **/*.war **/*.ear"/>
         </delete>
</target>


<target name="ejb" if="has.ejb" depends="prepare">
  <jar jarfile="dist/${app}Ejb.jar">
       <fileset dir="bin" includes="${pack}/ejb/**.*class"/>
       <fileset dir="bin/${pack}/ejb" includes="META-INF/**.*"/>
  </jar>
</target>


<target name="war" if="has.war" depends="prepare">
  <delete dir="webbuild"/>
  <mkdir dir="webbuild"/>
  <mkdir dir="webbuild/WEB-INF"/>
  <mkdir dir="webbuild/WEB-INF/classes"/>
  <copy todir="webbuild">
     <fileset dir="bin/${pack}/web" excludes="**.*class"/>
  </copy>
  <copy todir="webbuild/WEB-INF/classes">
    <fileset dir="bin" includes="${pack}/web/**.*class"/>
  </copy>
  <jar jarfile="dist/${app}.war" basedir= "webbuild"/>
</target>

<target name="client-jar" if="has.clientjar" depends="prepare">
   <manifest file="MANIFEST.MF">
     <attribute name="Main-Class" value="${main.class}"/>
   </manifest>
   <jar jarfile="dist/${app}Client.jar" manifest="MANIFEST.MF">
       <fileset dir="bin" includes="${pack}/client/**.*"/>
   </jar>
   <delete file="MANIFEST.MF"/>
</target>

<target name="prepare-lib">
  <delete dir="dist/lib"/>
  <mkdir dir= "dist/lib"/>
</target>

<target name="common" if="has.common" depends="prepare,prepare-lib">
  <jar jarfile="dist/lib/${app}Common.jar">
        <fileset dir="bin" includes="${pack}/common/**.*class"/>
  </jar>
</target>

<target name="entity" if="has.entity" depends="prepare,prepare-lib">
  <jar jarfile="dist/lib/${app}Entity.jar">
        <fileset dir="bin" includes="${pack}/entity/**.*class"/>
        <fileset dir="bin/${pack}/entity" includes="META-INF/**.*"/>
  </jar>
</target>


<target name="java-client" if="has.javaclient" depends="prepare">
   <mkdir dir="javaclient"/>
   <delete>
     <fileset dir="javaclient" includes="**/*.*"/>
   </delete>
   <copy todir="javaclient">
     <fileset dir="bin" includes="${pack}/client/**.*class"/>
     <fileset dir="bin" includes="${pack}/common/**.*class"/>
     <fileset dir="bin" includes="${pack}/entity/**.*class"/>
   </copy>
   <echo file="javaclient/runClient.bat" append="false"
     message="java -cp .;${javaee.home}/lib/appserv-rt.jar;${javaee.home}/lib/javaee.jar; ${main.class} JAVA_CLIENT"/>
</target>

<target name="dist" depends="ejb,entity,war,common,client-jar">
   <jar jarfile="dist/${app}.ear" basedir="dist"/>
</target>

<target name="deploy" depends="java-client,dist">
        <exec executable="${asadmin}">
            <arg line=" deploy "/>
            <arg line=" --user ${javaee.server.username}" />
            <arg line=" --passwordfile ../common/password.txt" />
            <arg line=" --contextroot ${app}"/>
            <arg line=" --retrieve ."/>
            <arg line="dist/${app}.ear" />
</exec>
</target>
Jak widać tworzony jest tu katalog dist, w którym umieszczane są wszystkie JARy. Tworzone są też niezbędne katalogi dla klienta itp. Zadanie dist pakuje ostatecznie niezbędne modułu do pliku EAR. Zadanie deploy uruchamia instrukcję asadmin z opcją deploy, co powoduje wdrożenie aplikacji na serwerze. Podajemy przy tym hasło zapisane w pliku password.txt, a także żadamy załadowania jaru klicnckiego (opcja retrieve).

Mając to wszystko przygotowane w łatwy sposób - z poziomu Eclipse - będziemy mogli wdrażac każdą aplikację.

Zobacz prezentację wdrażania aplikacji Pit.

Po wdrożeniu możemy sprawdzić stan komponentów za pomocą konsoli administracyjnej serwera. Warto podkreślić, że identyfikacja modułów przez serwer następuje na podstawie informacji zawartych w samych klasach lub JARach (niepotrzeben były żadne deskryptory).
Zobacz prezentację.

Klienta aplikacji możemy uruchomić teraz na trzy sposoby:

Potrzebny do WebStartu html może wyglądać na przykład tak:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="pl">
<head>
  <title>WebStart Test</title>
</head>

<body>
<h2 style="text-align: center;">Start klienta aplikacji Pit</h2>
<br>
<div style="text-align: center;">
<form method="get"
 action="http://localhost:8080/Pit1/Pit1Client"
 name="start">
 <input name="b" value="Start aplikacji" type="submit"><br>
</form>
</div>
</body>
</html>
Zobacz prezentację WebStartu.

Uruchomienie aplikacji w kontenerze klienta odbywa się za pomoca instrukcji appclient.


            appclient -client nazwa_pliku_jar_klienta


Ten JAR klienta dostajemy albo używając opcji -retrieve przy asadmin deploy, albo z poziomu konsoli administracyjnej wybierając "Download client stubs".
W obu przypadkach uzyskiwany JAR (z serwera działającego lokalnie) zawiera pełne klasy komponentów EJB, co oczywiście nie powinno mieć miejsca (powinny być tylko stuby).

Dlatego właśnie testujemy też wolnostojącego klienta, który zawiera tylko odwołania do interfejsu biznesowego, a dostęp do komponentów EJB uzyskuje zdalnie poprzez JNDI. Tu kluczową sprawą jest dobór odpowiednich dostawców JNDI. Standardowy (zawarty w Java SE) nie działa.
W tym celu wystarczy udostępnić bibliotekę appserv-rt.jar, która zawiera plik jndi.properties w postaci:
java.naming.factory.initial=com.sun.enterprise.naming.SerialInitContextFactory
java.naming.factory.url.pkgs=com.sun.enterprise.naming
# Required to add a javax.naming.spi.StateFactory for CosNaming that
# supports dynamic RMI-IIOP.
java.naming.factory.state=com.sun.corba.ee.impl.presentation.rmi.JNDIStateFactoryImpl
Wymienione klasy są w bibliotece javaee.jar - zatem i ją należy umieścić na ścieżce classpath.
Robi to dla nas skrypt antowy, produkując w wyniku taki oto mniej więcej runClient.bat.

setlocal
set elib= ... tu katalog lib serwera aplikacji
java -cp .;%elib%\appserv-rt.jar;%elib%\javaee.jar  pit.client.PitClient JAVA_CLIENT
endlocal

Uruchomienie klienta w kontenerze klienta i jako wolnostojącego pokazuje następująca prezentacja.



4. Komponenty encyjne i elementy Java Persistance API

Rozważymy teraz prosty przykład bazy danych studentów i kursów, w których uczestniczą.
Każdy student może uczestniczyć w wielu kursach. 
W każdym kursie może uczestniczyć wielu studentów.
Mamy tu relację Many-to-Many.

Komponenty encyjne przedstawiono na poniższych wydrukach programów.

package school.entity;

import java.io.*;
import java.util.*;

import javax.persistence.*;

import static javax.persistence.CascadeType.REMOVE;

@SuppressWarnings("serial")
@Entity
@Table(name="SCHOOL_STUDENT")
@NamedQueries({
  @NamedQuery(name = "getAllStudents",query = "SELECT s FROM Student s"),

  @NamedQuery(name = "getStudentsForCourse",
      query = "SELECT s FROM Student s, IN (s.courses) AS c WHERE c.id = :course_id"
  )
})


public class Student implements Serializable {

  private String id;
  private String name;
  private Collection<Course> courses;

  public Student() {
  }

  public Student(String index, String name) {
    this.id = index;
    this.name = name;
  }


  @Id
  public String getId() {
    return id;
  }

  public void setId(String index) {
    this.id = index;
  }

  @ManyToMany(cascade = REMOVE, mappedBy = "students")
  public Collection<Course> getCourses() {
    return courses;
  }

  public void setCourses(Collection<Course> courses) {
    this.courses = courses;
  }


  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String toString() {
    return id + " " + name;
  }


}
 
package school.entity;

import java.io.*;
import java.util.*;

import javax.persistence.*;

import static javax.persistence.CascadeType.REMOVE;

@SuppressWarnings("serial")
@Entity
@Table(name="SCHOOL_COURSE")
@NamedQueries({
  @NamedQuery(name = "getAllCourses",
      query = "SELECT c FROM Course c"
  ),

  @NamedQuery(name = "getCoursesForStudent",
      query = "SELECT c FROM Course c, IN (c.students) AS s WHERE s.id = :stud_id"
  )

}
)

public class Course implements Serializable{
  private String id;
  private String name;
  private Set<Student> students;

  public Course() {
  }

  public Course(String id, String name) {
    this.id = id;
    this.name = name;
  }

  @Id
  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  @ManyToMany(cascade = REMOVE)
  @JoinTable(name = "SCHOOL_COURSE_STUDENT",
    joinColumns = @JoinColumn(name = "COURSE_ID", referencedColumnName = "ID"),
    inverseJoinColumns = @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID")
  )
  public Set<Student> getStudents() {
    return students;
  }

  public void setStudents(Set<Student> students) {
    this.students = students;
  }

  public void addStudent(Student s) {
    getStudents().add(s);
  }

  public String toString() {
    return id + " " + name;
  }

}
W klasie Student relacja do kursów oznaczona jest adnotacją @ManyToMany. Opcja cascade określa sposób postępowania z powiązanymi rekordami (przy usuwaniu). Opcja mappedBy określa powiązanie z odpowiednim polem klasy Course (istotnie students są kolekcją studentów uczęszczających na dany kurs).
W klasie Course określamy powiązania pomiędzy tabelami.  Adnotacja @JoinTable spowoduje wygenerowanie pomocniczej tabeli, w której identyfikatorom studentów będą przyporządkowane identyfikatory kursów.
W obu klasach dostarczono definicji nazwanych zapytań (@NamedQueries). Dokładny opis składni Java Persistence Query Language można znaleźć w specyfikacji oraz w Java EE 5 Tutorial.

Dostęp do bazy danych uzyskujemy poprzez DbAccesBean implemntujący interfejs DbAccess.

package school.common;

import java.util.*;

import javax.ejb.*;

import school.entity.*;

@Remote
public interface DbAccess {

  void createCourse(String id, String name) throws SchoolDbException;;
  void createStudent(String id, String name) throws SchoolDbException;
  void assignCourseForStudent(String studId, String courseId) throws SchoolDbException;
  List<Student> getStudents(String courseId);
  List<Course> getCourses(String studId);

}
package school.ejb;

import java.util.*;

import javax.ejb.*;
import javax.persistence.*;

import school.common.*;
import school.entity.*;

@Stateful
public class DbAcessBean implements DbAccess {

  @PersistenceContext private EntityManager em;

  public void createCourse(String id, String name) throws SchoolDbException{
    Course c = new Course(id, name);
    try {
      em.persist(c);
    } catch (Exception exc) {
      throw new SchoolDbException(exc + "\n" + exc.getCause());
    }
  }

  public void createStudent(String id, String name) throws SchoolDbException {
    Student s = new Student(id, name);
    try {
      em.persist(s);
    } catch (Exception exc) {
      throw new SchoolDbException(exc + "\n" + exc.getCause()) ;
    }
  }



  public void assignCourseForStudent(String studId, String courseId) throws SchoolDbException {
      Student s = em.find(Student.class, studId);
      Course c = em.find(Course.class, courseId);
      if (s == null) throw new SchoolDbException("Invalid student id " + studId);
      if (c == null) throw new SchoolDbException("Invalid course id " + courseId);
      c.addStudent(s);

  }

  @SuppressWarnings("unchecked")
  public List<Student> getStudents(String courseId) {
    List<Student> result = null;
    if (courseId == null)
      result = em.createNamedQuery("getAllStudents").getResultList();
    else
      result = em.createNamedQuery("getStudentsForCourse")
                .setParameter("course_id", courseId)
                .getResultList();
    return result;
  }

  @SuppressWarnings("unchecked")
  public List<Course> getCourses(String studId) {
    List<Course> result = null;
    if (studId == null)
      result =  em.createNamedQuery("getAllCourses").getResultList();
    else
      result = em.createNamedQuery("getCourserForStudent")
                .setParameter("stud_id", studId)
                .getResultList();
    return result;
  }


}
Po to, by móc obsługiwac wyjątki typu SQLException wprowadziliśmy własną klasę SchoolDbException.
package school.common;

import java.io.*;


@SuppressWarnings("serial")
public class SchoolDbException extends Exception implements Serializable  {

  public SchoolDbException() {
    super();

  }

  public SchoolDbException(String message) {
    super(message);

  }

  public SchoolDbException(String message, Throwable cause) {
    super(message, cause);

  }

  public SchoolDbException(Throwable cause) {
    super(cause);

  }

}
Musi ona być serializowalna, aby móc uzyskać dostęp do wyjątku po stronie klienta.

Testowy klient tworzy studentów, kursy, przypisuje studentó do kursów i pokazuje wyniki wybranych zapytań.

package school.client;

import java.util.*;

import javax.ejb.EJB;
import javax.naming.*;
import javax.swing.*;

import school.common.*;
import school.entity.*;

public class SchoolDbClient {

  @EJB
  private static DbAccess dba;

  public SchoolDbClient() {
    this("DEFAULT_CONTEXT");
  }

  public SchoolDbClient(String appType) {
    init(appType);
    String[] studs = { "Kowalski Jan", "Kowalska Anna", "Malinowski Jan" };
    try {
      for (int i = 0; i < studs.length; i++) {
        System.out.println("Create student " + i + "\n");

        dba.createStudent("s000" + i, studs[i]);
      }

      String[] kurs = { "PRG", "WPR", "MPR" };
      for (int i = 0; i < kurs.length; i++) {
        dba.createCourse(kurs[i], kurs[i] + " - opis");
      }

      dba.assignCourseForStudent("s0000", "WPR");
      dba.assignCourseForStudent("s0000", "MPR");
      dba.assignCourseForStudent("s0001", "PRG");
      dba.assignCourseForStudent("s0002", "PRG");
      dba.assignCourseForStudent("s0002", "WPR");
      dba.assignCourseForStudent("s0002", "MPR");
    } catch (SchoolDbException exc) {
      exc.printStackTrace();
      System.exit(1);
    }

    StringBuffer sb = new StringBuffer();

    List<Student> s = dba.getStudents(null);
    for (Student info : s)
      sb.append(info).append('\n');
    JOptionPane.showMessageDialog(null, sb.toString());

    sb.setLength(0);
    s = dba.getStudents("WPR");
    for (Student info : s)
      sb.append(info).append('\n');
    JOptionPane.showMessageDialog(null, sb.toString());
  }

  private void init(String type) {
    if (type.equals("JAVA_APP"))
      throw new RuntimeException("Java App not supported");
    else if (type.equals("JAVA_CLIENT")) {
      try {
        InitialContext ic = new InitialContext();
        dba = (DbAccess) ic.lookup("school.common.DbAccess");
      } catch (NamingException e) {
        e.printStackTrace();
      }
    }
  }

  public static void main(String[] args) {
    if (args.length >= 1)
      new SchoolDbClient(args[0]);
    else
      new SchoolDbClient();
  }

}

Warto zwrócić uwagę, że klient posługuje się zdalnymi komponentami encyjnymi. W przypadku działania w kontenerze klienta nie ma z tym problemu, ale wolnostojący klient nie ma dostępu do niektórych klas (np. IndirectList) i po to, by mu to zapewnić należy użyć opcji -javaagent przy uruchamianiu
Tutaj podajemy jako agenta: toplink-essentials-agent.jar.
Należy także zapewnić serializację klas.

Biblioteka TopLink pozwala także tworzyć tabele na podstawie definicji klas encyjnych.
Opcję tę specyfikujemy w pliku persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
  <persistence-unit name="em" transaction-type="JTA">
    <jta-data-source>jdbc/SchoolDB</jta-data-source>
    <properties>
      <property name="toplink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
Zwróćmy uwagę na źródło danych. Odpowiednią nazwę JNDI musimy stworzyć np. z konsoli administracyjnej.

r

Tu ważne jest by zapewnić odpowiednią pulę połaczeń (lista rozwijalna Pool Name). Każdy schemat bazodanowy (odrębna baza danych) winien mieć swoją pulę (którą możemy utworzyć wybierając odpowiednią opcję i podając "fizyczną" nazwę bazy, host, port oraz inne parametry).
W przykładowych aplikacjach wygodnie jest korzystać z wbudowanego w  serwer RDBMS Derby.

Działanie klienta aplikacji SchoolDb obrazuje następująca prezentacja.


5. Podsumowanie

Zapoznaliśmy się z:


6. Zadania


Zad. 1
Zapoznać się ze specyfikacją EJB 3.0 i Java Persistance API

Zad. 2
Napisać aplikację EJB 3.0 pozwalającą na dokonywanie zakupów w sklepie z książkami.
Dostarczyć wolnostojącego klienta i modułu WEB.

Zad. 3
Użyć inteceptorów do sprawdzania poprawności argumentów wywołania określonych metod.