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.

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.