Spring JMS

JMS – technologia, którą ugryzłem podczas przygotowań do SCBCD, lecz później odeszła w niepamięć. Na szkoleniu “Developing Architectures for Enterprise Java Applications” wspominano o niej namiętnie w ramach zagadnień integracji systemów B2B. Stąd ten post i wewnętrzna konieczność poznania Spring JMS. Zaprezentuję jak za pomocą framework’u Spring wstawiać i pobierać wiadomości z kolejki serwera aplikacyjnego JBoss 5.1.0. GA z poziomu “grubego” klienta. W dalszej części artykułu poruszę także zagadnienie “współdzielenia” transakcji między systemami JMS i bazami danych RDBMS.

Tworzenie kolejki na serwerze JBoss

Przejdź do katalogu $JBOSS_HOME/server/default/deploy/, a następnie wyedytuj nowy plik o nazwie hrjob-destination-service.xml. Jego finalną zawartość prezentuje poniższy listing.

<?xml version="1.0" encoding="UTF-8"?>
<server>
  <mbean code="org.jboss.jms.server.destination.QueueService"
         name="jboss.messaging.destination:service=Queue,name=HrJobQueue"
         xmbean-dd="xmdesc/Queue-xmbean.xml">
    <depends optional-attribute-name="ServerPeer">jboss.messaging:service=ServerPeer</depends>
    <depends>jboss.messaging:service=PostOffice</depends>
  </mbean>
</server>

Tym samym wykorzystaliśmy funkcję Hot Deploy’u serwisów JBoss (dokumentacja). Mam nadzieję, że wszystko poszło pomyślnie. Jeśli nie, tzn. nie widzicie nowej kolejki w panelu administracyjnym, wymagany jest restart serwera.

Wstawianie wiadomości do kolejki

Referencję do obiektu kolejki HrJobQueue pobierzemy z kontenera JNDI serwera JBoss. W tym celu posłużymy się Spring’owym JndiTemplate.

<bean id="jndiTemplate" class="org.springframework.jndi.JndiTemplate">
  <property name="environment">
    <props>
      <prop key="java.naming.factory.initial">org.jnp.interfaces.NamingContextFactory</prop>
      <prop key="java.naming.provider.url">jnp://localhost:1099</prop>
      <prop key="java.naming.factory.url.pkgs">org.jboss.naming:org.jnp.interfaces</prop>
    </props>
  </property>
</bean>

Powyższa konfiguracja może różnić się dla poszczególnych serwerów aplikacyjnych. Następnie zdefiniujemy ziarna symbolizujące ConnectionFactory oraz JmsTemplate.

<bean id="jmsConnectionFactory" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiTemplate" ref="jndiTemplate"/>
  <property name="jndiName" value="ConnectionFactory"/>
</bean>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
  <property name="connectionFactory" ref="jmsConnectionFactory"/>
  <property name="receiveTimeout" value="100"/>
  <property name="defaultDestination" ref="defaultDestination"/>
  <property name="messageConverter" ref="oxmMessageConverter"/>
</bean>

Na wzór postu Gordon’a Dickens’a (http://gordondickens.com/wordpress/2011/02/07/sending-beans-as-xml-with-jmstemplate), wiadomości przesyłać będę w postaci XML’owej. Podejście to uniezależnia pod względem architektury innych użytkowników kolejki. Przed wysłaniem, obiekt POJO języka Java zostanie zserializowany do formatu XML za pomocą standardu JAXB. Przy odbiorze nastąpi automatyczna deserializacja. Parametr receiveTimeout JmsTemplate’a określa maksymalny czas oczekiwania na wiadomość podczas korzystania z metod typu JmsTemplate.receiveAndConvert(). Podejście to omówię szerzej w sekcji dotyczącej odbioru komunikatu. defaultDestination to kolejny bean pobrany ze zdalnego serwisu JNDI prezentujący domyślną lokację docelową – kolejkę HrJobQueue.

<bean name="defaultDestination" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiTemplate" ref="jndiTemplate"/>
  <property name="jndiName" value="queue/HrJobQueue"/>
</bean>

messageConverter odpowiada za serializację i deserializację wiadomości. W naszym przypadku całość implementacji sprowadza się do stworzenia klasy POJO z adnotacjami JAXB oraz niewielką konfiguracją Spring’ową.

@XmlRootElement(name = "payload")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name= "PayloadClass", propOrder={"firstName", "lastName"})
public class PayloadClass implements Serializable {
   @XmlElement(required=true, nillable=false)
   private String firstName = null;

