Pokazywanie postów oznaczonych etykietą ejb3. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą ejb3. Pokaż wszystkie posty

poniedziałek, 26 maja 2008

EJB3 - interceptory

Jedną z nowości w specyfikacji EJB w wersji trzeciej są interceptory, poprzez które w prosty sposób można zastosować w budowanym systemie paradygmat programowania zorientowanego aspektowo.
Interceptory w EJB3 mogą odnosić się zarówno do cyklu życia ziaren jak i ich metod biznesowych. Implementuje się je w postaci metod. Metody-interceptory mogą znajdować się w klasach ziaren jak i w klasach bazowych tych ziaren, ale także mogą się znajdować w odrębnych klasach (klasy interceptorów). Metody-interceptory mogą mieć dowolny poziom dostępu (public, package, protected, private), ale nie mogą być za to ani static, ani final.
Warto zaznaczyć, że implementacja interceptorów nie jest niczym ograniczona, gdyż można z nich wywoływać JNDI, JMS, inne ziarna, JDBC, oraz EntityManager'a.

1. InvocationContext

Metoda-interceptor do skutecznego działania musi mieć przede wszystkim kontekst wywołania, tj. instancję ziarna, informację o wywoływanej metodzie biznesowej oraz jej parametrów, a także rezultatu wywołania. Te dane można uzyskać poprzez obiekt InvocationContext, który jest przekazywany jako parametr do metody-interceptora.
Na szczególną uwagę zasługuje metoda InvocationContext.proceed(). Jej wywołanie powoduje przejście sterowania do następnego interceptora lub (jeśli jest wywołana w ostatnim interceptorze w łańcuchu) do metody biznesowej ziarna (jeśli jest to interceptor metod biznesowych).

2. Klasa interceptorów

Klasa interceptorów jest zwykłą klasą(nie musi być nawet oznaczona specjalną adnotacją), która po prostu posiada w sobie zdefiniowane metody-interceptory. Klasy te muszą posiadać publiczny bezargumentowy konstruktor.
Cykl życia interceptorów jest ściśle związany z cyklem życia ziarna do którego jest przypisany.
Klasy interceptorów można przypisywać do całego ziarna lub do metod biznesowych ziaren z osobna poprzez adnotację @Interceptors, który jako argument przyjmuje listę klas interceptorów.
W praktyce dołączanie klasy interceptorów wygląda mniej więcej następująco:
@Stateless
@Interceptors(pl.dwalczak.Interceptor1.class)
public class Ziarno {
...
  @Interceptors(pl.dwalczak.Interceptor2.class)
  public void metodaBiznesowa() {
  }
}

3. Interceptory cyklu życia ziaren

Aby dana metoda była interceptorem cyklu życia ziarna należy ją oznaczyć jedną z następujących adnotacji, odpowiednio do pożądanej fazy życia:   - @PostConstruct,   - @PostActivate,   - @PreDestroy,   - @PrePassivate Jeżeli metoda jest zdefiniowana w klasie ziarna, to powinna mieć następującą sygnaturę:
void <METHOD>()
Natomiast, jeżeli znajduje się w klasie interceptorów to powinna ona wyglądać tat:
void <METHOD>(InvocationContext)

4. Interceptory metod biznesowych

Aby dana metoda była interceptorem dla metod biznesowych, należy ją oznaczyć adnotacją @AroundInvoke.
W jednej klasie interceptora, ziarna, bądź klasie bazowej ziarna można tą adnotacją oznaczyć tylko jedną metodę, niemniej nie prowadzi to do ograniczenia, żeby metoda biznesowa ziarna miała tylko jeden interceptor. Wszystkie interceptory zdefiniowane w klasach bazowych, interceptorach podłączonych poprzez adnotację @Interceptors oraz ten zdefiniowany w klasie ziarna będą obowiązywać, a kolejność ich wykonania będzie zgodna z kolejnością ich deklarowania oraz hierarchią dziedziczenia klasy ziarna.
Metody oznaczone adnotacją @AroundInvoke są wykonywane na tym samycm stosie wywołania co metoda biznesowa. Taka metoda powinna zwracać wartość zwróconą przez metodę biznesową, oraz być zdolną do wyrzucenia wyjątku wyrzucanego przez metodę biznesową. Z tego powodu powinna mieć następującą sygnaturę:
Object <METHOD>(InvocationContext) throws Exception
Interceptor aby przekazać sterowanie dalej, tj. do następnego interceptora, bądź metody biznesowej, powinien wywołać metodę proceed na obiekcie InvocationContext.

