Introduzione
In questo articolo vediamo come poter eseguire transazioni distribuite (XA Transaction) in applicazioni stand alone, quindi al di fuori di un container J2EE, utilizzando il popolare framework Spring. Queste tipologie di transazioni sono molto comuni in ambienti enterprise, dove sistemi diversi devono essere sincronizzati per raggiungere uno stato consistente. Per ottenere questo obbiettivo abbiamo bisogno di utilizzare un gestore delle transazioni conforme alle specifiche JTA (Java Transaction API) e che quindi supportino il two fase commit, come ad esempio JBossTS, Atomikos and Bitronix.
Two Phase Commit (2PC)
Accenniamo brevemente al funzionamento di tale protocollo. Una transazione distribuita, anche detta XA (eXtended Architecture) Transaction, è caratterizzata da un id globale e tanti id locali (xid) per ogni risorsa XA. La prima fase del protocollo si realizza invocando il metodo prepare(xid) su tutte le risorse distribuite, le quali possono rispondere con OK o ABORT. Dopo aver ricevuto la risposta da tutte le risorse, l’XA manager decide se inviare il comando commit(xid) o abort(xid): Il commit sarà inviato nel caso in cui tutte le risorse abbiano risposto con OK. L’abort nel caso in cui anche una sola risorsa abbia inviato l’ABORT. Infine il metodo end(xid) è invocato su tutte le risorse per indicare che la transazione è terminata. Il protocollo è in grado di ritornare in situazioni consistenti a seguito di diverse situazioni di errore (failure) che possono verificarsi durante le fasi descritte. La spiegazione di come ciò accada è al di fuori degli scopi di questo articolo. Per maggiori dettagli si rimanda quindi alla specifica X/Open Distributed Transaction Processing definita dall’Open Group.
Le API JTA, definite da Sun Microsystem, sono API di alto livello che specificano le interfacce tra il Transaction Manager e tutte le altre componenti coinvolte in una transazione distribuita: risorse XA e applicazioni. JTA si compone principalmente di tre parti:
- Una interfaccia di alto livello utilizzata dalle applicazione per la demarcazione delle transazioni (UserTransaction).
- Una implementazione dello standard X/Open XA protocol, inclusa nel package
javax.transaction.xa
, che consiste nelle interfacce XAResource, Xid e XAException. - Una interfaccia di alto livello per la specifica di un transaction manager che integrato in un application server consente di gestire le transazioni utente.
Spring e Bitronix
Spring fornisce supporto alle transazioni JTA attraverso il componente JtaTransactionManager
. Se l’applicazione è eseguita in un container J2EE, tale componente è in grado di recuperare la corretta transazione utente (javax.transaction.UserTransaction
) dalla directory JNDI. Il componente è poi specializzato in altri 9 sottoclassi specifiche per differenti application server, e che sono automaticamente referenziati da Spring, senza alcun intervento utente, quali, ad esempio WebLogicJtaTransactionManager e
WebSphereUowTransactionManager
.
Per configurare l’utilizzo di JTA in Spring è sufficiente inserire nell’xml di contesto il seguente namespace:
1 |
<tx:jta-transaction-manager /> |
Diversamente in applicazioni non ospitate in application server J2EE, è necessario utilizzare nel progetto uno specifico JTA transaction manager, come ad esempio Bitronix che utilizzeremo nel nostro esempio. Bitronix Transaction Manager (BTM) è una implementazione semplice ma completa delle specifiche JTA (1.1) che supporta diverse tipologie di risorse XA: database, JMS e JCA.
Configurazione del Progetto
Generiamo una applicazione standalone utilizzando Maven e modifichiamo il pom.xml
inserendo le necessarie dipendenze.
In particolare inseriamo le dipendenze di Spring:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!-- Spring Core --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <!-- Spring tx --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <!-- Spring beans --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> |
E le dipendenze di Bitronix:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!-- Bitronix --> <dependency> <groupId>org.codehaus.btm</groupId> <artifactId>btm</artifactId> <version>${bitronix.version}</version> </dependency> <!-- slf4j --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.21</version> </dependency> |
Per il nostro progetto abbiamo bisogno anche dei driver per Oracle 12. Per includerli come dependency è innanzitutto necessario installarli nel repository locale. Scaricate quindi il jar ojdbc7.jar
ed eseguite la seguente istruzione maven (eventualmente aggiornando il numero di versione):
1 2 3 4 5 6 7 |
mvn install:install-file -DgroupId=com.oracle.jdbc -DartifactId=ojdbc7 -Dversion=12.1.0.2 -Dpackaging=jar -Dfile=ojdbc7.jar -DgeneratePom=true |
pom.xml
:
1 2 3 4 5 |
<dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc7</artifactId> <version>12.1.0.2</version> </dependency> |
Configurazione della Connessione
Per configurare il JTA Transaction Manager ed i datasource verso i due database di test creiamo il file connection.xml
nella cartella resources del progetto maven. In tale file introduciamo innanzitutto i due bean di configurazione di Bitronix:
1 2 3 4 5 6 7 8 9 10 11 12 |
<!-- Bitronix Transaction Manager embedded configuration --> <bean id="btmConfig" factory-method="getConfiguration" class="bitronix.tm.TransactionManagerServices"> <!--Disabling Jmx avoids registering JMX Beans to any container --> <property name="disableJmx" value="true" /> <property name="serverId" value="spring-btm" /> </bean> <!-- create BTM transaction manager --> <bean id="BitronixTransactionManager" factory-method="getTransactionManager" class="bitronix.tm.TransactionManagerServices" depends-on="btmConfig" destroy-method="shutdown" /> |
1 2 3 4 5 6 |
<!-- Spring JtaTransactionManager --> <bean id="JtaTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager" ref="BitronixTransactionManager" /> <property name="userTransaction" ref="BitronixTransactionManager" /> <property name="allowCustomIsolationLevels" value="true" /> </bean> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<bean id="xaDataSource1" class="bitronix.tm.resource.jdbc.PoolingDataSource" init-method="init" destroy-method="close"> <property name="className" value="oracle.jdbc.xa.client.OracleXADataSource" /> <property name="uniqueName" value="xaDataSource1" /> <property name="minPoolSize" value="1" /> <property name="maxPoolSize" value="4" /> <property name="testQuery" value="SELECT 1 FROM dual" /> <property name="driverProperties"> <props> <prop key="URL">jdbc:oracle:thin:@//IP:1521/SERVICE_NAME</prop> <prop key="user">LOGIN</prop> <prop key="password">PASSWORD</prop> </props> </property> <property name="allowLocalTransactions" value="true" /> </bean> |
Configurazione dei Bean
Si è stabilito di mantenere separato l’xml in cui è definita la connessione da quello in cui sono dichiarati e bean dell’applicazione. Creiamo quindi un file context.xml
e configuriamo il bean MyFacade per l’accesso ai dati. Per utilizzare la transazione definita abbiamo due possibilità. Utilizzare direttamente il bean JtaStransactionManager
nel codice oppure, più elegantemente, utilizzare Spring AOP e wrappare il bean in un TransactionProxyfactoryBean
. Questo proxy intercetta le invocazioni ai metodi del bean e gestisce le transazioni in base a come viene configurato. Per i nostri scopi la configurazione nel context.xml
del bean sarà:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<bean id="MyFacade" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager" ref="JtaTransactionManager" /> <property name="transactionAttributes"> <props> <prop key="*">PROPAGATION_REQUIRED, -Exception</prop> </props> </property> <property name="target"> <bean class="it.javaboss.MyFacade"> <property name="dataSource1" ref="xaDataSource1"/> <property name="dataSource2" ref="xaDataSource2"/> </bean> </property> </bean> |
Il codice seguente mostra la classe MyFacade
che utilizza i due datasource per eseguire due insert. Per eseguire dei test si consiglia di modificare le insert in modo da generare eccezioni, ad esempio utilizzando nella seconda insert una tabella inesistente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package it.javaboss; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import javax.sql.DataSource; public class MyFacade { private DataSource dataSource1; public void setDataSource1(DataSource dataSource) { this.dataSource1 = dataSource; } private DataSource dataSource2; public void setDataSource2(DataSource dataSource) { this.dataSource2 = dataSource; } public void update() throws SQLException { Connection conn1 = null; Connection conn2 = null; try { conn1 = dataSource1.getConnection(); PreparedStatement ps1 = conn1.prepareStatement( "insert into TABLE1 values (...)" ); ps1.execute(); conn2 = dataSource2.getConnection(); PreparedStatement ps2 = conn1.prepareStatement( "insert into TABLE2 values (...)" ); ps2.execute(); } finally { if (conn1 != null) { try { conn1.close(); } catch (SQLException e) {} } if (conn2 != null) { try { conn2.close(); } catch (SQLException e) {} } } } } |
Ovviamente affinché il TransactionProxyfactoryBean
faccia il suo lavoro non deve assolutamente essere silenziata l’eccezione all’interno del codice del metodo update()
.
Riportiamo infine la classe Main che inizializza il contesto di Spring ed esegue l’update sul facade.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static void main(String[] args) { String[] springConfig = { "context.xml", "connection.xml"}; ApplicationContext context = new ClassPathXmlApplicationContext(springConfig); MyFacade myFacade = (MyFacade) context.getBean("MyFacade"); try { myFacade.update(); } catch (SQLException e) { e.printStackTrace(); } ( (AbstractApplicationContext) context ).registerShutdownHook(); } |
Codice Sorgente
Il codice sorgente può essere scaricato qui spring-jta