   @XmlElement(required=true, nillable=false)
   private String lastName = null;

   /* Getters & Setters */
   ...
}
<oxm:jaxb2-marshaller id="marshaller">
  <oxm:class-to-be-bound name="edu.lantoniak.spring.jms.pojo.PayloadClass"/>
</oxm:jaxb2-marshaller>
<bean id="oxmMessageConverter"
      class="org.springframework.jms.support.converter.MarshallingMessageConverter">
  <property name="marshaller" ref="marshaller"/>
  <property name="unmarshaller" ref="marshaller"/>
</bean>

Serwis wysyłający wiadomość prezentuje się bardzo prosto (patrz niżej). Za wstrzyknięcie odpowiedniej referencji do jmsTemplate odpowiada Spring.

public class JMSSender {
   private JmsTemplate jmsTemplate = null;

   public void sendObjectMessage() {
      PayloadClass payload = new PayloadClass();
      payload.setFirstName("Lukasz");
      payload.setLastName("Antoniak");
      jmsTemplate.convertAndSend(payload);
   }

   public void setJmsTemplate(JmsTemplate jmsTemplate) {
      this.jmsTemplate = jmsTemplate;
   }

   public JmsTemplate getJmsTemplate() {
      return jmsTemplate;
   }
}

Pobieranie wiadomości z kolejki

Spring JMS umożliwia pobranie wiadomości z kolejki na dwa główne sposoby. Pierwszy polega na wywołaniu funkcji JmsTemplate.receiveXXX(). Metoda ta oczekuje maksymalnie receiveTimeout milisekund. Jeśli w zadanym czasie wiadomość nie nadejdzie – zwraca null. JmsTemplate.receiveAndConvert() automatycznie konwertuje komunikat wykorzystując wpięty w JmsTemplate messageConverter. Fragment kodu źródłowego zamieściłem poniżej.

PayloadClass payload = (PayloadClass) jmsSender.getJmsTemplate().receiveAndConvert();
if (payload != null) {
   System.out.println("First Name: " + payload.getFirstName());
   System.out.println("Last Name: " + payload.getLastName());
}

Drugi sposób pobierania wiadomości polega na implementacji interfejsu MessageListener (zupełnie jak MDB). Klasa implementująca musi zostać wpięta w Spring’owy komponent DefaultMessageListenerContainer (czytaj dokumentację i JavaDoc!). Niezbędną konfigurację przedstawia kolejny listing.

<bean id="messageListener" class="edu.lantoniak.spring.jms.services.JMSReceiver">
  <property name="messageConverter" ref="oxmMessageConverter"/>
</bean>
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
  <property name="connectionFactory" ref="jmsConnectionFactory"/>
  <property name="destination" ref="defaultDestination"/>
  <property name="messageListener" ref="messageListener"/>
</bean>

Nie widziałem żeby Spring udostępniał abstrakcyjną klasę implementującą MessageListener zapewniającą automatyczną konwersję wiadomości. Stąd konieczność wstrzyknięcia obiektu oxmMessageConverter. Poniżej zamieszczam kompletny kod źródłowy listener’a.

public class JMSReceiver implements MessageListener {
   private MessageConverter messageConverter = null;

   public void onMessage(Message message) {
      if (message instanceof BytesMessage) {
         try {
            PayloadClass payload = (PayloadClass) messageConverter.fromMessage(message);
            System.out.println("First Name: " + payload.getFirstName());
            System.out.println("Last Name: " + payload.getLastName());
         } catch (JMSException e) {
            throw new RuntimeException(e);
         }
      } else {
         throw new IllegalArgumentException("Message must be of type BytesMessage.");
      }
   }