5. Zasoby pomocnicze

Specyfikacja EJB3
The Interceptor Pattern

poniedziałek, 7 kwietnia 2008

Testowanie ziaren EJB3 z użyciem TestNG

Po pierwszych potyczkach z JPA nadszedł czas na zdobycie pierwszych doświadczeń w pisaniu ziaren EJB3. Najpierw chciałem jednak wybadać sprawy związane z testowaniem tychże ziaren. W poprzednich wersjach EJB z testowaniem było dość ciężko, gdyż jak wiadomo ziarna, żeby działały musiały być osadzone w kontenerze EJB, które były dostępne tylko w serwerach aplikacji. Jak dla mnie, jest to zdecydowanie za skomplikowany i zbyt czasochłonny proces testowania. Niestety ziarna EJB3 również wymagają, żeby uruchamiać je w kontenerze EJB.

Wbudowywalny kontener EJB3

Od pewnego czasu JBoss udostępnia wbudowywalny kontener EJB3. Jest to kontener, który można uruchomić poza serwerem aplikacji. W chwili obecnej kontener ten ma co prawda pewne ograniczenia, ale póki udostępnia wszystkie wymagane przeze mnie usługi nie będę się tym zbytnio przejmował ;)
Niemniej koncepcja wbudowywalnego kontenera EJB3 umożliwia stworzenie, szybkiego i łatwego w użyciu środowiska testów jednostkowych dla ziaren EJB3.

Środowisko testowe EJB3

Przy tworzeniu środowiska testów jednostkowych dla ziaren wykorzystam wspomniany wbudowywalny kontener EJB3.
Do jego uruchomienia wymagane jest, aby w zasięgu classpath'a znajdowały się (oprócz implementacji tego kontenera i bibliotek zależnych) następujące pliki konfiguracyjne: embedded-jboss-beans.xml, ejb3-interceptors-aop.xml, jndi.properties, default.persistence.properties.
W celu uniknięcia umieszczenia tych plików we wszystkich podprojektach modułów EJB tworzących korporacyjną aplikację, jak i określania w każdym z nich zależności od jar'ów wbudowanego kontenera EJB3, postanowiłem stworzyć dedykowany projekt maven'a. Projekt środowiska testów jednostkowych EJB będzie zawierał wspomniane pliki konfiguracyjne, zdefiniowane zależności do jar'ów implementacji kontenera oraz bazową klasę testową, dla testów jednostkowych ziaren.
A oto implementacja bazowej klasy testów jednostkowych:
package pl.dwalczak.ejb3testenv;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.apache.log4j.Logger;
import org.jboss.ejb3.embedded.EJB3StandaloneBootstrap;
import org.jboss.ejb3.embedded.EJB3StandaloneDeployer;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;

abstract public class Ejb3Test {

 private static final Logger LOG = Logger.getLogger(Ejb3Test.class);
 private static final String LOCAL_POSTFIX = "/local";
 private InitialContext initialContext;
 private EJB3StandaloneDeployer deployer;

 @BeforeSuite
 public void startupEjb3() {
  EJB3StandaloneBootstrap.boot(null);
  EJB3StandaloneBootstrap.scanClasspath();

  deployer = EJB3StandaloneBootstrap.createDeployer();
  try {
   deployer.setKernel(EJB3StandaloneBootstrap.getKernel());
   deployer.getArchivesByResource().add("META-INF/ejb-jar.xml");
   deployer.create();
   deployer.start();
   initialContext = new InitialContext();
  } catch (NamingException e) {
   LOG.error("Can't initialize context.", e);
   throw new RuntimeException(e);
  } catch (Exception e) {
   LOG.error("Deployer error.", e);
   throw new RuntimeException(e);
  }
 }

 @AfterSuite
 public void shutdownEjb3() {
  if (null != initialContext) {
   try {
    initialContext.close();
   } catch (NamingException e) {
    LOG.error("Can't close context.", e);
   }
  }
  if (null != deployer) {
   try {
    deployer.stop();
    deployer.destroy();
   } catch (Exception e) {
    LOG.error("Can't close deployer.", e);
   }
  }
  EJB3StandaloneBootstrap.shutdown();
 }

