Aplikacja Java EE 5 z MDB z JPA w trybie JTA i PostgreSQL w Apache Geronimo 2

Z Jacek Laskowski - Wiki Projektanta Java EE

Po zakończeniu artykułu Uruchomienie ziarna MDB w Apache Geronimo o ziarnie sterowanym komunikatami (ang. message-driven bean, czy po prostu MDB) uruchomionym na serwerze Apache Geronimo 2 postanowiłem kontynuować jego rozwój o dodanie encji Java Persistence 1.0 (JPA) w trybie JTA z relacyjną bazą danych PostgreSQL. W poprzednim artykule poruszyłem kwestię konfiguracji kolejki komunikatów JMS i związanym z nią ziarnem MDB, podczas gdy teraz nastąpi rozbudowanie technologiczne aplikacji o funkcjonalność dostarczaną przez usługę JPA (część kontenera EJB 3.0, który w Apache Geronimo jest reprezentowany przez Apache OpenEJB) z bazą danych PostgreSQL (domyślną bazą danych w Apache Geronimo jest Apache Derby).

Podczas lektury zaprezentowane zostaną kroki niezbędne do stworzenia kompletnej aplikacji, a w tym zarejestrowanie sterownika JDBC oraz utworzenia puli połączeń do bazy danych PostgreSQL. Rozwój aplikacji oparty jest o aplikację z artykułu Uruchomienie ziarna MDB w Apache Geronimo, gdzie można znaleźć tekst wprowadzający, jak i przedstawienie wielu faktów z zakresu konfiguracji aplikacji korporacyjnej w Apache Geronimo.

Spis treści

Oprogramowanie

Środowisko składa się z następujących elementów:

Zakłada się, że powyższe oprogramowanie jest zainstalowane i działa poprawnie. Instalacja oprogramowania sprowadza się do pobrania i rozpakowania paczek w wybranym katalogu.

Kompletny projekt do zaimportowania do NetBeans IDE 6.0 dostępny jest jako mdb-jpa-jta-postgresql-geronimo.zip.

Moduł encji JPA - TicketServiceJPA

Encja JPA - Komunikat

Modyfikacje rozpoczniemy od wprowadzenia encji JPA - Komunikat, która jest reprezentowana przez klasę pl.jaceklaskowski.ticketservice.entity.Komunikat.

package pl.jaceklaskowski.ticketservice.entity;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
public class Komunikat implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id    
    @GeneratedValue
    private Long id;
    
    @Column(nullable = false)
    private String tresc;

    @Column(nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Date czas;

    public Komunikat() {
    }

    public Komunikat(String tresc, Date czas) {
        this.tresc = tresc;
        this.czas = czas;
    }

    public Long getId() {
        return id;
    }

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

    public String getTresc() {
        return tresc;
    }

    public void setTresc(String tresc) {
        this.tresc = tresc;
    }

    public Date getCzas() {
        return czas;
    }

    public void setCzas(Date czas) {
        this.czas = czas;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof Komunikat)) {
            return false;
        }
        Komunikat other = (Komunikat) object;
        if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return getClass().getCanonicalName() + "[id=" + id + "]";
    }
}

Encja Komunikat posiada atrybuty zawierające informacje o treści i dacie odbioru komunikatu przez ziarno MDB. Za pomocą encji, a właściwie za pomocą JPA, które będzie zarządzać encją, będziemy zapisywać informacje o odebranych komunikatach przez MDB do bazy danych.

Konfiguracja JPA - persistence.xml

Do poprawnego działania encji Komunikat konieczne jest utworzenie konfiguracji JPA 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="ticketservicePU" transaction-type="JTA">
        <properties>
            <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(SchemaAction='add,deleteTableContents')"/>
            <property name="openjpa.jdbc.DBDictionary" value="postgres(SupportsDeferredConstraints=false)"/> 
        </properties>
        <jta-data-source>jdbc/postgres</jta-data-source>
        <non-jta-data-source>jdbc/postgres</non-jta-data-source>
    </persistence-unit>
</persistence>

Plik persistence.xml definiuje nazwę jednostki trwałej na podstawie, której będzie tworzony kontekst trwały zarządzany przez zarządcę trwałego (ang. entity manager). Aplikacja będzie uruchomiona na Apache Geronimo 2, który jest certyfikowanym serwerem aplikacji Java EE 5, co umożliwia wykorzystanie trybu JTA (atrybut transaction-type w persistence-unit), który zleca serwerowi aplikacyjnemu zarządzanie transakcjami (ang. CMTD - Container-Managed Transaction Demarcation) oraz możliwość skorzystania z transakcji rozproszonych (w przeciwieństwie do lokalnych zasobom, np. opartych o połączenia do bazy danych). Pozostałe elementy konfiguracji to definicja dwóch typów źródeł danych - jta-data-source oraz non-jta-data-source wskazujące na nazwy w drzewie nazw JNDI z pulą połączeń do bazy danych. Konfiguracja nazw JNDI jest specyficzna dla serwera aplikacji i w specyficzny dla Apache Geronimo sposób zostanie zdefiniowane.

Konfiguracja modułu - openejb-jar.xml

Powiązanie wymagań aplikacji ze środowiskiem uruchomieniowym Apache Geronimo następuje poprzez plik openejb-jar.xml, który znajduje się w katalogu META-INF.

<?xml version="1.0" encoding="UTF-8"?>
<openejb-jar xmlns="http://www.openejb.org/xml/ns/openejb-jar-2.1">
    <environment xmlns="http://geronimo.apache.org/xml/ns/deployment-1.2">
        <moduleId>
            <groupId>pl.jaceklaskowski.ticketservice</groupId>
            <artifactId>TicketServiceJPA</artifactId>
            <version>1.0</version>
        </moduleId>
        <dependencies>
            <dependency>
                <groupId>org.apache.geronimo.configs</groupId>
                <artifactId>openjpa</artifactId>
                <type>car</type>
            </dependency>
        </dependencies>
        <hidden-classes/>
        <non-overridable-classes/>
    </environment>
