JBoss AS 7 – Remote JMS queue

Today I’d like to post a quick, laconic, and hopefully 100% correct sample of remote JMS queue utilization in JBoss 7 AS. Diagram posted below shows the target environment where one application server instance hosts JMS queue, and the other one acts as message producer and consumer.

Remote JMS Environment

JBoss2 (queue owner) configuration:

<hornetq-server>
   <security-domain>hornetq-security-domain</security-domain>
   ...
   <jms-destinations>
      <jms-queue name="RemoteQueue1">
         <entry name="queue/RemoteQueue1"/>
         <entry name="java:jboss/exported/jms/queue/RemoteQueue1"/>
      </jms-queue>
   </jms-destinations>
</hornetq-server>
...
<security-domains>
   ...
   <security-domain name="hornetq-security-domain" cache-type="default">
      <authentication>
         <login-module code="UsersRoles" flag="required">
            <module-option name="usersProperties" value="/opt/jboss3/standalone/configuration/jms-users.properties"/>
            <module-option name="rolesProperties" value="/opt/jboss3/standalone/configuration/jms-roles.properties"/>
         </login-module>
       </authentication>
   </security-domain>
</security-domains>
[jboss@jmsenv Desktop]$ cat /opt/jboss3/standalone/configuration/jms-users.properties
jmsuser=jmspassword
[jboss@jmsenv Desktop]$ cat /opt/jboss3/standalone/configuration/jms-roles.properties
jmsuser=guest

The security related entries are not required if you decide to assign unauthorized users to “guest” group (which is by default allowed to send and receive messages). This can be achieved by adding <module-option name="unauthenticatedIdentity" value="guest"/> to login module configuration.

JBoss1 (queue user) configuration:

<hornetq-server>
   ...
   <connectors>
      <connector name="remote-jms-nonmgmt">
         <factory-class>org.hornetq.core.remoting.impl.netty.NettyConnectorFactory</factory-class>
         <param key="host" value="192.168.117.159"/>
         <param key="port" value="5445"/>
      </connector>
      <netty-connector name="remote-jms-mgmt" socket-binding="remote-jms-binding"/>
   </connectors>
   <jms-connection-factories>
      ...
      <connection-factory name="ConnectionFactory1NonMgmt">
         <connectors>
            <connector-ref connector-name="remote-jms-nonmgmt"/>
         </connectors>
         <entries>
            <entry name="java:/ConnectionFactory1NonMgmt"/>
         </entries>
      </connection-factory>
      <pooled-connection-factory name="ConnectionFactory1Mgmt">
         <user>jmsuser</user>
         <password>jmspassword</password>
         <connectors>
            <connector-ref connector-name="remote-jms-mgmt"/>
         </connectors>
         <entries>
            <entry name="java:/ConnectionFactory1Mgmt"/>
         </entries>
      </pooled-connection-factory>
   </jms-connection-factories>
</hornetq-server>
...
<socket-binding-group name="full-ha-sockets" default-interface="public">
   ...
   <outbound-socket-binding name="remote-jms-binding">
      <remote-destination host="192.168.117.159" port="5445"/>
   </outbound-socket-binding>
</socket-binding-group>

Producer code:

@Resource(mappedName = "java:/ConnectionFactory1Mgmt")
private ConnectionFactory connectionFactory;

public void sendMessage(String text) {
   Connection connection = connectionFactory.createConnection();
   Session session = connection.createSession( false, Session.AUTO_ACKNOWLEDGE );
   Queue queue = session.createQueue( "RemoteQueue1" );
   MessageProducer producer = session.createProducer( queue );
   connection.start();
   TextMessage message = session.createTextMessage();
   message.setText( text );
   producer.send( message );
   session.close();
   connection.close();
}

Message-Driven Bean consumer code:

import org.jboss.ejb3.annotation.ResourceAdapter;

@ResourceAdapter("ConnectionFactory1Mgmt")
@MessageDriven(
   name="ConsumerMdb",
   activationConfig = {
      @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"),
      @ActivationConfigProperty(propertyName="destination", propertyValue="queue/RemoteQueue1")
})
public class ConsumerMdb implements MessageListener {
   ...
}

Here you might need to import org.jboss.ejb3:jboss-ejb3-ext-api:2.0.0 library into you project, but don’t forget to mark it as provided by the runtime environment.

Manually consuming messages from EJB (inside container):