 @SuppressWarnings(value = "unchecked")
 public  T lookup(String beanName, Class businessInterface) {
  T result = null;
  try {
   result = (T) initialContext.lookup(beanName + LOCAL_POSTFIX);
  } catch (NamingException e) {
   LOG.error("Can't lookup bean: " + beanName, e);
   throw new RuntimeException(e);
  }
  return result;
 }
}
Dzięki temu przystosowanie nowego modułu EJB w maven'ie będzie polegało na dodaniu zależności do tego projektu (przy scope ustawionym na test).

Trzeba tu dodać, że bardziej złożona aplikacja korporacyjna będzie raczej wymagała specjalnie dostosowanego projektu środowiska testowego. Niemniej myślę, że przygotowany przeze mnie projekt tegoż środowiska jest dobrym punktem startowym.

Testowanie ziaren EJB3 - przykład

W moim przykładzie mamy do czynienia z bardzo prostym ziarnem:
package pl.dwalczak.ejb3pg1;
import javax.ejb.Stateless;
import org.apache.log4j.Logger;

@Stateless
public class CalculatorBean implements Calculator {
 
 private static Logger LOG = Logger.getLogger(CalculatorBean.class);

 public int add(int x, int y) {
  LOG.debug("add(" + x + ", " + y + ")");
  return x + y;
 }

}
Interfejs biznesowy:
package pl.dwalczak.ejb3pg1;

public interface Calculator {

 int add(int x, int y);
}
Po dodaniu zależności do projektu środowiska testowego można napisać test jednostkowy do tego ziarna, który może wyglądać w sposób następujący:
package pl.dwalczak.ejb3pg1;
import org.testng.Assert;
import org.testng.annotations.Test;
import pl.dwalczak.ejb3testenv.Ejb3Test;

public class CalculatorTest extends Ejb3Test {
 
 @Test
 public void testAdd() {
  Calculator calculator = lookup("CalculatorBean", Calculator.class);
  int sum = calculator.add(2, 2);
  Assert.assertEquals(sum, 4);
 }

}
Jest jeszcze tylko jedna kwestia. Do modułu EJB trzeba dodać plik konfiguracyjny META-INF/ejb-jar.xml, nawet jeśli nie trzeba w nim nic konfigurować. Jest to po prostu niezbędne, aby wykorzystywany kontener zaczytał klasy ziaren EJB3, pomimo że te są skonfigurowane z użyciem adnotacji.

Zasoby

Pliki źródłowe przykładu
Pliki źródłowe ejb3-test-env
Embeddable EJB 3.0
JSR-000220 Enterprise JavaBeans 3.0
JavaTM Platform Enterprise Edition, v 5.0 API Specifications

poniedziałek, 17 marca 2008

JPA - Pierwsze kroki

Przydługawy wstęp

Ponieważ od opublikowania ostatecznej wersji specyfikacji EJB3 upłynęło już sporo czasu, i pojawia się coraz więcej implementacji tej technologii - mniej lub bardziej kompletnych. To prawdopodobnie oznacza to, iż EJB3 będzie coraz częściej wybierana jako technologia bazowa w nowych projektach. Dlatego uznałem, że czas najwyższy zapoznać się z tą technologią nieco bliżej. Chciałbym zacząć od części specyfikacji określającej najniższą warstwę przeciętnej webaplikacji – modelu trwałych danych i styku z bazą danych (czy może raczej z JDBC), czyli Java Persistence API – JPA. W zasadzie JPA nie jest niczym rewolucyjnym. Sięgnięto raczej po sprawdzone koncepcje w najpopularniejszym chyba frameworku ORM – Hibernate. Cieszy mnie to tym bardziej, że przez ostatnie dwa lata z Hibernate'em miałem sporo do czynienia. Co więcej obecnie Hibernate stanowi jedną z implementacji JPA – i ja mam zamiar z niej skorzystać. Niewątpliwym plusem JPA jest to, że w zwarty i w dość prosty sposób standaryzuje podstawowe mechanizmy ORM. Interfejs JPA nie jest zbyt rozbudowany i w zaawansowanej aplikacji może się okazać jednak niewystarczający. W JPA nie znajdziemy wielu zaawansowanych elementów istniejących framework'ów ORM. Nie ma w nim API do dynamicznego tworzenia kryterium wyszukiwania, czy też nie ma możliwości definiowania pola klasy jako wyliczanej formuły SQL. Dlatego dostępne implementacje JPA dostarczają rozszerzenia do podstawowej specyfikacji. Oznacza to jednak, że transparentność implementacji specyfikacji JPA może okazać się tylko iluzoryczna. Z perspektywy użytkownika Hibernate'a poznanie JPA jest i tak nieuniknione jeżeli do mapowania chce stosować anotacji. Niemniej stosowanie konfiguracji w stylu JPA oraz EntityManager'a nie jest konieczne – przynajmniej na razie. Inną zaletą JPA jest możliwość jego użycia nie tylko w kontenerze EJB3, ale również w zwykłej JSE. No ale starczy już tych dywagacji, przejdę więc do opisu konkretnego przykładu użycia JPA.