</openejb-jar>

Za pomocą sekcji moduleId nadajemy nazwę aplikacji, aby uprościć jej zarządzanie z poziomu Geronimo. Warto zapamiętać, że Geronimo jest po prostu środowiskiem uruchomieniowym modułów i możliwość uruchamiania modułów, które są jednocześnie aplikacjami Java EE zawdzięcza wyłącznie dedykowanym modułom, które rozumieją ich znaczenie. Nie wnikając w szczegóły (poza to, co już zostało powiedziane) należy dodać, że z punktu widzenia Geronimo aplikacja Java EE jest niczym więcej, jak pewnym modułem (analogicznym do aplikacji, która nie korzysta z Java EE). Właśnie dlatego warto rozważyć nadanie nazwy aplikacji, aby później uprościć jej administrację (najbardziej widoczne jest to podczas pracy bez konsoli graficznej Geronimo, na poziomie linii poleceń). Element moduleId nie jest obowiązkowy, a jedynie zalecany i przy jego braku, podczas instalacji Geronimo nada własną nazwę.

Moduł ziarna MDB - TicketServiceMDB

Ziarno MDB - TicketServiceMDB

Moduł składa się z pojedyńczego ziarna sterowanego komunikatami (ang. message-driven bean lub MDB) - TicketServiceBean.

Określenie klasy jako klasy ziarna MDB następuje poprzez adnotację @MessageDriven, w której podaje się wymagania odnośnie zasobów zarządzanych JMS w środowisku uruchomieniowym Geronimo niezbędnych dla poprawnej pracy ziarna MDB. W wymaganiach podano typ zasobu JMS - javax.jms.Queue jako wartość parametru destinationType oraz nazwę kolejki JMS - TicketQueue w parametrze destination (będą one wykorzyste w późniejszej konfiguracji aplikacji przemysłowej TicketServiceEAR przed uruchomieniem w Geronimo).

package pl.jaceklaskowski.ticketservice.ejb;

import java.util.Calendar;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.ejb.MessageDrivenContext;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import pl.jaceklaskowski.ticketservice.entity.Komunikat;

@MessageDriven(
    activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
        @ActivationConfigProperty(propertyName = "destination", propertyValue = "TicketQueue")
    }
)
public class TicketServiceBean implements MessageListener {

    @Resource
    private MessageDrivenContext ctx;
    
    @PersistenceContext
    private EntityManager em;
    
    private final Logger logger = Logger.getLogger(getClass().getName());

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void onMessage(Message message) {
        try {
            if (message instanceof TextMessage) {
                TextMessage msg = (TextMessage) message;
                String tresc = msg.getText();
                logger.info("Odebrano komunikat: " + tresc);
                em.persist(new Komunikat(tresc, Calendar.getInstance().getTime()));
            } else {
                logger.warning("Komunikat niewłaściwego typu: " + message.getClass().getName());
            }
        } catch (JMSException e) {
            e.printStackTrace();
            ctx.setRollbackOnly();
        } catch (Throwable te) {
            te.printStackTrace();
        }
    }

    @PostConstruct
    void initialize() {
        logger.info("Wykonano PostConstruct - kontekst przekazany (wstrzelony)? " + (ctx != null));
    }

    @PreDestroy
    void destroy() {
        logger.info("Wykonano PreDestroy");
    }

    @AroundInvoke
    Object sprawdzKtoWykonujeMetodeBiznesowa( InvocationContext invocationContext) throws Exception {
        logger.info("Metoda przechwytująca wykonana - wywołujący: " + ctx.getCallerPrincipal().getName());
        return invocationContext.proceed();
    }
}

Ciekawostką ziarna jest wykorzystanie zarządcy encji (ang. entity manager) przekazanego przez serwer aplikacji za pomocą mechanizmu wstrzeliwania zależności (ang. dependency injection) poprzez adnotację @PersistenceContext. Po otrzymaniu komunikatu, wykonywana jest seria metod przechwytujących - interceptorów, które oznaczone są w klasie ziarna za pomocą adnotacji @PostConstruct, @PreDestroy oraz @AroundInvoke. Każda z nich wywoływana jest podczas zmiany stadium rozwojowego ziarna - zaraz po utworzeniu, a przed wywołaniem metody biznesowej onMessage (@PostConstruct), przed usunięciem ziarna z serwera (@PreDestroy) oraz każdorazowo podczas wykonania metody biznesowej (@AroundInvoke), która w przypadku ziarna MDB jest metodą interfejsu komunikacyjnego (w przypadku interfejsu JMS - javax.jms.MessageListener - będzie to metoda onMessage(Message message)) zanim metoda komunikacyjna zostanie faktycznie wywołana (i to jedynie, jeśli interceptor @AroundInvoke wykona metodę InvocationContext.proceed()).

Konfiguracja zasobów JMS dla ziarna MDB - openejb-jar.xml

Do poprawnej konfiguracji ziarna MDB wymagane jest zadeklarowanie konfiguracji Geronimo, w której zdefiniowane są zasoby JMS - kolejka o nazwie TicketQueue. Związanie ziarna z konfiguracją zasobów JMS następuje w sekcji resource-link sekcji enterprise-beans, która wskazuje na zasóby ukryte pod nazwą jms-resources (będą one zdefiniowane w jeszcze jednym pliku konfiguracyjnym). Być może początkowa konfiguracja niewprost, może jedynie przyprawić o zawrót głowy, to ma tę zaletę, że wprowadza warstwy oddzielające fizyczną konfigurację Geronimo z jej konfiguracją symboliczną, co skutkuje, że zmiana konfiguracji jednego elementu nie implikuje zmian w module zależnym. Geronimo udostępnia domyślną kolejkę DefaultActiveMQConnectionFactory oraz MDBTransferBeanOutQueue, które są zasobami ogólniedostępnymi. W tym artykule decydujemy się na zasoby JMS dedykowane naszej aplikacji i stąd niewielkie skomplikowanie przykładu.