   public void setMessageConverter(MessageConverter messageConverter) {
      this.messageConverter = messageConverter;
   }
}

Prezentowany MessageListener zostanie uruchomiony (zacznie nasłuchiwać na kolejce) od razu po starcie kontenera Spring. W celu jego tymczasowego wyłączenia, podczas zabaw własnych z aplikacją, należy wykomentować deklarację bean’a jmsContainer. Istnieje ponadto możliwość zniszczenia wspomnianego bean’a w dowolnym momencie.

DefaultMessageListenerContainer jmsContainer = (DefaultMessageListenerContainer) ctx.getBean("jmsContainer");
jmsContainer.destroy();

Warto wykonać ową metodę podczas sekwencji kończącej działanie aplikacji. Dzięki temu listener elegancko zajmknie połączenie JMS. W przeciwnym wypadku JBoss po jakimś czasie zgłosi w logach, iż istnieje problem z komunikacją z określonym klientem JMS i nastąpi jego wyrejestrowanie.

Transakcyjność, czyli JMS i RDBMS

W aplikacjach enterprise rzadko kiedy wykorzystuje się samą technologię JMS. Najczęściej współistnieje ona z bazą danych, gdzie informacje przekazane w komunikatach są przechowywane. Jeśli niepowodzeniem zakończy się rozlokowanie danych w schemacie RDBMS, to ferelna wiadomość nie powinna zniknąć z kolejki. Być może problem stanowi jedynie ilość wolnego miejsca w przestrzeni tabel, a ta zostanie niebawem rozszerzona. Obsługa transakcji w Spring Framework została dokładnie opisana w dokumentacji (http://static.springsource.org/spring/docs/2.5.x/reference/jms.html) i każdy powinien się z nią zapoznać. Ogromne znaczenie ma fakt łączenia się do kolejki z wewnątrz kontenera Java EE lub ze zdalnego klienta. W naszym przypadku (zdalny klient) należy ustawić parametr sessionTransacted obiektu JmsTemplate na true. Operacja ta spowoduje wysłanie bądź nie wiadomości w zależności od Spring’owego zarządcy transakcji. Konfiguracja przedstawia się następująco.

<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
  <property name="connectionFactory" ref="jmsConnectionFactory"/>
  <property name="receiveTimeout" value="100"/>
  <property name="defaultDestination" ref="defaultDestination"/>
  <property name="messageConverter" ref="oxmMessageConverter"/>
  <property name="sessionTransacted" value="true"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="transactionManager"
      class="org.springframework.orm.hibernate3.HibernateTransactionManager">
  <property name="sessionFactory" ref="hibernateSessionFactory"/>
</bean>

Założyłem, iż posiadamy gotową fabrykę sesji Hibernate. Po Internecie krążą stosy materiałów na temat integracji Spring z Hibernate (słowa kluczowe: HibernateDaoSupport, HibernateTemplate) oraz obsłudze transakcji przez Spring (http://slawekturowicz.blogspot.com/2011/01/transakcyjnosc-w-springu.html). Na łamach niniejszego artykułu nie zamierzam skupiać się na owych zagadnieniach. Spójrzmy na poniższy serwis wstawiający komunikat do kolejki JMS, a następnie rekord do bazy danych.

<bean id="jmsAndJdbcService" class="edu.lantoniak.spring.jms.services.JmsAndJdbcServiceImpl">
  <property name="jmsTemplate" ref="jmsTemplate"/>
  <property name="sessionFactory" ref="hibernateSessionFactory"/>
</bean>
public class JmsAndJdbcServiceImpl extends HibernateDaoSupport implements JmsAndJdbcService {
   private JmsTemplate jmsTemplate = null;

   @Transactional
   public void insertJmsMessageAndGenerateDatabaseFault() {
      jmsTemplate.send(new MessageCreator() {
         public Message createMessage(Session session) throws JMSException {
            return session.createTextMessage("This message shall not be seen.");
         }
      });

      /* Try to persist a JOB record that will cause unique key constraint violation. */
      JobEntity job = new JobEntity();
      job.setJobId("AD_PRES");
      job.setJobTitle("President");
      getHibernateTemplate().persist(job);
   }

   public void setJmsTemplate(JmsTemplate jmsTemplate) {
      this.jmsTemplate = jmsTemplate;
   }
}

Jeśli proces utrwalania obiektu typu JobEntity nie powiedzie się, całość operacji insertJmsMessageAndGenerateDatabaseFault zakończy się niepowodzeniem (komunikat JMS nie zostanie wysłany). W listingu powyżej staram się wstawić stanowisko o istniejącym wcześniej identyfikatorze (domyślny schemat HR bazy danych Oracle).

Transakcyjne odbieranie komunikatów JMS okazuje się równie przyjemne (czyt. proste) :). Wspomnianego wcześniej zarządce transakcji wpinamy do obiektu jmsContainer. Taka obsługa transakcji poprawna jest dla grubego klienta, gdzie transakcją zarządza Spring, a nie kontener Java EE (podrozdział “19.4.5. Processing messages within transactions” dokumentacji)! W innym przypadku manager’em transakcji powinien zostać obiekt typu JtaTransactionManager.

<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
  <property name="connectionFactory" ref="jmsConnectionFactory"/>
  <property name="destination" ref="defaultDestination"/>
  <property name="messageListener" ref="messageListener"/>
  <property name="transactionManager" ref="transactionManager"/>
</bean>

Zmodyfikowana klasa JMSReceiver wygląda następująco.

public class JMSReceiver extends HibernateDaoSupport implements MessageListener {
   private MessageConverter messageConverter = null;

   @Transactional
   public void onMessage(Message message) {
      JobEntity job = new JobEntity();
      job.setJobId("New");
      job.setJobTitle("New Title");
      getHibernateTemplate().persist(job);

      if (message instanceof BytesMessage) {
         try {
            PayloadClass payload = (PayloadClass) messageConverter.fromMessage(message);
            System.out.println("First Name: " + payload.getFirstName());
            System.out.println("Last Name: " + payload.getLastName());
         } catch (JMSException e) {
            throw new RuntimeException(e);
         }
      } else {
         throw new IllegalArgumentException("Message must be of type BytesMessage.");
      }
   }

   public void setMessageConverter(MessageConverter messageConverter) {
      this.messageConverter = messageConverter;
   }
}
<bean id="messageListener" class="edu.lantoniak.spring.jms.services.JMSReceiver">
  <property name="messageConverter" ref="oxmMessageConverter"/>
  <property name="sessionFactory" ref="hibernateSessionFactory"/>
</bean>

Jeśli utrwalenie obiektu JobEntity zakończy się niepowodzeniem, przetwarzany komunikat zostanie zwrócony do kolejki.

Komunikacja spoza kontenera J2EE

Gruntownie przemyślana architektura aplikacji typu enterprise narzuca sztywne ograniczenia na korzystanie z zasobów współdzielonych. Powodów należy doszukiwać się oczywiście w aspektach wydajności. Co by się stało gdyby każda aplikacja grubego klienta używała dowolnej ilości połączeń do zdalnej bazy danych? Katastrofa. Aby zapewnić wykorzystanie tylko jednego połączenia JMS (przez producenta i konsumenta) zastosować należy klasę SingleConnectionFactory opakowującą standardową fabrykę połączeń.

<bean id="jmsSingleConnectionFactory"
      class="org.springframework.jms.connection.SingleConnectionFactory">
  <property name="targetConnectionFactory" ref="jmsConnectionFactory"/>
  <property name="reconnectOnException" value="true"/>
</bean>

Zamiast jmsConnectionFactory do jmsTemplate i jmsContainer wpinamy jmsSingleConnectionFactory.

JMS i WebLogic

Serwer aplikacyjny WebLogic udostępnia spory wachlarz rozszerzeń technologii JMS. Za przykład posłuży mi funkcja Unit of Order (UOO) zapewniająca chronologiczną kolejność przychodzących komunikatów. Polecam odwiedzenie WebLogic YouTube Channel (http://www.youtube.com/oracleweblogic) oraz zabawę z przykładami z projektu wls-1034-examples (https://www.samplecode.oracle.com/sf/projects/wls-1034-examples).

Na koniec zamieszczam oczywiście kompletną wersję przykładowego projektu mojego autorstwa (tutaj). Długie godziny męczyłem się z dopasowaniem poszczególnych bibliotek zależnych (głównie JMS i JNDI) do JBoss 5.1.0 GA, więc zachęcam do pobrania źródeł.

<bean id=”jndiTemplate” class=”org.springframework.jndi.JndiTemplate”>
<property name=”environment”>
<props>
<prop key=”java.naming.factory.initial”>org.jnp.interfaces.NamingContextFactory</prop>
<prop key=”java.naming.provider.url”>jnp://localhost:1099</prop>
<prop key=”java.naming.factory.url.pkgs”>org.jboss.naming:org.jnp.interfaces</prop>
</props>
</property>
</bean>
Advertisements

Errata: Biblioteki zewnętrzne, EAR i Maven

Pierwsza errata na moim blogu :). Umiem przyznać się do błędu, lecz miejmy nadzieję, że to jedna z ostatnich tego typu sytuacji. Rzecz dotyczy dodawania bibliotek zewnętrznych do pakietu EAR zbudowanego przy użyciu Maven. Poniżej zamieszczam najbardziej elegancką według mnie konfigurację maven-ear-plugin:

<plugin>
  <artifactId>maven-ear-plugin</artifactId>
  <version>2.4.2</version>
  <configuration>
    <jboss>
      <version>5</version>
    </jboss>
    <defaultLibBundleDir>lib</defaultLibBundleDir>
    <modules>
      <ejbModule>
        <groupId>javaee-app.controller</groupId>
        <artifactId>controller</artifactId>
      </ejbModule>
      <webModule>
        <groupId>javaee-app.view</groupId>
        <artifactId>view</artifactId>
        <contextRoot>view</contextRoot>
      </webModule>
      <jarModule>
        <groupId>javaee-app.model</groupId>
        <artifactId>model</artifactId>
        <includeInApplicationXml>true</includeInApplicationXml>
        <bundleDir>/</bundleDir>
      </jarModule>
      <jarModule>
        <groupId>javaee-app.utils</groupId>
        <artifactId>utils</artifactId>
        <includeInApplicationXml>true</includeInApplicationXml>
        <bundleDir>/</bundleDir>
      </jarModule>
      <jarModule>
        <groupId>org.springframework</groupId>
        <artifactId>spring</artifactId>
      </jarModule>
    </modules>
  </configuration>
</plugin>

Szczególną uwagę należy zwrócić na tag defaultLibBundleDir, dzięki któremu wszystkie biblioteki dodatkowe zostaną umieszczane domyślnie w katalogu lib (tutaj Spring Framework). Wszystkie moduły deploy’owane jako jarModule także znalazłyby się we wspomnianym wcześniej katalogu gdyby nie nadpisanie konfiguracji znacznikiem bundleDir. W tym wypadku należy jednak wymusić dodanie odpowiedniego wpisu do pliku application.xml przez ustawienie atrybutu includeInApplicationXml na true.

Dzięki przedstawionej wyżej konfiguracji dodatkowe biblioteki zostaną umieszczone w katalogu lib archiwum EAR oraz nie będą wymagały dodatkowego wpisu w głównym deskryptorze aplikacji.

Pragnę także oznajmić, że idąc z duchem czasu zamierzam przesiąść się ze starego, dobrego Subversion na Mercurial’a. Źródła projektu można pobrać z repozytorium: https://bitbucket.org/lukasz_antoniak/javaee-app.

JBoss Maven Plugin

Ostatnio na stronie administracyjnej blog’a przeglądałem, czego ludzie szukają trafiając na moją witrynę. Często pojawiało się hasło “jboss maven plugin”. Ale przecież ja o nim tylko wspomniałem… Indexer Google’a działa wyśmienicie. Przydałoby się jednak napisać coś więcej o tak często wyszukiwanym przez społeczność narzędziu. Jako punkt początkowy przyjmę aplikację stworzoną na końcu post’a EJB3 i Spring.

Dokumentacja jboss-maven-plugin (http://mojo.codehaus.org/jboss-maven-plugin) napisana została bardzo treściwie i zrozumiale. Sam naciąłem się jednak na kilka aspektów w niej nie poruszonych. Konfigurację plugin’u umieściłem w pliku pom.xml projektu EAR. Przedstawia się ona następująco:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>jboss-maven-plugin</artifactId>
  <version>1.4.1</version>
  <configuration>
    <jbossHome>D:\Praca\Certificates\Sun\SCBCD\Tutorial\jboss-5.1.0.GA</jbossHome>
    <serverName>default</serverName>
    <hostName>localhost</hostName>
    <port>8080</port>
    <fileNames>
      <fileName>${basedir}\target\ear-1.0.ear</fileName>
    </fileNames>
  </configuration>
</plugin>

Uwagę należy zwrócić na wartość parametru fileName, gdyż dodałem tam referencję do katalogu głównego, w którym znajduje się plik pom.xml${basedir}. W przeciwnym razie otrzymywałem w logach serwera JBoss poniższy wyjątek:

08:28:45,875 WARN  [MainDeployer] Failed to deploy: file:/D:/Praca/Certificates/Sun/SCBCD/Tutorial/jboss-5.1.0.GA/bin/target/ear-1.0.ear
java.io.FileNotFoundException: ear-1.0.ear doesn't exist. (rootURI: file:/D:/Praca/Certificates/Sun/SCBCD/Tutorial/jboss-5.1.0.GA/bin/target/ear-1.0.ear, file: D:\Praca\Certificates\Sun\SCBCD\Tutorial\jboss-5.1.0.GA\bin\target\ear-1.0.ear)

Na konsoli zaś:

[INFO] Mojo error occurred: Server returned HTTP response code: 500 for URL: http://localhost:8080/jmx-console/HtmlAdaptor?action=invokeOpByName&name=jboss.system:service%3DMainDeployer&methodName=deploy&argType=java.net.URL&arg0=target%5Cear-1.0.ear

W celu zastosowania goal’i jboss:start i jboss:stop wymagane jest określenie zmiennej jbossHome. Na tak skonfigurowanym środowisku, bez problemu wykonać można z katalogu ear polecenia:

mvn clean package
mvn jboss:start
mvn jboss:deploy
mvn jboss:redeploy
mvn jboss:undeploy
mvn jboss:stop

Polecam głębsze zapoznanie się z dokumentacją i zabawę opcją hard-deploy.

Budowanie aplikacji Java EE wykorzystując Apache Maven

Wykonując ćwiczenia przygotowujące do SCBCD oparłem się na tworzeniu małych projektów Eclipse’owych oraz deploy’owaniu ich za pomocą wtyczki do obsługi serwera JBoss. Przyszedł jednak czas na odświeżenie i ugruntowanie wiadomości z zakresu Maven oraz sposobu pakowania archiwum EAR. Stworzę więc prosty projekt Java EE składający się z kilku modułów:

  • Model – przechowujący mapowanie ORM i pliki encji.
  • View – moduł widoku aplikacji opartym na JSF.
  • Controller – warstwa serwisów aplikacji wykorzystująca EJB.
  • Utils – pomocniczy moduł z często używanymi funkcjami.
  • EAR – projekt budujący archiwum EAR.

W celu stworzenia głównego katalogu projektu wydajemy z konsoli komendę:

mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=javaee-app -DartifactId=javaee-app -Dversion=1.0

Następnie edytujemy plik pom.xml i zmieniamy sposób pakowania (tag packaging) na pom. Kolejna czynność polega na stworzeniu wszystkich modułów pomocniczych:

mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp -DgroupId=javaee-app.view -DartifactId=view -DpackageName=edu.lantoniak.view -Dversion=1.0
mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=javaee-app.utils -DartifactId=utils -DpackageName=edu.lantoniak.utils -Dversion=1.0
mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=javaee-app.controller -DartifactId=controller -DpackageName=edu.lantoniak.controller -Dversion=1.0
mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=javaee-app.model -DartifactId=model -DpackageName=edu.lantoniak.model -Dversion=1.0
mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=javaee-app.ear -DartifactId=ear -DpackageName=edu.lantoniak.ear -Dversion=1.0

Do pliku POM głównego projektu proponuję dodać wszystkie zależności związane z Javą EE, JPA oraz JSF. Plik ten w końcowej wersji powinien wyglądać następująco:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>javaee-app</groupId>
   <artifactId>javaee-app</artifactId>
   <packaging>pom</packaging>
   <version>1.0</version>
   <name>javaee-app</name>
   <url>http://maven.apache.org</url>
   <dependencies>
      <dependency>
         <groupId>javaee</groupId>
         <artifactId>javaee-api</artifactId>
         <version>5</version>
         <scope>provided</scope>
      </dependency>
      <dependency>
         <groupId>javax.persistence</groupId>
         <artifactId>persistence-api</artifactId>
         <version>1.0</version>
         <scope>provided</scope>
      </dependency>
      <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>servlet-api</artifactId>
         <version>2.5</version>
         <scope>provided</scope>
      </dependency>
      <dependency>
         <groupId>javax.faces</groupId>
         <artifactId>jsf-api</artifactId>
         <version>1.2</version>
         <scope>provided</scope>
      </dependency>
      <dependency>
         <groupId>javax.faces</groupId>
         <artifactId>jsf-impl</artifactId>
         <version>1.2_13</version>
         <scope>provided</scope>
      </dependency>
      <dependency>
         <groupId>javax.servlet.jsp</groupId>
         <artifactId>jsp-api</artifactId>
         <version>2.1</version>
         <scope>provided</scope>
      </dependency>
      <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>3.8.1</version>
         <scope>test</scope>
      </dependency>
   </dependencies>
   <modules>
      <module>view</module>
      <module>utils</module>
      <module>controller</module>
      <module>ear</module>
      <module>model</module>
   </modules>
</project>

Jeśli pobranie pewnych zależności kończy się niepowodzeniem, proponuję dodać następujące repozytorium Maven’a:

<repositories>
   <repository>
      <id>java.net1</id>
      <name>Java.Net Maven1 Repository, hosts the javaee-api dependency</name>
      <url>http://download.java.net/maven/1</url>
      <layout>legacy</layout>
   </repository>
</repositories>

Wracając jednak do pliku POM głównego projektu – na uwagę zasługuje opcja provided znacznika scope. Dzięki temu Maven, przy budowania EAR’a, nie umieszcza w nim bibliotek zainstalowanych na serwerze aplikacyjnym. Wewnątrz tagu modules wymienione zostały wszystkie moduły, z których składa się aplikacja. Zależności między modułami umieszcza się w plikach POM konkretnych modułów wraz z odniesieniem do parent’a, czyli projektu głównego (patrz do kodu źródłowego).

Moduł Controller
Moduł Controller korzysta z EJB, do którego zależność umieszczona została w głównym pliku POM projektu. W celu zbudowania projektu EJB, skorzystać należy z dodatku maven-ejb-plugin:

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-ejb-plugin</artifactId>
   <configuration>
      <ejbVersion>3.0</ejbVersion>
   </configuration>
</plugin>

Minimalna konfiguracja wymaga jedynie wyspecyfikowanie używanej wersji EJB. Moduł Controller zależeć będzie od dwóch wewnętrznych modułów – Model i Utils (w końcu ciężko byłoby korzystać z JPA po stronie ziaren EJB bez klas encji):

<dependency>
   <groupId>javaee-app.model</groupId>
   <artifactId>model</artifactId>
   <version>1.0</version>
   <scope>compile</scope>
</dependency>
<dependency>
   <groupId>javaee-app.utils</groupId>
   <artifactId>utils</artifactId>
   <version>1.0</version>
   <scope>compile</scope>
</dependency>

Niestety oddzielenie modelu aplikacji (mapowania ORM) od warstwy EJB ciągnie za sobą dwie poważne wady. Deklaracja Persistence Unit pozostać musi po stronie modułu Controller’a, ponieważ zasięg widoczności PU nie wychodzi poza archiwum JAR, a umieszczenie korzenia PU bezpośrednio w archiwum EAR uznałem za gorszy pomysł. Druga bolączka to konieczność wyspecyfikowania nazwy archiwum JAR, w którym znajdą się klasy poszczególnych encji, w pliku persistence.xml (controller/main/resources/META-INF/persistence.xml, linijka 7):

<?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="javaee-appPU">
      <jta-data-source>java:/OracleDS</jta-data-source>
      <jar-file>model-1.0.jar</jar-file>
   </persistence-unit>
</persistence>

Moduł EAR
Moduł EAR odpowiada za stworzenia archiwum EAR (w tym za generację pliku application.xml, spakowanie pozostałych modułów etc.), które w dalszych krokach podlega instalacji na serwerze aplikacyjnym. Do budowy archiwum zastosuję plugin maven-ear-plugin:

<plugin>
   <artifactId>maven-ear-plugin</artifactId>
   <version>2.4.2</version>
   <configuration>
      <jboss>
         <version>5</version>
      </jboss>
      <modules>
         <ejbModule>
            <groupId>javaee-app.controller</groupId>
            <artifactId>controller</artifactId>
         </ejbModule>
         <webModule>
            <groupId>javaee-app.view</groupId>
            <artifactId>view</artifactId>
            <contextRoot>view</contextRoot>
         </webModule>
         <jarModule>
            <groupId>javaee-app.model</groupId>
            <artifactId>model</artifactId>
            <includeInApplicationXml>true</includeInApplicationXml>
         </jarModule>
         <jarModule>
            <groupId>javaee-app.utils</groupId>
            <artifactId>utils</artifactId>
            <includeInApplicationXml>true</includeInApplicationXml>
         </jarModule>
      </modules>
   </configuration>
</plugin>

Instrukcje wewnątrz znacznika modules specyfikują poszczególne moduły aplikacji wraz z ich typami (ejbModule, webModule, jarModule). Ponadto moduły Model i Utils powinny zostać uwzględnione w pliku application.xml (webModule i ejbModule są tam dodane domyślnie – odpowiednio w tagach web i ejb). Powoduje to konieczność ustawienia flagi includeInApplicationXml na true. Plugin maven-ear-plugin umożliwia także zmianę Context Root, pod którym deploy’owana jest aplikacji (linia 16).

Moduł Model, Utils i View
Konfiguracja Maven’a dla tych projektów nie wyróżnia się niczym szczególnym. Zawiera ona jedynie deklarację zależności między wewnętrznymi i zewnętrznymi bibliotekami.

Co dalej?
Aplikację zbudowaną przy pomocy polecenia mvn clean package (plik ear/target/ear-1.0.ear) umieścić można na serwerze JBoss korzystając z konsoli administratora (http://localhost:8080/admin-console/) lub wtyczki do Maven’a jboss-maven-plugin (http://mojo.codehaus.org/jboss-maven-plugin/).

Podczas tworzenia projektu Maven’owego pod EJB, JPA i JSF wzorowałem się na tutorialach Jacka Laskowskiego i Alexander Shvets:

Mam nadzieję, że mój krótki wpis, udostępniona przykładowa aplikacja oraz ich tutoriale pomogą w tzw. szybkim starcie z Java EE i Maven.

Źródła projektu: JavaEE-app.zip.
Errata: Biblioteki zewnętrzne, EAR i Maven.

Uwaga! Dla przejrzystości artykułu nie omawiałem wszystkich zagadnień wykorzystywanych podczas dewelopmentu aplikacji Java EE (mapowania ORM, zasady działania managed bean’ów w JSF, konfiguracji web.xml, persistence.xml etc.). Zachęcam do ściągnięcia projektu i obejrzenia źródeł.