Dziedzina problemu

Do zrobienia jest model, w którego centrum znajduje się byt Wiadomość - Message, który zawiera w sobie: temat - title, treść - content, oraz powiązania do bytu Użytkownik - User w roli nadawcy i odbiorcy.

Konfiguracja JPA

Specyfikacja JPA określa, że plik konfiguracyjny powinien znajdować się w pliku META-INF/persistence.xml. Listing pliku konfiguracyjnego mojej przykładowej aplikacji:
<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="jpapg1">
  <!-- Określenie dostawcy implementującego JPA. -->
  <provider>org.hibernate.ejb.HibernatePersistence</provider>
  <!-- 
  <class></class>
   Tu można podać listę klas mapujących model bazodanowy.
   W kontenerach EJB3 jest to zupełnie zbędne, gdyż specyfikacja JPA
   narzuca na implementację obowiązek automatycznego wyszukiwania klas.
   Ta cecha nie jest natomiast określona w przypadku uruchomienia w
   zwykłym środowisku JSE. Hibernate'a można jednak sparametryzować,
   żeby wyszukiwał te klasy również w JSE.
   -->
  <properties>
   <!-- Parametry konfiguracyjne dostawce JPA - Hibernate -->
   <property name="hibernate.archive.autodetection" value="class" />
   <property name="hibernate.show_sql" value="true" />
   <property name="hibernate.format_sql" value="true" />
   <!-- Parametry dostępu do bazy danych. -->
   <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
   <property name="hibernate.connection.url" value="jdbc:hsqldb:file:jpapg1db" />
   <property name="hibernate.connection.username" value="sa" />
   <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
   <!--
    Parametr określający generowanie schematu bazy danych
    na podstawie mapingów.
    -->
   <property name="hibernate.hbm2ddl.auto" value="create" />
  </properties>
 </persistence-unit>
</persistence>

Model danych i mapowanie

Ograniczę się tylko na przedstawieniu listingu kodu źródłowego przykładu, który - mam przynajmniej taką nadzieję - jest wystarczająco okroszony komentarzami.
Użytkownik - User
package pl.dwalczak.jpapg1.model;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@NamedQueries(value={
@NamedQuery(
        name="user.findAll",
        query="select u from User u order by u.nickName asc"
),
@NamedQuery(
  name="user.findByNickName",
  query="select u from User u where u.nickName = :nickName"
)})

// Definicja sekwencji "users_seq" służącej do generowania klucza głównego tabeli "users".
@SequenceGenerator(name="users_seq")

// Mapowanie tabeli "users".
@Entity
@Table(name="users")
public class User {

 private Long id;
 private String nickName;
 private List receivedMessages;
 private List sendMessages;
 
 // dostępność domyślnego konstruktora jest wymagana przez JPA.
 public User() {
 }
 
 public User(String nickName) {
  this.nickName = nickName;
 }

 // Klucz główny tabeli (kolumna "usr_id"),
 // którego wartość jest wyznaczana przez sekwencje "users_seq".
 @Id
 @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="users_seq")
 @Column(name="usr_id")
 public Long getId() {
  return id;
 }

 protected void setId(Long id) {
  this.id = id;
 }

 // Mapowanie kolumny "usr_nickname", która musi posiadać unikalne wartośc
 // i mieć maksymalną długość 24 znaków.
 @Column(name="usr_nickname", unique=true, nullable=false, length=24)
 public String getNickName() {
  return nickName;
 }

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

 // Mapowanie połączenia do tabeli "messages" w roli odbiorcy wiadomości.
 // Określenie kolumny w tabeli "messages" po której odbywa się złączenie ("msg_to").
 // Zaznaczenie, że w przypadku zapisu bytu "User" nie ma nic robić z "Message"
 // skojarzonymi przez te powiązanie.
 @OneToMany
 @JoinColumn(name="msg_to", insertable=false, updatable=false)
 public List getReceivedMessages() {
  return receivedMessages;
 }

 public void setReceivedMessages(List receivedMessages) {
  this.receivedMessages = receivedMessages;
 }

 // Definicja połączenia do tabeli "messages" w roli nadawcy wiadomości.
 @OneToMany
 @JoinColumn(name="msg_from", insertable=false, updatable=false)
 public List getSendMessages() {
  return sendMessages;
 }

 public void setSendMessages(List sendMessages) {
  this.sendMessages = sendMessages;
 }
}
Wiadomość - Message
package pl.dwalczak.jpapg1.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@NamedQueries(value={
@NamedQuery(
        name="message.findReceived",
        query="select m from Message m where m.to = :user"
),
@NamedQuery(
        name="message.findSend",
        query="select m from Message m where m.from = :user"
)})