Poza samym wskazaniem konfiguracji kolejki, potrzebne jest określenie zależności (moduły konfiguracyjne Geronimo) org.apache.geronimo.configs/activemq-broker//car oraz org.apache.geronimo.configs/openjpa//car, które dostarczają wymagane zasoby (chociażby uzupełniają moduł ładujący klasy - ang. classloader - o typy z pakietów JMS i JPA, jak i specyficzną dla Geronimo konfigurację zasobów - kolejki komunikatów, pulę połączeń z bazą danych).

Plik openejb-jar.xml powinien znajdować się w katalogu META-INF.

<?xml version="1.0" encoding="UTF-8"?>
<openejb-jar 
    xmlns="http://www.openejb.org/xml/ns/openejb-jar-2.1"
    xmlns:naming="http://geronimo.apache.org/xml/ns/naming-1.1" 
    xmlns:security="http://geronimo.apache.org/xml/ns/security-1.1" 
    xmlns:sys="http://geronimo.apache.org/xml/ns/deployment-1.2">
    <environment xmlns="http://geronimo.apache.org/xml/ns/deployment-1.2">
        <moduleId>			
            <groupId>pl.jaceklaskowski.ticketservice</groupId>
            <artifactId>TicketServiceEJB</artifactId>
            <version>1.0</version>
            <type>car</type>
        </moduleId>
        <dependencies>
            <dependency>
                <groupId>org.apache.geronimo.configs</groupId>
                <artifactId>activemq-broker</artifactId>
                <type>car</type>
            </dependency>
            <dependency>
                <groupId>org.apache.geronimo.configs</groupId>
                <artifactId>openjpa</artifactId>
                <type>car</type>
            </dependency>
        </dependencies>
        <hidden-classes/>
        <non-overridable-classes/>
    </environment>
    <enterprise-beans>
        <message-driven>
            <ejb-name>TicketServiceBean</ejb-name>
            <resource-adapter>
                <resource-link>jms-resources</resource-link>
            </resource-adapter>
        </message-driven>
    </enterprise-beans>
</openejb-jar>

Moduł aplikacji przemysłowej - TicketServiceEAR

Konfiguracja aplikacji przemysłowej - geronimo-application.xml

Związanie zasobów symbolicznych z ziarna MDB - TicketServiceBean - oraz encji JPA - Komunikat - z zasobami fizycznymi odbywało się za pomocą pliku konfiguracyjnego Geronimo - openejb-jar.xml dystrybuowanym z nimi w katalogu META-INF.

W pliku geronimo-application.xml definiowane są fizyczne zasoby, które zostaną utworzone przez Geronimo podczas instalacji aplikacji przemysłowej TicketServiceEAR (alternatywnie moglibyśmy skorzystać z już dostępnych zasobów, czy to dostarczanych domyślnie przez Geronimo, czy też zdefiniować własne w osobnym kroku i w ten sposób pozbyć się tego pliku). Warto zwrócić uwagę na sekcję resourceadapter-instance z wartością jms-resources, element name w sekcji connectiondefinition-instance o wartości TicketConnectionFactory oraz message-destination-name w sekcji adminobject-instance o wartości TicketQueue, które odpowiadają deklaracjom zasobów JMS w ziarnie MDB - TicketServiceBean. Konfiguracja puli połączeń do PostgreSQL następuje w sekcji kolejnej odpowiadającej modułowi zewnętrznemu postgresql (sekcja ext-module), gdzie deklarujemy zależność od biblioteki sterownika JDBC dla PostgreSQL - postgresql/postgresql/8.2-506.jdbc3/jar oraz nazwę dla puli jdbc/postgres w sekcji connectiondefinition-instance (właśnie poprzez konfigurację niewprost, symboliczną, możemy później skierować aplikację na użycie innej bazy danych niż PostgreSQL bez konieczności zmiany jakiejkolwiek innej części aplikacji, a już tym bardziej ziarna MDB).