InitialContext context = new InitialContext();
ConnectionFactory connectionFactory = (ConnectionFactory) context.lookup( "ConnectionFactory1NonMgmt" );
Connection connection = connectionFactory.createConnection( "jmsuser", "jmspassword" );
Session session = connection.createSession( false, Session.AUTO_ACKNOWLEDGE );
Queue queue = session.createQueue( "RemoteQueue1" );
MessageConsumer consumer = session.createConsumer( queue );
connection.start();
Message message = consumer.receive();

General notes:

  1. Use <pooled-connection-factory> for MDBs and message producers deployed inside container.
  2. Use <connection-factory> for non-managed producers and consumers (including the remote once).

Reading:

  1. JBoss forum thread: https://community.jboss.org/message/722711.
  2. Justin Bertram explaining my doubts (thanks!): https://community.jboss.org/message/780778.
Advertisements

TopConf 2012

Wszyscy wierzymy w bezstronność serwisu www.random.org, jednakże po raz drugi z rzędu za jego sprawą (oraz liderów WJUG) udało mi się wyjechać na zagraniczną konferencję. Tegorocznym celem okazał się Tallinn i odbywający się tam w pierwszych dniach listopada event TopConf 2012. 31 października wyruszyłem w osiemnastogodzinną podróż autokarem do stolicy Estonii. Wszelkie komentarze uprzedzę stwierdzeniem, że nie znalazłem dogodnego połączenia lotniczego gwarantującego mi uczestnictwo w porannych wykładach pierwszego dnia imprezy. Pod względem organizacyjnym konferencja okazała się bliźniaczo podobna do ubiegłorocznej GOTO Prague, relacje z której znajdziecie na niniejszym blogu. Dość jednak słów na temat logistyki – przejdźmy do opisu wyróżniających się wykładów.

Dzień pierwszy

Konferencję oficjalnie rozpoczął John-Henry Harris opisując proces projektowania modeli zabawek w firmie Lego. Wystąpienie utrzymane w bardzo zabawnym stylu, pokazujące istotę kreatywności oraz sposobność czerpania inspiracji z otaczającego Nas świata (np. koparka). Lego kładzie ogromny nacisk na jakość wypuszczanych produktów oraz ich perfekcyjne wykonanie. Modele powinny wytrzymywać wysokie temperatury, co weryfikowane jest poprzez opiekanie gotowego prototypu w piekarniku :).

Głodny wiedzy technicznej, postanowiłem jeszcze raz usłyszeć Vaclav’a Pech’a i “Unleash your processor(s)”. Prezentacja nie zmieniła się przez ostatni rok, jednakże zachęciła mnie do upgrade’u grubych klientów zaimplementowanych w Java’ie do JDK7 i skorzystania z Fork/Join Thread Pools. W implementacji Fork/Join każdy wątek posiada swoją własną kolejkę zadań, przez co wykonywanie dużej ilości krótkich task’ów nie powoduje opóźnień wynikających z blokowania końca współdzielonej kolejki (charakterystyka poprzednich wersji JDK i Executors#newFixedThreadPool(int)). BTW, organizator udostępnił niektóre prezentacje na głównej stronie konferencji w dziale “Conference program”.

Na następnej prelekcji Jakub Nabrdalik przestrzegał przed pułapkami, jakie niesie za sobą niewłaściwie stosowany Test Driven Development. Część uwag, takich jak np. Assertion Roulette, znałem już wcześniej. Inne, odnoszące się do ciągłego mock’owania serwisów zależnych, pisania jedynie testów integracyjnych oraz nadmiernego zliczania wywołań funkcji, wydały mi się sensowne i postaram się kierować nimi na co dzień.

Hitem dnia okazała się dla mnie prezentacja Alvin’a Richards’a “MongoDB Design Decisions and Use Cases”. Wystąpienie miało na celu ogólne przedstawienie produktu oraz jego możliwości. Do najciekawszych zaliczyłbym niemal liniową skalowalność, różne poziomy commit’owania (od fire & forget, przez znane z RDBMS redo log, aż po replikację zmian na zapasowe instancje) i automatyczną reallokację danych w celu zrównoważenia obciążenia wewnątrz klastra. Dzięki zastosowanemu modelowi danych JSON, MongoDB nie musi implementować rozproszonych operacji JOIN ani standardu 2PC, gdyż wszystkie dane powiązane z konkretnym dokumentem przechowywane są na jednym węźle. Alvin zdradził, że pięć instytucji finansowych zaimplementowało swoje systemy typu ERP lub CSM w oparciu o MongoDB.

Bardzo mieszane uczucia zawładnęły mną po wysłuchaniu prezentacji “What’s new in JEE 6. Spring ends, Summer comes?” Michała Szkopińskiego. Wystąpienie zawierało kilka ciekawych informacji, takich jak dążenie Oracle i IBM do konsolidacji JVM w wersji 8, nowe podejście odnośnie deployment’u aplikacji JEE7 w chmórze (automatyczne tworzenie i konfiguracja serwisów JDBC i LDAP – dobrze zrozumiałem?), oraz rozszerzanie zasobów w przypadku przekroczenia zdefiniowanego SLA. Z technicznego punktu widzenia prezentacja pozostawiała wiele do życzenia – odejście od deskryptorów XML w stronę adnotacji, @Stateless, @PostConstruct i @PreDestroy nie należą do nowości w JEE6.

Dzień zakończyłem około północy w jednym z Tallinnskich pubów wraz z grupą około dwudziestu uczestników konferencji :).