// Definicja sekwencji "messages_seq" służącej do generowania klucza głównego tabeli "messages".
@SequenceGenerator(name="messages_seq")

// Mapowanie tabeli "messages".
@Entity
@Table(name="messages")
public class Message {

 private Long id;
 private String title;
 private String content;
 private User from;
 private User to;
 
  // dostępność domyślnego konstruktora jest wymagana przez JPA
 public Message() {
 }
 
 public Message(String title, String content, User from, User to) {
  this.title = title;
  this.content = content;
  this.from = from;
  this.to = to;
 }

 // Klucz główny tabeli (kolumna "msg_id"),
 // którego wartość jest wyznaczana przez sekwencje "messages_seq".
 @Id
 @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="messages_seq")
 @Column(name="msg_id")
 public Long getId() {
  return id;
 }

 protected void setId(Long id) {
  this.id = id;
 }

 // Mapowanie kolumny "msg_title", której wartość może mieć maksymalnie 64 znaków.
 @Column(name="msg_title", length=64)
 public String getTitle() {
  return title;
 }

 public void setTitle(String title) {
  this.title = title;
 }

 // Mapowanie kolumny "msg_content", której wartość może mieć maksymalnie 1024 znaków.
 @Column(name="msg_content", length=1024)
 public String getContent() {
  return content;
 }

 public void setContent(String content) {
  this.content = content;
 }

 // Mapowanie do tabeli "users" - nadawcy wiadomości.
 // Powiązanie to jest obowiązkowe - musi być ustawione - i jest realizowane przez
 // klucz obcy do tabeli "users", którego wartość przechowuje kolumna "msg_from".
 // Jest to odpowiednik do powiązania zdefiniowanego na User.sendMessages.
 @ManyToOne(optional=false)
 @JoinColumn(name="msg_from", nullable=false)
 public User getFrom() {
  return from;
 }

 public void setFrom(User from) {
  this.from = from;
 }

 // Mapowanie do tabeli "users" - adresat wiadomości.
 // Jest to odpowiednik do powiązania zdefiniowanego na User.receivedMessages.
 @ManyToOne(optional=false)
 @JoinColumn(name="msg_to", nullable=false)
 public User getTo() {
  return to;
 }

 public void setTo(User to) {
  this.to = to;
 }
}

Entity Manager

Entity Manager jest tym w JPA, czym jest Session w Hibernate. W JSE można go pozyskać w następujący sposób:
javax.persistence.Persistence.createEntityManagerFactory($PERSISTANCE_UNIT_NAME).createEntityManager();
W EJB3 można to zrobić przez wstrzyknięcie (przez kontener) do pola w beanie EJB:
import javax.ejb.Stateless;
import javax.persistence.PersistenceContext;
@Stateless
public class ExampleBean implements Example {
 @PersistenceContext
 EntityManager em;
 ...
}
Entity Manager jest punktem wyjścia do wykonania takich operacji jak: - utrwalenie stanu objektu (metoda persist) - wykonanie zapytań (metoda find oraz grupa metod z rodziny createQuery) - manulnego określania tranzakcji - rozpoczęcie, akceptacji oraz wycofania (metoda getTransaction())

Język zapytań

Język zapytań w JPA to JPA-QL (rozszerzenie EJB-QL) i jest bardzo podobny do Hibernate'owego HQL'a. Myślę, że wykorzystane w tym przykładzie zapytania są na tyle proste, że nie wymagają komentarza.

Zasoby

Pliki źródłowe przykładu
Java Persistence with Hibernate - Sample Chapter 2
JPA - javadoc
Hibernate Annotations
Hibernate EntityManager