<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://geronimo.apache.org/xml/ns/j2ee/application-2.0">
    <environment xmlns="http://geronimo.apache.org/xml/ns/deployment-1.2">
        <moduleId>
            <groupId>pl.jaceklaskowski.ticketservice</groupId>
            <artifactId>TicketServiceEAR</artifactId>
            <version>1.0</version>
            <type>ear</type>
        </moduleId>
    </environment>
    <ext-module>
        <connector>jms-resources</connector>
        <external-path xmlns:dep="http://geronimo.apache.org/xml/ns/deployment-1.2">
            <dep:groupId>org.apache.geronimo.modules</dep:groupId>
            <dep:artifactId>geronimo-activemq-ra</dep:artifactId>
            <dep:version>2.1-SNAPSHOT</dep:version>
            <dep:type>rar</dep:type>
        </external-path>
        <connector xmlns="http://geronimo.apache.org/xml/ns/j2ee/connector-1.2">
            <dep:environment xmlns:dep="http://geronimo.apache.org/xml/ns/deployment-1.2">
                <dep:dependencies>
                    <dep:dependency>
                        <dep:groupId>org.apache.geronimo.configs</dep:groupId>
                        <dep:artifactId>activemq-broker</dep:artifactId>
                        <dep:type>car</dep:type>
                    </dep:dependency>
                </dep:dependencies>
            </dep:environment>
            <resourceadapter>
                <resourceadapter-instance>
                    <resourceadapter-name>jms-resources</resourceadapter-name>
                    <workmanager xmlns="http://geronimo.apache.org/xml/ns/naming-1.2">
                        <gbean-link>DefaultWorkManager</gbean-link>
                    </workmanager>
                </resourceadapter-instance>
                <outbound-resourceadapter>
                    <connection-definition>
                        <connectionfactory-interface>javax.jms.ConnectionFactory</connectionfactory-interface>
                        <connectiondefinition-instance>
                            <name>TicketConnectionFactory</name>
                            <implemented-interface>javax.jms.QueueConnectionFactory</implemented-interface>
                            <implemented-interface>javax.jms.TopicConnectionFactory</implemented-interface>
                            <connectionmanager>
                                <xa-transaction>
                                    <transaction-caching/>
                                </xa-transaction>
                                <single-pool>
                                    <match-one/>
                                </single-pool>
                            </connectionmanager>
                        </connectiondefinition-instance>
                    </connection-definition>
                </outbound-resourceadapter>
            </resourceadapter>
            <adminobject>
                <adminobject-interface>javax.jms.Queue</adminobject-interface>
                <adminobject-class>org.apache.activemq.command.ActiveMQQueue</adminobject-class>
                <adminobject-instance>
                    <message-destination-name>TicketQueue</message-destination-name>
                    <config-property-setting name="PhysicalName">TicketQueue</config-property-setting>
                </adminobject-instance>
            </adminobject>
        </connector>
    </ext-module>    
    <ext-module>    
        <connector>postgresql</connector>
        <external-path xmlns:dep="http://geronimo.apache.org/xml/ns/deployment-1.2">
            <dep:groupId>org.tranql</dep:groupId>
            <dep:artifactId>tranql-connector-postgresql-xa</dep:artifactId>
            <dep:type>rar</dep:type>
        </external-path>
        <connector xmlns="http://geronimo.apache.org/xml/ns/j2ee/connector-1.2">
            <environment xmlns="http://geronimo.apache.org/xml/ns/deployment-1.2">
                <moduleId>
                    <groupId>pl.jaceklaskowski.ticketservice</groupId>
                    <artifactId>postgresql</artifactId>
                    <version>1.0</version>
                    <type>rar</type>
                </moduleId>
                <dependencies>
                    <dependency>
                        <groupId>postgresql</groupId>
                        <artifactId>postgresql</artifactId>
                        <version>8.2-506.jdbc3</version>
                        <type>jar</type>
                    </dependency>
                </dependencies>
            </environment>
            <resourceadapter>
                <outbound-resourceadapter>
                    <connection-definition>
                        <connectionfactory-interface>javax.sql.DataSource</connectionfactory-interface>
                        <connectiondefinition-instance>
                            <name>jdbc/postgres</name>
                            <config-property-setting name="UserName">geronimo</config-property-setting>
                            <config-property-setting name="Password">geronimo</config-property-setting>
                            <config-property-setting name="DatabaseName">sandbox</config-property-setting>
                            <connectionmanager>
                                <local-transaction/>
                                <single-pool>
                                    <max-size>100</max-size>
                                    <min-size>0</min-size>
                                    <blocking-timeout-milliseconds>5000</blocking-timeout-milliseconds>
                                    <match-one/>
                                </single-pool>
                            </connectionmanager>
                        </connectiondefinition-instance>
                    </connection-definition>
                </outbound-resourceadapter>
            </resourceadapter>
        </connector>
    </ext-module>    
</application>

Moduł zdalnego klienta - TicketServiceClient