Dzień drugi

Drugi dzień konferencji rozpocząłem od wysłuchania prezentacji Alvin’a Richards’a “Scaling for Humongous amounts of data with MongoDB”. Prawdę mówiąc nie maiłem wcześniej styczności z MongoDB, dlatego też wystąpienie przybrało dla mnie charakter bardziej hasłowy. Alvin pokazywał różne podejścia do tworzenia modeli danych JSON wraz z ich pozytywnymi oraz negatywnymi konsekwencjami. Wybór konkretnej reprezentacji danych (np. zagnieżdżanie vs. link’owanie powiązanych fragmentów dokumentu) znacząco wpływa na wydajność zapytań, ich liczbę oraz sposób wykonania. Mam nadzieję, że prezentacja zostanie zamieszczona na stronie konferencji i będę mógł się z nią jeszcze raz zapoznać podczas nauki MongoDB.

Na uwagę zasługiwała też trzygodzinna ścieżka poświęcona bezpieczeństwu systemów informatycznych. Podobno uczestnicy na żywo włamywali się na publiczne strony internetowe. Żałuję obecnie, że wybrałem inną serię wykładów. Polecam także przejrzenie listy przejętych stron na www.zone-h.org/archive oraz zabawę exploit’ami z Armitage :). Na konferencję zdecydowanie opłacało się zabrać własny komputer, gdyż organizatorzy w ramach konkursu udostępnili wewnętrzny, specjalnie spreparowany serwis do hack’owania.

Konferencję zakończył Dan North luźną, aczkolwiek pouczającą, prezentacją “Simplicity: the Way of the Unusual Architect”. Podczas implementacji nowych rozwiązań nie należy od razu opierać się o standardowy stos technologii J2EE. Kluczowe pytanie, na które powinno się udzielić odpowiedzi, to co muszę zrobić aby w najkrótszym czasie osiągnąć zamierzony cel. Proste rozwiązania z reguły okazują się najbardziej skuteczne :).

Pragnę jeszcze raz serdecznie podziękować WJUG i Marcinowi Zajączkowskiemu za umożliwienie mi wyjazdu na TopConf 2012.

JAX-WS i WS-Addressing

Standard WS-Addressing wykorzystuje się powszechnie w przypadku asynchronicznej komunikacji między dwoma Web Service’ami. Nadawca wiadomości określa w nagłówku komunikatu SOAP docelowy adres (endpoint), na który dostarczyć należy odpowiedź po ukończeniu długotrwałego przetwarzania. Standard JAX-WS wspiera WS-Addressing co pokrótce zaprezentuję w niniejszym tutorial’u.

Kod źródłowy asynchronicznego serwisu zamieściłem poniżej. Listingi zawierają wszystkie instrukcje typu import, ponieważ większość z wykorzystywanych klas znajduje się zarówno w pakiecie com.sun.xml.internal.ws.api, jak i com.sun.xml.ws.api. Odwoływanie się do niewłaściwej klasy skutkuje pojawieniem się stosownego komunikatu o błędzie podczas deploy’mentu. Przykładowa aplikacja zostanie uruchomiona na serwerze aplikacyjnym WebLogic 10.3.5.

