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>
Follow

Get every new post delivered to your Inbox.