Klasa klienta zdalnego pl.jaceklaskowski.ticketservice.TicketServiceClient korzysta z zasobów JMS udostępnianych przez Apache ActiveMQ, który jest domyślnym dostawcą JMS w Geronimo. Wymaganiem ActiveMQ jest, aby kontekst JNDI tworzony był z odpowiednimi parametrami środowiskowymi, w szczególności wymagany jest parametr Context.PROVIDER_URL wskazujący na adres serwera i portu, na którym nasłuchuje zarządca ActiveMQ (domyślnie tcp://localhost:61616 w Geronimo) oraz parametry connectionFactoryNames i queue.TicketQueue, które wskazują na nazwę fabryki kolejek jak i samą kolejkę. Ważne jest, aby nazwa parametru queue.TicketQueue składała się z nazwy kolejki, której nazwa jest podana również jako wartość. Dalsze kroki są już niezależne od dostawcy JMS i wymagają jedynie wiedzy o samej specyfikacji JMS.

UWAGA: Klient wymaga dopracowania obsługi sytuacji wyjątkowych. Kiedy pojawi się wyjątek zasoby nie będą posprzątane (wbrew temu, co sugeruje komentarz). Ze względu na fakt, że wystąpienie wyjątku faktycznie kończy klienta i wszystkie zasoby, włącznie z zasobami JMS, są sprzątane przymykamy oko na takie niewielkie niedociągnięcie.

package pl.jaceklaskowski.ticketservice;

import java.util.Properties;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;

public class TicketServiceClient {

    private final static int NUMBER_OF_MESSAGES = 1;
    
    public static void main(String[] args) throws Exception {
        //
        // Krok 1 - Połączenie z serwerem Geronimo
        //
        Properties env = new Properties();
        env.put(Context.PROVIDER_URL, "tcp://localhost:61616");
        env.put("connectionFactoryNames", "TicketConnectionFactory");
        env.put("queue.TicketQueue", "TicketQueue");
        Context jndiContext = new InitialContext(env);

        QueueConnectionFactory factory = (QueueConnectionFactory) jndiContext.lookup("TicketConnectionFactory");
        Queue queue = (Queue) jndiContext.lookup("TicketQueue");

        //
        // Krok 2 - Utworzenie połączeń z zasobami JMS
        //
        QueueConnection connection = factory.createQueueConnection();
        QueueSession session = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
        MessageProducer producer = session.createProducer(queue);

        //
        // Krok 3 - Wysłanie wiadomości
        //
        for (int i = 0; i < NUMBER_OF_MESSAGES; i++) {
            System.out.println("Wysylana wiadomosc nr #" + i);
            TextMessage txtMsg = session.createTextMessage();
            txtMsg.setText("Wiadomosc od Jacka o numerze #" + i);
            producer.send(txtMsg);
        }

        //
        // Krok 4 - Zakończenie aplikacji - sprzątanie
        //
        session.close();
        connection.close();
    }
}

Klient zdalny zarządzany jest przez narzędzie Apache Maven 2. Wybór Maven podyktowany był potrzebą zniwelowania konieczności samodzielnego zarządzania bibliotekami zależnymi, które wymagane są do uruchomienia aplikacji z Apache ActiveMQ.

Ciekawostką konfiguracji projektu jest skorzystanie z wtyczki exec:exec, dzięki której uruchamiany jest klient. Przy takiej konfiguracji wystarczy wydać polecenie mvn install exec:exec, aby zbudować i uruchomić klienta (i w międzyczasie pobrać niezbędne biblioteki zależne).

Plik konfiguracyjny projektu pom.xml.

<?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>pl.jaceklaskowski.ticketservice</groupId>
    <artifactId>client</artifactId>
    <packaging>jar</packaging>
    <version>1.0</version>
    <name>TicketServiceClient</name>
    <url>http://www.JacekLaskowski.pl/wiki/Aplikacja_Java_EE_5_z_MDB_z_JPA_w_trybie_JTA_i_PostgreSQL_w_Apache_Geronimo_2</url>
    <dependencies>
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>apache-activemq</artifactId>
            <version>4.1.1</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>pl.jaceklaskowski.ticketservice.TicketServiceClient</mainClass>
                            <addClasspath>true</addClasspath>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>java</executable>
                    <arguments>
                        <argument>-classpath</argument>
                        <classpath/>
                        <argument>pl.jaceklaskowski.ticketservice.TicketServiceClient</argument>
                    </arguments>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Konfiguracja PostgreSQL

Utworzenie koniecznej bazy danych w PostgreSQL oparłem o materiał opisany w artykule Aplikacja korporacyjna z JPA w trybie JTA z GlassFish i PostgreSQL. Bez angażowanie się w detale administracji PostgreSQL operacja utworzenia bazy danych sprowadza się do wykonania następujących poleceń.

jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2
$ ./bin/initdb -D "c:/temp/geronimo-postgresql-sampledb" --locale=Polish_Poland.28592

jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2
$ ./bin/pg_ctl -D "c:/temp/geronimo-postgresql-sampledb" -l "c:/temp/geronimo-postgresql-sampledb/logfile" start
server starting

jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2
$ ./bin/createuser.exe --createdb --pwprompt --no-superuser --no-createrole geronimo
Enter password for new role: <enter geronimo>
Enter it again: <enter geronimo>
CREATE ROLE

jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2
$ ./bin/createdb.exe --encoding=UNICODE --username geronimo sandbox
CREATE DATABASE

jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2
$ ./bin/psql sandbox
Welcome to psql 8.2.5, the PostgreSQL interactive terminal.

Type:  \copyright for distribution terms
       \h for help with SQL commands
       \? for help with psql commands
       \g or terminate with semicolon to execute query
       \q to quit

Warning: Console code page (852) differs from Windows code page (1250)
         8-bit characters may not work correctly. See psql reference
         page "Notes for Windows users" for details.

sandbox=# \l
         List of databases
   Name    |   Owner    | Encoding
-----------+------------+-----------
 postgres  | jlaskowski | SQL_ASCII
 sandbox   | geronimo   | UTF8
 template0 | jlaskowski | SQL_ASCII
 template1 | jlaskowski | SQL_ASCII
(4 rows)

sandbox=# \q

Uruchomienie aplikacji korporacyjnej w Apache Geronimo

Uruchomienie Geronimo

Uruchomienie aplikacji rozpoczynamy od uruchomienia Geronimo poleceniem geronimo.sh (bądź geronimo.bat na MS Windows).

UWAGA: Poniższy skrypt wykonany został z użyciem parametru -vv, który wskazuje na wyświetlanie wszystkich komunikatów Geronimo podczas jego startu. Parametr -vv jest opcjonalny i stosowany powinien być wyłącznie podczas diagnozy sytuacji awaryjnych, gdzie szczegółowe informacje o środowisku Geronimo są niezbędne.

jlaskowski@dev /cygdrive/c/geronimo
$ ./bin/geronimo.sh run -vv
Using GERONIMO_BASE:   c:\geronimo
Using GERONIMO_HOME:   c:\geronimo
Using GERONIMO_TMPDIR: c:\geronimo\var\temp
Using JRE_HOME:        c:\apps\java5\jre
21:03:26,171 DEBUG [BasicKernel] Starting boot
21:03:26,343 DEBUG [GBeanInstanceState] GBeanInstanceState for: geronimo/boot/none/car?role=kernel State changed from stopped to starting
21:03:26,343 DEBUG [GBeanInstanceState] GBeanInstanceState for: geronimo/boot/none/car?role=kernel State changed from starting to running
21:03:26,343 DEBUG [BasicKernel] Booted
21:03:26,406 DEBUG [AbstractRepository] Repository root is C:\geronimo\repository
21:03:26,421 DEBUG [GBeanInstanceState] GBeanInstanceState for:
 org.apache.geronimo.configs/j2ee-system/2.1-SNAPSHOT/car?configurationName=org.apache.geronimo.configs/j2ee-system/2.1-SNAPSHOT/car State changed from stopped to starting
21:03:26,484 DEBUG [Configuration] ClassLoader structure for configuration org.apache.geronimo.configs/j2ee-system/2.1-SNAPSHOT/car
Parent configurations:
ClassPath:
     file:/C:/geronimo/repository/org/apache/geronimo/modules/geronimo-common/2.1-SNAPSHOT/geronimo-common-2.1-SNAPSHOT.jar
     file:/C:/geronimo/repository/org/apache/geronimo/modules/geronimo-system/2.1-SNAPSHOT/geronimo-system-2.1-SNAPSHOT.jar
     file:/C:/geronimo/repository/org/apache/geronimo/modules/geronimo-util/2.1-SNAPSHOT/geronimo-util-2.1-SNAPSHOT.jar
     file:/C:/geronimo/repository/asm/asm/2.2.3/asm-2.2.3.jar
     file:/C:/geronimo/repository/asm/asm-commons/2.2.3/asm-commons-2.2.3.jar
     file:/C:/geronimo/repository/commons-cli/commons-cli/1.0/commons-cli-1.0.jar
     file:/C:/geronimo/repository/jline/jline/0.9.91/jline-0.9.91.jar
     file:/C:/geronimo/repository/commons-jexl/commons-jexl/1.1/commons-jexl-1.1.jar
     file:/C:/geronimo/repository/ognl/ognl/2.6.9/ognl-2.6.9.jar
     file:/C:/geronimo/repository/javax/xml/bind/jaxb-api/2.0/jaxb-api-2.0.jar
     file:/C:/geronimo/repository/com/sun/xml/bind/jaxb-impl/2.0.5/jaxb-impl-2.0.5.jar
     file:/C:/geronimo/repository/org/apache/geronimo/specs/geronimo-stax-api_1.0_spec/1.0/geronimo-stax-api_1.0_spec-1.0.jar
     file:/C:/geronimo/repository/woodstox/wstx-asl/3.2.1/wstx-asl-3.2.1.jar
     file:/C:/geronimo/repository/org/apache/geronimo/specs/geronimo-activation_1.1_spec/1.0/geronimo-activation_1.1_spec-1.0.jar
...
21:03:56,171 INFO  [startup] Assembling app: C:\.m2\org\apache\geronimo\applications\geronimo-mejb\2.1-SNAPSHOT\geronimo-mejb-2.1-SNAPSHOT.jar
21:03:56,359 INFO  [startup] Jndi(name=ejb/mgmt/MEJBRemoteHome) --> Ejb(deployment-id=mejb/ejb/mgmt/MEJB)
21:03:56,359 INFO  [startup] Created Ejb(deployment-id=mejb/ejb/mgmt/MEJB, ejb-name=ejb/mgmt/MEJB, container=Default Stateless Container)
21:03:56,375 INFO  [startup] Deployed Application(path=C:\.m2\org\apache\geronimo\applications\geronimo-mejb\2.1-SNAPSHOT\geronimo-mejb-2.1-SNAPSHOT.jar)
Geronimo startup complete

Komunikat Geronimo startup complete wskazuje na pomyślne uruchomienie Geronimo.

Instalacja sterownika JDBC dla PostgreSQL - postgresql-8.2-506.jdbc3.jar

Kolejnym krokiem podczas uruchomienia aplikacji przemysłowej TicketServiceEAR jest instalacja sterownika JDBC dla PostgreSQL - postgresql-8.2-506.jdbc3.jar, który znajduje się w katalogu jdbc w katalogu domowym PostgreSQL. W tym celu skorzystamy z konsoli administracyjnej Geronimo dostępnej pod adresem http://localhost:8080/console (konto administracyjne to system z hasłem manager).

Grafika:geronimo-mdb-jpa-jta-console.png

Wybieramy z menu po lewej pozycję Services > Repository, a następnie wciskamy przycisk Browse..., co spowoduje otworzenie okienka z katalogami, w którym wskazujemy na plik postgresql-8.2-506.jdbc3.jar. Poprawiamy dane do tych widocznych na poniższym obrazku (poprawne wypełnienie wartości ma znaczenie, gdyż użyliśmy ich w pliku konfiguracyjnym geronimo-application.xml, gdzie definiowaliśmy pulę połączeń do bazy danych).

Grafika:geronimo-mdb-jpa-jta-postgresqldriver.png

Zatwierdzamy formularz przyciskiem Install. Na liście Current Repository Entries powinna pojawić się pozycja reprezentująca sterownik JDBC dla PostgreSQL - postgresql/postgresql/8.2-506.jdbc3/jar (lista posortowana jest alfabetycznie, więc pozycji należy szukać na samym końcu).

Na konsoli Geronimo powinny pojawić się następujące komunikaty:

21:51:47,890 INFO  [RepositoryViewPortlet] Copying into repository postgresql/postgresql/8.2-506.jdbc3/jar...
21:51:47,890 INFO  [RepositoryViewPortlet] Finished.

Alternatywnie (acz niskopoziomowo) instalację moglibyśmy przeprowadzić po prostu kopiując plik sterownika postgresql-8.2-506.jdbc3.jar do odpowiedniego podkatalogu w katalogu repository w katalogu domowym Geronimo (w tym przypadku jest to repository/postgresql/postgresql/8.2-506.jdbc3/).

Instalacja aplikacji korporacyjnej

Instalację aplikacji korporacyjnej TicketServiceEAR.ear możnaby przeprowadzić na kilka sposobów, z których skorzystanie z konsoli administracyjnej Geronimo (poprzez menu Applications > Deploy New) wydaje się być najbardziej trywialne. Wybierzemy jednak inny sposób, bardziej zaawansowany, przy pomocy skryptu deploy.sh (bądź deploy.bat na MS Windows).

jlaskowski@dev /cygdrive/c/geronimo
$ ./bin/deploy.sh --user system --password manager deploy TicketServiceEAR.ear
Using GERONIMO_BASE:   c:\geronimo
Using GERONIMO_HOME:   c:\geronimo
Using GERONIMO_TMPDIR: c:\geronimo\var\temp
Using JRE_HOME:        c:\apps\java5\jre
    Deployed pl.jaceklaskowski.ticketservice/TicketServiceEAR/1.0/ear
      `-> TicketServiceMDB.jar
      `-> jms-resources
      `-> postgresql

Poprawna instalacja kończy się wypisaniem wszystkich zasobów, jakie wchodziły w skład aplikacji. W naszym przypadku jest to moduł ziarna MDB - TicketServiceMDB.jar, zasoby jms-resources oraz postgresql (brakuje modułu JPA, jednakże jest on traktowany przez Geronimo wyłącznie jako biblioteka pomocnicza).

Na konsoli Geronimo pojawią się następujące komunikaty:

22:00:37,234 INFO  [config] Configuring Service(id=Default Stateless Container, type=Container, provider-id=Default Stateless Container)
22:00:37,234 INFO  [config] Configuring Service(id=Default Stateful Container, type=Container, provider-id=Default Stateful Container)
22:00:37,234 INFO  [config] Configuring Service(id=Default BMP Container, type=Container, provider-id=Default BMP Container)
22:00:37,234 INFO  [config] Configuring Service(id=Default CMP Container, type=Container, provider-id=Default CMP Container)
22:00:37,234 INFO  [config] Configuring app: pl.jaceklaskowski.ticketservice/TicketServiceEAR/1.0/ear
22:00:37,421 INFO  [OpenEJB] Auto-deploying ejb TicketServiceBean: EjbDeployment(deployment-id=TicketServiceMDB.jar/TicketServiceBean)
22:00:37,812 INFO  [config] Loaded Module: pl.jaceklaskowski.ticketservice/TicketServiceEAR/1.0/ear
22:00:40,406 INFO  [Enhance] You have enabled runtime enhancement, but have not specified the set of persistent classes.  
OpenJPA must look for metadata for every loaded class, which might increase class load times significantly.
22:00:42,156 INFO  [config] Configuring Service(id=Default MDB Container, type=Container, provider-id=Default MDB Container)
22:00:44,140 INFO  [startup] Assembling app: c:\geronimo\var\temp\geronimo-deploymentUtil2775.jar
22:00:44,203 INFO  [startup] Jndi(name=TicketServiceMDB.jar/TicketServiceBean) --> Ejb(deployment-id=TicketServiceMDB.jar/TicketServiceBean)
22:00:44,421 INFO  [startup] Created Ejb(deployment-id=TicketServiceMDB.jar/TicketServiceBean, ejb-name=TicketServiceBean, 
container=jms-resources.jms-resources-javax.jms.MessageListener)
22:00:44,421 INFO  [startup] Deployed Application(path=c:\geronimo\var\temp\geronimo-deploymentUtil2775.jar)

Uruchomienie aplikacji

W końcu upragniona pora na uruchomienie klienta, który prześle komunikat do kolejki TicketQueue, na której nasłuchuje ziarno MDB - TicketServiceBean.

Projekt klienta tworzony jest przy pomocy Apache Maven 2, w którym zdefiniowano wykonanie klasy klienta pl.jaceklaskowski.ticketservice.TicketServiceClient w konfiguracji wtyczki exec-maven-plugin, więc uruchomienie polecenia mvn compile exec:exec spowoduje jego wykonanie.

$ mvn compile exec:exec
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'exec'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building TicketServiceClient
[INFO]    task-segment: [compile, exec:exec]
[INFO] ----------------------------------------------------------------------------
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Compiling 1 source file to c:\mdb-jpa-jta-postgresql-geronimo\TicketServiceClient-M2\target\classes
[INFO] [exec:exec]
[INFO] Wysylana wiadomosc nr #0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6 seconds
[INFO] Finished at: Sat Oct 27 22:36:10 CEST 2007
[INFO] Final Memory: 9M/254M
[INFO] ------------------------------------------------------------------------

Wykonanie klienta (wysłanie pojedyńczej wiadomości) spowoduje wywołanie ziarna MDB - TicketServiceMDB, co można zaobserwować na konsoli Geronimo.

2007-10-27 22:36:10 pl.jaceklaskowski.ticketservice.ejb.TicketServiceBean initialize
INFO: Wykonano PostConstruct - kontekst przekazany (wstrzelony)? true
22:36:11,343 INFO  [Transaction] TX Required: Started transaction org.apache.geronimo.transaction.manager.TransactionImpl@7c8c05
22:36:11,359 INFO  [OpenEJB] invoking method onMessage on TicketServiceMDB.jar/TicketServiceBean
2007-10-27 22:36:11 pl.jaceklaskowski.ticketservice.ejb.TicketServiceBean sprawdzKtoWykonujeMetodeBiznesowa
INFO: Metoda przechwytująca wykonana - wywołujący: Unauthenticated
2007-10-27 22:36:11 pl.jaceklaskowski.ticketservice.ejb.TicketServiceBean onMessage
INFO: Odebrano komunikat: Wiadomosc od Jacka o numerze #0
22:36:11,718 INFO  [Runtime] Starting OpenJPA 1.0.0
22:36:11,812 INFO  [JDBC] Using dictionary class "org.apache.openjpa.jdbc.sql.PostgresDictionary".
22:36:15,250 INFO  [Transaction] TX Required: Committing transaction org.apache.geronimo.transaction.manager.TransactionImpl@7c8c05
22:36:15,406 ERROR [Transaction] Please correct the integration and supply a NamedXAResource
java.lang.IllegalStateException: Cannot log transactions as org.apache.activemq.ra.LocalAndXATransaction@ccc170 is not a NamedXAResource.
        at org.apache.geronimo.transaction.manager.TransactionImpl$TransactionBranch.getResourceName(TransactionImpl.java:697)
        at org.apache.geronimo.transaction.log.HOWLLog.prepare(HOWLLog.java:254)
        at org.apache.geronimo.transaction.log.HOWLLog$$FastClassByCGLIB$$7315be2e.invoke(<generated>)
        at net.sf.cglib.reflect.FastMethod.invoke(FastMethod.java:53)
        at org.apache.geronimo.gbean.runtime.FastMethodInvoker.invoke(FastMethodInvoker.java:38)
        at org.apache.geronimo.gbean.runtime.GBeanOperation.invoke(GBeanOperation.java:124)
        at org.apache.geronimo.gbean.runtime.GBeanInstance.invoke(GBeanInstance.java:830)
        at org.apache.geronimo.gbean.runtime.RawInvoker.invoke(RawInvoker.java:57)
        at org.apache.geronimo.kernel.basic.RawOperationInvoker.invoke(RawOperationInvoker.java:35)
        at org.apache.geronimo.kernel.basic.ProxyMethodInterceptor.intercept(ProxyMethodInterceptor.java:96)
        at org.apache.geronimo.gbean.GBeanLifecycle$$EnhancerByCGLIB$$b5a58fd8.prepare(<generated>)
        at org.apache.geronimo.transaction.manager.TransactionImpl.internalPrepare(TransactionImpl.java:444)
        at org.apache.geronimo.transaction.manager.TransactionImpl.commit(TransactionImpl.java:316)
        at org.apache.geronimo.transaction.manager.TransactionManagerImpl.commit(TransactionManagerImpl.java:245)
        at org.apache.openejb.core.transaction.TransactionPolicy.commitTransaction(TransactionPolicy.java:140)
        at org.apache.openejb.core.transaction.TxRequired.afterInvoke(TxRequired.java:75)
        at org.apache.openejb.core.mdb.MdbContainer.afterDelivery(MdbContainer.java:388)
        at org.apache.openejb.core.mdb.EndpointHandler.afterDelivery(EndpointHandler.java:274)
        at org.apache.openejb.core.mdb.EndpointHandler.invoke(EndpointHandler.java:164)
        at $Proxy41.afterDelivery(Unknown Source)
        at org.apache.activemq.ra.MessageEndpointProxy$MessageEndpointAlive.afterDelivery(MessageEndpointProxy.java:126)
        at org.apache.activemq.ra.MessageEndpointProxy.afterDelivery(MessageEndpointProxy.java:65)
        at org.apache.activemq.ra.ServerSessionImpl.afterDelivery(ServerSessionImpl.java:216)
        at org.apache.activemq.ActiveMQSession.run(ActiveMQSession.java:751)
        at org.apache.activemq.ra.ServerSessionImpl.run(ServerSessionImpl.java:165)
        at org.apache.geronimo.connector.work.WorkerContext.run(WorkerContext.java:290)
        at org.apache.geronimo.connector.work.pool.NamedRunnable.run(NamedRunnable.java:32)
        at org.apache.geronimo.pool.ThreadPool$1.run(ThreadPool.java:214)
        at org.apache.geronimo.pool.ThreadPool$ContextClassLoaderRunnable.run(ThreadPool.java:344)
        at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)
        at java.lang.Thread.run(Thread.java:595)

Wyjątek java.lang.IllegalStateException: Cannot log transactions as org.apache.activemq.ra.LocalAndXATransaction@7609be is not a NamedXAResource jest jedynie informacją o niedoskonałej integracji monitora transakcji między projektami Apache Geronimo oraz Apache OpenEJB i jako, że nie powoduje faktycznie problemu (poza swoim wyświetleniem) może być zignorowany.

Dla pełnego obrazu sytuacji należałoby sprawdzić tabelę Komunikat w bazie danych PostgreSQL, gdzie ziarno MDB TicketServiceBean umieszcza przechwycone komunikaty.

jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2
$ ./bin/psql sandbox
Welcome to psql 8.2.5, the PostgreSQL interactive terminal.

Type:  \copyright for distribution terms
       \h for help with SQL commands
       \? for help with psql commands
       \g or terminate with semicolon to execute query
       \q to quit

Warning: Console code page (852) differs from Windows code page (1250)
         8-bit characters may not work correctly. See psql reference
         page "Notes for Windows users" for details.
sandbox=# \dt komunikat
 public | komunikat | table | geronimo
sandbox=# select * from komunikat;
  1 | 2007-10-27 | Wiadomosc od Jacka o numerze #0

Rekord w tabeli komunikat w bazie danych PostgreSQL świadczy o poprawnym wykonaniu aplikacji (i świadczy o niskiej ważności wyjątku java.lang.IllegalStateException: Cannot log transactions as org.apache.activemq.ra.LocalAndXATransaction@7609be is not a NamedXAResource).

Na zakończenie można wykonać odinstalowanie aplikacji TicketServiceEAR, aby zaobserwować wykonanie metody przechwytującej - interceptora @PreDestroy ziarna TicketServiceBean. Wykonanie polecenia undeploy wymaga podania nazwy modułu, którą deklarowaliśmy w sekcji moduleId w pliku geronimo-application.xml.

jlaskowski@dev /cygdrive/c/geronimo
$ ./bin/deploy.sh --user system --password manager undeploy pl.jaceklaskowski.ticketservice/TicketServiceEAR/1.0/ear
Using GERONIMO_BASE:   c:\geronimo
Using GERONIMO_HOME:   c:\geronimo
Using GERONIMO_TMPDIR: c:\geronimo\var\temp
Using JRE_HOME:        c:\apps\java5\jre
    Module pl.jaceklaskowski.ticketservice/TicketServiceEAR/1.0/ear
    unloaded.
    Module pl.jaceklaskowski.ticketservice/TicketServiceEAR/1.0/ear
    uninstalled.

    Undeployed pl.jaceklaskowski.ticketservice/TicketServiceEAR/1.0/ear
      `-> TicketServiceMDB.jar
      `-> jms-resources
      `-> postgresql

Na konsoli Geronimo powinny pojawić się następujące komunikaty, a wśród nich i komunikat z metody przechwytującej @PreDestroy:

23:53:12,515 INFO  [startup] Undeploying app: c:\geronimo\var\temp\geronimo-deploymentUtil63712.jar
2007-10-27 23:53:12 pl.jaceklaskowski.ticketservice.ejb.TicketServiceBean destroy
INFO: Wykonano PreDestroy
23:53:12,578 INFO  [DirectoryMonitor] Hot deployer notified that an artifact was removed: pl.jaceklaskowski.ticketservice/TicketServiceEAR/1.0/ear
Osobiste