import javax.jws.Oneway;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService(serviceName = "svSampleService",
            targetNamespace = "http://lantoniak.edu/wsaddressing/service",
            portName = "prSampleService")
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT, parameterStyle=SOAPBinding.ParameterStyle.BARE)
public interface SampleService {
   @Oneway
   public void initiateAccountCreation(Client client);
}
import com.sun.xml.ws.api.SOAPVersion;
import com.sun.xml.ws.api.addressing.AddressingVersion;
import com.sun.xml.ws.api.addressing.OneWayFeature;
import com.sun.xml.ws.api.addressing.WSEndpointReference;
import com.sun.xml.ws.api.message.HeaderList;
import com.sun.xml.ws.api.message.Headers;
import com.sun.xml.ws.developer.JAXWSProperties;
import com.sun.xml.ws.developer.WSBindingProvider;
import edu.lantoniak.wsaddressing.Account;
import edu.lantoniak.wsaddressing.Client;
import edu.lantoniak.wsaddressing.SampleCallback;
import edu.lantoniak.wsaddressing.SampleService;
import org.apache.log4j.Logger;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.jws.WebService;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Service;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.soap.Addressing;
import java.net.URL;

@Stateless
@WebService(endpointInterface="edu.lantoniak.wsaddressing.SampleService",
            serviceName = "svSampleService",
            targetNamespace = "http://lantoniak.edu/wsaddressing/service",
            portName = "prSampleService")
@Addressing(enabled = true, required = true)
public class SampleServiceImpl implements SampleService {
   private static final Logger log = Logger.getLogger(SampleServiceImpl.class);

   @Resource
   private WebServiceContext context;

   @Override
   public void initiateAccountCreation(Client client) {
      // Get message details...
      HeaderList hl = (HeaderList) context.getMessageContext().get(JAXWSProperties.INBOUND_HEADER_LIST_PROPERTY);
      WSEndpointReference reference = hl.getReplyTo(AddressingVersion.W3C, SOAPVersion.SOAP_11);
      String messageId = hl.getMessageID(AddressingVersion.W3C, SOAPVersion.SOAP_11);
      String callbackAddress = reference.getAddress();
      log.info("Received message id: " + messageId);
      log.info("Received callback address: " + callbackAddress);

      // Process something...
      Account account = new Account();
      account.setType("lukasz".equalsIgnoreCase(client.getName()) ? "saving" : "standard");

      // Send response...
      try {
         Service service = Service.create(new URL(reference.getAddress() + "?WSDL"),
                                          new QName("http://lantoniak.edu/wsaddressing/service", "svSampleCallback"));
         SampleCallback callbackPort = service.getPort(new QName("http://lantoniak.edu/wsaddressing/service", "prSampleCallback"),
                                                       SampleCallback.class, new OneWayFeature());
         WSBindingProvider bindingProvider = (WSBindingProvider) callbackPort;
         bindingProvider.setAddress(reference.getAddress());
         bindingProvider.setOutboundHeaders(Headers.create(AddressingVersion.W3C.relatesToTag, messageId));
         bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, reference.getAddress());

         callbackPort.reviewAccountDetails(account);
      } catch (Exception e) {
         log.error("Error while calling callback operation.", e);
         throw new WebServiceException(e);
      }
   }
}

Metoda SampleServiceImpl#initiateAccountCreation(Client) pobiera identyfikator komunikatu i docelowy endpoint, a następnie przetwarza otrzymaną wiadomość oraz kieruje odpowiedź na zadany adres (wywołuje zdalny Web Service). Poza serwisem, zaimplementować należy także sam interfejs (bądź kompletną usługę) typu callback, patrz linia 56. Główną metodę serwisu oraz callback oznaczyłem adnotacją @Oneway, aby proces konsumenta nie oczekiwał na zwrócenie wyniku.

import javax.jws.Oneway;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService(serviceName = "svSampleCallback",
            targetNamespace = "http://lantoniak.edu/wsaddressing/service",
            portName = "prSampleCallback")
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT, parameterStyle=SOAPBinding.ParameterStyle.BARE)
public interface SampleCallback {
   @Oneway
   public void reviewAccountDetails(Account account);
}
@Stateless
@WebService(endpointInterface="edu.lantoniak.wsaddressing.SampleCallback",
            serviceName = "svSampleCallback",
            targetNamespace = "http://lantoniak.edu/wsaddressing/service",
            portName = "prSampleCallback")
public class SampleCallbackImpl implements SampleCallback {
   private static final Logger log = Logger.getLogger(SampleCallbackImpl.class);

   @Override
   public void reviewAccountDetails(Account account) {
      log.info("Received new account type " + account.getType() + ".");
   }
}

Zaimplementowaną usługę wywołałem z poziomu SoapUI. Skorzystałem przy tym z zakładki “WS-A” w celu określenia docelowego miejsca wysłania odpowiedzi. Atrybut “Replay to:” ustawiłem na adres serwisu typu callback zdeploy’owanego na serwerze aplikacyjnym (http://192.168.56.1:7001/SampleCallbackImpl/svSampleCallback). Po wysłaniu żądania SOAP, w log’ach WLS otrzymałem następujące komunikaty:

INFO SampleServiceImpl:49 - Received message id: uuid:327aa8e3-6a1e-4cc2-8c17-2c71ff92205e
INFO SampleServiceImpl:50 - Received callback address: http://192.168.56.1:7001/SampleCallbackImpl/svSampleCallback
INFO SampleCallbackImpl:23 - Received new account type saving.

Twórcy narzędzia SoapUI wyposażyli je w funkcję “Mock Service”, za pomocą której w łatwy sposób zasymulować można przykładowy serwis na podstawie kontraktu WSDL.

Wywołanie asynchronicznego serwisu z poziomu SoapUI (odpowiedź kierowana do usługi działającej na serwerze WebLogic):

Wywołanie asynchronicznego serwisu z poziomu SoapUI (odpowiedź kierowana do tzw. Mock Service):

Przykładowy komunikat SOAP wysłany przez SoapUI do WLS:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://lantoniak.edu/wsaddressing/service">
   <soapenv:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
      <wsa:Action>http://lantoniak.edu/wsaddressing/service/SampleService/initiateAccountCreation</wsa:Action>
      <wsa:ReplyTo>
         <wsa:Address>http://localhost:8088/prSampleCallbackBinding</wsa:Address>
      </wsa:ReplyTo>
      <wsa:MessageID>uuid:73c98c07-a9bc-47ab-b1f5-0ae216ea8f64</wsa:MessageID>
      <wsa:To>http://192.168.56.1:7001/SampleServiceImpl/svSampleService</wsa:To>
   </soapenv:Header>
   <soapenv:Body>
      <ser:initiateAccountCreation>
         <id>1</id>
         <name>Lukasz</name>
      </ser:initiateAccountCreation>
   </soapenv:Body>
</soapenv:Envelope>

Przykładowy komunikat SOAP wysłany przez WLS do Mock Service SoapUI (zwróć uwagę na powtarzający się identyfikator wiadomości):

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Header>
      <wsa:RelatesTo xmlns:wsa="http://www.w3.org/2005/08/addressing">uuid:73c98c07-a9bc-47ab-b1f5-0ae216ea8f64</wsa:RelatesTo>
   </S:Header>
   <S:Body>
      <ns2:reviewAccountDetails xmlns:ns2="http://lantoniak.edu/wsaddressing/service">
         <type>saving</type>
      </ns2:reviewAccountDetails>
   </S:Body>
</S:Envelope>

Kompletny kod źródłowy aplikacji znajduje się tutaj.

GOTO Prague 2011

GOTO Prague LogoCzwartego października otrzymałem mail’a od Marcina Zajączkowskiego z zaproszeniem na konferencję GOTO Prague 2011. Pierwotnie darmowa wejściówka WJUG trafiła do Artura Wójcika, lecz okazało się, że nie uda mu się wziąć udziału w imprezie. Zachwycony wiadomością od Marcina, zaplanowałem dwudniowy urlop i przystąpiłem do organizacji wyjazdu.

W poniedziałek 21 listopada wyjechałem pociągiem z Warszawy, aby dotrzeć do centrum Pragi następnego dnia o 07:40 rano. Podróż w wagonie sypialnym wspominam dość komfortowo, chociaż jestem zbyt długi na standardowe łóżka PKP. Miejsce konferencji organizatorzy wybrali znakomicie. Ze względu na lokalizację hotelu Dorint Hotel Don Giovanni zaraz przy stacji metra, nie miałem żadnych problemów z dojazdem.

Szacuję, że w konferencji brało udział maksymalnie 150 osób. Powodów niewielkiej popularności doszukiwałbym się w cenie oraz niedawno zakończonym Devoxx’ie czy też JDD.

Rozpoczęła się pierwsza prezentacja… Erich Gamma i “Design Patterns – Past, Present & Future”. Wystąpienie miało na celu określenie dalszego rozwoju wzorców projektowych oraz ich pozycji w obecnej inżynierii oprogramowania. Nadarzyła się także chwila refleksji, w której to Erich żartobliwie zasugerował wykreślenie Singleton’a ze zbioru wzorców. Często nadmierne jego wykorzystywanie nie prowadzi do odpowiedniej strukturyzacji kodu, a usunięcie Singleton’a lub Mediator’a zajmuje całe tygodnie. Erich wspomniał także o konieczności projektowania przejrzystego API, które jest na tyle stabilne, aby nie ulegało modyfikacji z każdą nową wersją oprogramowania. Z prezentowanego materiału szczególnie przypadł mi do gustu pomysł obsługi żądań asynchronicznych typu: promise.then(completitionHandler, errorHandler, progressHandler). Na koniec wystąpienia Erich wyświetlił listę podstawowych, obecnie najbardziej użytecznych według niego wzorców: Composite, Strategy, State, Command, Tempalte Method, Null Object (nie udało mi się niestety zapisać wszystkich :(). BTW po zakończeniu każdej z prezentacji słuchacze wychodzący z sali mieli możliwość oceny prelekcji poprzez wybranie na ekranie iPhone’a jednej z trzech buziek – uśmiechniętej, poważnej i smutnej. Nie wiem czy organizatorzy opublikowali gdzieś wyniki owej ankiety.

Podczas głównej części konferencji równolegle odbywały się trzy prezentacje w sąsiadujących pomieszczeniach. Wystąpienia podzielono tematycznie na cztery ścieżki: Agile, Architecture, Solution Track oraz Technology. Swojego udziału nie ograniczałem do jednej ścieżki. Pierwszego dnia najbardziej podobała mi się prezentacja Vaclav’a Pech’a “Unleash your processor(s)”. Vaclav zachęcał do wielowątkowego programowania oraz korzystania z biblioteki GPars (http://www.gpars.org). W pamięci utkwił mi następujący fragment “niebezpiecznego” kodu:

public class ClickCounter implements ActionListener {
   public ClickCounter(JButton button) {
      button.addActionListener(this); // ClickCounter instance might not be constructed.
   }

   public void actionPerformed(final ActionEvent e) {
      ...
   }
}

GOTO Prague Bad Words

Na koniec pierwszego dnia konferencji, organizatorzy zaplanowali imprezę integracyjną w odremontowanej piwnicy w centrum Pragi. Wąskie, okrągłe stoły nie sprzyjały integracji w szerszym gronie. Po wypiciu zaledwie jednego piwa opuściłem lokal wraz z czterema wcześniej poznanymi Polakami w celu zwiedzenia Mostu Karola oraz zabawy w lokalnych pubach. Cała wyprawa zakończyła się szczęśliwym powrotem (ostatnim metrem) do hotelu Fortuna Rhea gdzie miałem nocleg.

Drugi dzień konferencji okazał się zdecydowanie bardziej udany. Prelegenci otrzymywali ode mnie prawie same uśmiechnięte buźki :). Na szczególną uwagę zasługuje prezentacja Attila Szegedi’a “JVM performance optimizations at Twitter’s scale” (slajdy do pobrania ze strony konferencji http://gotocon.com/prague-2011/schedule). Po jej wysłuchaniu doszedłem do wniosku, że koniecznie muszę pogłębić wiedzę z zakresu optymalizacji JVM. Wiedza ta przydać się może nie tylko podczas tuningu aplikacji, lecz także w trakcie jej modelowania. Attila uświadomił mi, iż dziedziczenie klas niesie za sobą narzut pamięciowy.

Konferencja powoli dobiegała końca, gdy na scenę wyszedł Kevlin Henney z prezentacją “Cool & Useless”. Wystąpienie to porównać można do naprawdę niezłego kabaretu. Kevlin pokazywał cały szereg przykładów ciekawych, lecz w znacznym stopniu bezużytecznych projektów oraz fragmentów kodu. Szczególnie w pamięci zapadł mi następujący algorytm sortowania, mistrzostwo świata :).

#!/bin/bash
function f() {
   sleep "$1"
   echo "$1"
}

while [ -n "$1" ]
do
   f "$1" &
   shift
done
wait

Do Warszawy wyjechałem jeszcze tego samego dnia wieczorem. 24 listopada o 09:00 rano miałem już przed oczami “ukochaną” konsolę WebLogic 11g.

Na koniec pragnę bardzo serdecznie podziękować Marcinowi Zajączkowskiemu za zorganizowanie darmowej wejściówki! Tak trzymać!

Hibernate Envers – Tracking entity names modified during revisions (HHH-5580)

Hibernate Envers is an open-source project that provides historical versioning of application’s entity data. It is based on the idea of entity revisions, which shall be intuitive for every user of source control management tools. Since the release 4.0.0.Beta1 of Hibernate Core, Envers enables users to retrieve modifications applied exactly at a specified revision number without defining concrete entity type. This feature has been requested several times on Hibernate forum (for example here) and reported as a major improvement in the JIRA system (HHH-5580).

By default, Envers does not track entity types that have been modified in each revision. This implies the necessity to query all tables storing audited data in order to retrieve changes made during given revision [1]. While designing the patch for HHH-5580 issue, me and Adam Warski agreed to add an optional table called REVCHANGES, which shall store entity names of modified persistent objects. Single record encapsulates the revision identifier (foreign key to REVINFO table) and a string value [1].

The mechanism of tracking modified entity names can be enabled in three different ways [1]:

  • set org.hibernate.envers.track_entities_changed_in_revision parameter to possitive.
  • extend DefaultTrackingModifiedEntitiesRevisionEntity (instead of standard DefaultRevisionEntity) with your custom revision entity.
  • mark an appropriate field of a custom revision entity with @ModifiedEntityNames annotation. The property is required to be of Set<String> type.

In this blog entry, I am going to describe only the first possibility. Examples of remaining – DefaultTrackingModifiedEntitiesRevisionEntity superclass and @ModifiedEntityNames annotation – can be found in the Envers documentation and test cases (here and here).

For tutorial purpose I will use Envers demo which source code is freely available on the Hiernate Git repository (https://github.com/hibernate/hibernate-core/tree/master/hibernate-envers/src/demo). After enabling org.hibernate.envers.track_entities_changed_in_revision parameter in persistence.xml file and running TestConsole main class, REVCHANGES table is created and populated with the following content:

REV  ENTITYNAME
----------------------------------------
54  org.hibernate.envers.demo.Person
54  org.hibernate.envers.demo.Address

Enabling default mechanism of tracking modified entity names allows user to utilize the following methods exposed by CrossTypeRevisionChangesReader (available from AuditReader) interface [1]:

  • Set<Pair<String, Class>> findEntityTypes(Number revision) – Returns set of entity names and corresponding Java classes modified in a given revision.
  • List<Object> findEntities(Number revision) – Find all entities changed (added, updated and removed) in a specified revision.
  • List<Object> findEntities(Number revision, RevisionType revisionType) – Returns snapshots of all audited entities changed (added, updated or removed) in a given revision filtered by modification type.
  • Map<RevisionType, List<Object>> findEntitiesGroupByRevisionType(Number revision) – Returns a map containing lists of entity snapshots grouped by modification operation (e.g. addition, update and removal).

Please note that some of operations listed above might be expensive in terms of performance. Envers has to execute several queries, which number depends on how many different entity types have changed in a specified revision. JavaDoc and documentation contains detailed runtime complexity description. For example, when retrieving all objects changed during revision 54 (List<Object> modified = getAuditReader().getCrossTypeRevisionChangesReader().findEntities(54);), the following three queries are executed:

Hibernate:
    select
        this_.REV as REV4_0_,
        this_.REVTSTMP as REVTSTMP4_0_,
        modifieden2_.REV as REV4_2_,
        modifieden2_.ENTITYNAME as ENTITYNAME2_
    from
        REVINFO this_
    left outer join
        REVCHANGES modifieden2_
            on this_.REV=modifieden2_.REV
    where
        this_.REV in (
            ?
        )
Hibernate:
    select
        address_au0_.id as id2_,
        address_au0_.REV as REV2_,
        address_au0_.REVTYPE as REVTYPE2_,
        address_au0_.flatNumber as flatNumber2_,
        address_au0_.houseNumber as houseNum5_2_,
        address_au0_.streetName as streetName2_
    from
        Address_AUD address_au0_
    where
        address_au0_.REV=?
Hibernate:
    select
        person_aud0_.id as id3_,
        person_aud0_.REV as REV3_,
        person_aud0_.REVTYPE as REVTYPE3_,
        person_aud0_.name as name3_,
        person_aud0_.surname as surname3_,
        person_aud0_.address_id as address6_3_
    from
        Person_AUD person_aud0_
    where
        person_aud0_.REV=?

The first one returns all entity types modified in a given revision. Remaining SQL statements retrieve essential data from proper audit tables.

From my previous experience with various frameworks, I’ve realized that most of the time the default behavior is not enough. Users, that wish to customize tracking of modified entity types, might want to implement EntityTrackingRevisionListener interface. In this case they shall not enable the default mechanism (by for example leaving negative value of org.hibernate.envers.track_entities_changed_in_revision parameter). EntityTrackingRevisionListener exposes one method that notifies whenever audited entity instance has been added, modified or removed within current revision boundaries [1]. entityChanged() procedure provides user with information about modified entity class and name, its identifier, revision type and revision entity. BTW Community – do you think that active Hibernate session would be usefull as well?

Example implementation of EntityTrackingRevisionListener interface [1]:

public class CustomEntityTrackingRevisionListener implements EntityTrackingRevisionListener {
    @Override
    public void entityChanged(Class entityClass, String entityName,
                              Serializable entityId, RevisionType revisionType,
                              Object revisionEntity) {
        String type = entityClass.getName();
        ((CustomTrackingRevisionEntity)revisionEntity).addModifiedEntityType(type);
    }

    @Override
    public void newRevision(Object revisionEntity) {
    }
}

CustomEntityTrackingRevisionListener shall be passed as the value of @RevisionEntity annotation [1]:

@Entity
@RevisionEntity(CustomEntityTrackingRevisionListener.class)
public class CustomTrackingRevisionEntity {
    @Id
    @GeneratedValue
    @RevisionNumber
    private int customId;

    @RevisionTimestamp
    private long customTimestamp;

    @OneToMany(mappedBy="revision", cascade={CascadeType.PERSIST, CascadeType.REMOVE})
    private Set<ModifiedEntityTypeEntity> modifiedEntityTypes = new HashSet<ModifiedEntityTypeEntity>();

    public void addModifiedEntityType(String entityClassName) {
        modifiedEntityTypes.add(new ModifiedEntityTypeEntity(this, entityClassName));
    }

    ...
}

@Entity
public class ModifiedEntityTypeEntity {
    @Id
    @GeneratedValue
    private Integer id;

    @ManyToOne
    private CustomTrackingRevisionEntity revision;

    private String entityClassName;

    ...
}

To retrieve entity class names modified in a given revision, you have to query for corresponding revision entity and then access modified entity types.

CustomTrackingRevisionEntity ctre = getAuditReader().findRevision(CustomTrackingRevisionEntity.class, 54);
Set<ModifiedEntityTypeEntity> modifiedEntityTypes = ctre.getModifiedEntityTypes();

Please remember to use cascade option in the mapping of @RevisionEntity‘s relations (see CustomTrackingRevisionEntity.modifiedEntityTypes). While persisting each revision entity (master), Envers will not automatically cascade any operation to related objects (details).

In case of implementing EntityTrackingRevisionListener, the execution of AuditReader.getCrossTypeRevisionChangesReader() method will raise an exception. Users have to design and code their own API that retrieves snapshots of entities modified during specified revision.

That’s all folks :). Feel free to comment and post your suggestions.

References:
[1] Hibernate Envers documentation http://docs.jboss.org/hibernate/core/4.0/devguide/en-US/html/ch15.html.

HHH-4073 and so you code…

Dłuższy czas nie pisałem nic na blogu. Na fakt tez złożyło się szereg okoliczności – GeeCON, nawałnica w pracy związana z produkcyjnym uruchomieniem trzyletniego projektu, oraz nowe odcinki seriali Californication i Stargate Universe. Główną przyczyną moich zaniedbań stało się jednak aktywne uczestnictwo w open source’owym projekcie Hibernate Envers (moduł wchodzący w skład Hibernate Core).

Zaczęło się od krótkiego tutorialu dotyczącego serwera kontroli wersji Git, fork’u repozytorium Hibernate oraz ciężkich starć z Gradle. Po około tygodniu zmagań z przygotowaniem środowiska wrzuciłem pierwszą zakończoną sukcesem poprawkę HHH-4073. Nie czekając na oklaski rozwiązywałem dalej HHH-4787, HHH-5276, HHH-5808 oraz HHH-6069. Doszedłem w końcu do pierwszego poważnego pull request’u, w którym to zaimplementowałem (z pomocą team leader’a Adama Warskiego) mechanizm śledzenia zmodyfikowanych, w ramach kolejnych rewizji, encji – https://github.com/hibernate/hibernate-core/pull/84.

Co będzie dalej zobaczymy… Muszę przyznać, że zastanawianie się dobre kilkanaście minut nad pojedynczą linijką kodu oraz wymiana doświadczeń z innymi, bywają bardzo pouczające.

Zdjęcie z GeeCON 2011 (University Day):

Łukasz Antoniak, GeeCON 2011

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>