Detta inlägg ingår i serien Spring från början och kommer att behandla det transaktionsstöd som finns i springmodulen spring-tx
.
Transaktioner är ett sätt att hålla ihop en eller flera operationer. Typiskt gäller principen ”allt eller inget” vilket betyder att alla operationer som ingår i en transaktion ska exekveras utan fel för att transaktionen ska gälla. Det vanligaste fallet är databastransaktioner där exempelvis flera skrivningar måste exekveras utan fel innan en så kallad commit genomförs. Ett exempel på detta är det klassiska bankkontoexemplet med överflyttning av pengar från ett konto till ett annat. En överflyttning sker genom att ett uttag först sker från ett konto och sedan insättning på ett annat konto. Bägge operationerna måste lyckas, annars ska transaktionen inte gälla det vill säga att man ”rullar tillbaka” transaktionen via en så kallad rollback.
Spring erbjuder på ett enhetligt sätt stöd för att kunna hantera transaktioner av olika slag som till exempel JTA, JDBC, Hibernate, JPA och JDO. Transaktionsstödet kan användas på två sätt, deklarativt eller programmatiskt. Det deklarativa sättet är det absolut vanligaste och innebär att metadata kring transaktionslogik inte ligger inbäddad i den faktiska javakoden utan enbart finns deklarerad ”utanför” javakoden via metadata. Metadata kan antingen bestå av externa XML-filer eller annotationer och påverkar alltså inte javakoden, den är så att säga non-intrusive.
Deklarativt transaktionsstöd
Springs deklarativa transaktionsstöd möjliggörs via användning av Spring AOP som tidigare beskrivits i artikeln Introduktion till Spring AOP. Dock krävs inte djupa kunskaper inom Spring AOP för att kunna använda Springs transaktionsstöd. AOP Proxies används för att väva ihop en PlatformTransactionManager
med en så kallad TransactionInterceptor
som sedan hanterar själva transaktionen, det vill säga skapande, commit, rollback och så vidare. Gränssnittet PlatformTransactionManager
diskuteras senare i denna artikel.
Det deklarativa transaktionsstödet kan liknas vid vanlig EJB CMT (Container Managed Transactions). Där kan transaktionslogik specificeras ner till metodnivå via externa XML-filer som J2EE-containern kan tolka. De stora skillnaderna mellan Spring TX och EJB CMT är:
- JTA – EJB CMT kräver att det finns en transaktionshanterare som stöder JTA. JTA-stöd brukar typiskt finnas i en applikationsserver (såsom JBoss, WebLogic och så vidare). Spring TX behöver inte JTA och följdaktligen krävs heller ej någon applikationsserver. Dock kan Spring TX användas med hjälp av JTA om så önskas.
- POJO – Spring TX kan tillämpas på vilken springböna som helst, inte bara på EJB:s.
- Rollback – Spring TX erbjuder ett bredare stöd för rollback-hantering som de själva kallar för ”rollback-regler”. Olika aspekter kan konfigureras för att hantera felsituationer och reglerna är dessutom deklarativa. För EJB CMT finns endast alternativet att programmatiskt hantera rollbacks via setRollbackOnly.
Standardvärden
Som vanligt när det gäller Spring försöker man sätta så vettiga standardvärden (det vill säga defaultvärden) som möjligt på attribut och liknande. Springs transaktionshantering utgör inget undantag och nedanstående tabell visar de standardvärden som gäller för transaktionsattributen.
Attribut | Standardvärde | Beskrivning |
---|---|---|
Propagation | REQUIRED | Beskriver hur transaktionen ska överföras (det vill säga propageras) mellan olika kontext. Tillåtna värden är:- REQUIRES_NEW – En ny transaktion ska alltid skapas, även om en befintlig transaktion existerar. - REQUIRED – Om en befintlig transaktion existerar så ska denna användas. Annars ska en ny transaktion skapas. |
Isolation | ISOLATION_DEFAULT | Standardvärdet innebär att det värde som är satt på den underliggande datakällan gäller. Giltiga värden är:- ISOLATION_DEFAULT (standardvärdet) - ISOLATION_READ_UNCOMMITTED - ISOLATION_READ_UNCOMMITTED - ISOLATION_REPEATABLE_READ - ISOLATION_SERIALIZABLE |
Timeout | -1 | Specificerar hur länge en transaktion kan pågå innan en rollback sker. Standardvärdet -1 innebär att detta värde styrs av den underliggande datakällan |
Read-only | false | Attribut som kan användas för att prestandaoptimera operationer som endast innebär läsning. |
Rollback for | En lista över de checked exceptions som ger en rollback. Standardvärdet är en tom lista vilket innebär att endast unchecked exceptions ger automatisk rollback. | |
No rollback for | En lista över exceptions som inte ska orsaka rollback. Standardvärdet är en tom lista vilket innebär att alla unchecked exceptions ger automatisk rollback. |
Exempel – Deklarativt transaktionsstöd via XML
Följande exempel belyser många olika delar av Springs deklarativa transaktionsstöd – både via XML-konfiguration och via annotationer. Vi utgår från en tabell i databasen som kallas person
. DDL:en för denna tabell ser ut enligt följande för MySQL.
CREATE TABLE person (
id bigint(20) NOT NULL,
firstName varchar(255) NOT NULL,
lastName varchar(255) NOT NULL,
phoneNumber varchar(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
Tabellen person
innehåller alltså ett id, förnamn, efternamn och telefonnummer – inte direkt raketforskning men det räcker gott som exempel.
Vi vill bygga en enkel tjänst som erbjuder CRUD-metoder mot person-tabellen. Dessa metoder ska vara transaktionshanterade. Gränssnittet för denna tjänst ser ut som i figuren nedan.
package se.cygni.sample.spring.tx;
public interface PersonService {
void create(Person person);
Person retrieve(long id);
void update(Person person);
void delete(long id);
}
Observera att ingen transaktionsspecifik kod finns i gränssnittet. Implementationen av denna tjänst är en enkel POJO som andvänder SimpleJdbcTemplate
för att kommunicera med databasen. Klassen ärver springklassen SimpleJdbcDaoSupport
för att enkelt kunna få tag på en instans av typen SimpleJdbcTemplate
via metoden getSimpleJdbcTemplate
. Se även inlägget om Spring JDBC för mer information om just SimpleJdbcTemplate
och SimpleJdbcDaoSupport
.
package se.cygni.sample.spring.tx;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport;
import org.springframework.transaction.annotation.Transactional;
public class PersonServiceImpl
extends SimpleJdbcDaoSupport implements PersonService {
public void create(Person person) {
getSimpleJdbcTemplate().update(
"insert into person " +
"(id, firstName, lastName, phoneNumber) " +
"values(?, ?, ?, ?)",
person.getId(),
person.getFirstName(),
person.getLastName(),
person.getPhoneNumber());
}
public Person retrieve(long id) {
return getSimpleJdbcTemplate().queryForObject(
"select id, firstName, lastName, phoneNumber " +
"from person where id = ?",
new ParameterizedRowMapper<Person>() {
public Person mapRow(ResultSet rs, int rowNum)
throws SQLException {
Person person = new Person();
person.setId(rs.getLong("id"));
person.setFirstName(rs.getString("firstName"));
person.setLastName(rs.getString("lastName"));
person.setPhoneNumber(
rs.getString("phoneNumber"));
return person;
}
},
id);
}
public void update(Person person) {
getSimpleJdbcTemplate().update(
"update person firstName = ?, lastName = ?, " +
"phoneNumber = ? where id = ?",
person.getFirstName(),
person.getLastName(),
person.getPhoneNumber(),
person.getId());
}
public void delete(long id) {
getSimpleJdbcTemplate().update(
"delete from person where id = ?", id);
}
}
Klassen PersonServiceImpl
innehåller, liksom gränssnittet PersonService
, inte heller någon transaktionsspecifik kod. Nu gäller det bara att deklarera denna klass som en springböna och knyta ihop det hela med Spring TX. Applikationskontextet visas nedan.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<bean id="personService"
class="se.cygni.sample.spring.tx.PersonServiceImpl">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/person" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="retrieve*" read-only="true" />
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut
id="personServiceOperation"
expression=
"execution(* se.cygni.sample.spring.tx.PersonService.*(..))" />
<aop:advisor
advice-ref="txAdvice"
pointcut-ref="personServiceOperation" />
</aop:config>
</beans>
Observera att förutom schemat beans
så används också http://www.springframework.org/schema/tx
och http://www.springframework.org/schema/aop
. Dessa scheman erbjuder förenklad XML-syntax för både transaktionshanteringen och aspektkonfigurationen.
Bönan personService
länkas ihop med en vanlig dataSource
. I exemplet används BasicDataSource
från commons-dbcp men det går att använda andra implementationer som till exempel ett JNDI-uppslag eller liknande.
Datakällan länkas dessutom ihop med bönan txManager
som är av typen DataSourceTransactionManager
. Denna klass implementerar gränssnittet PlatformTransactionManager
som är ett av grundgränssnitten i modulen spring-tx. Det finns ett flertal implementationer av PlatformTransactionManager
som är värda att nämna:
DataSourceTransactionManager
– Används när man vill transaktionshantera en vanlig JDBC-datakälla som i detta exempelHibernateTransactionManager
– Denna transaktionshanterare kopplas ihop med enSessionFactory
från Hibernate och kan därmed hantera hibernatetransaktionerJpaTransactionManager
– Används för att kunna hantera JPA-transaktionerJtaTransactionManager
– När JTA används så är denna transaktionshanterare lämplig. Det finns dessutom specifika transaktionshanterare för olika applikationsservrar som till exempel WebSphere.JmsTransactionManager
– Används för JMS-operationer som kräver transaktionsstöd.
De ovan nämnda transaktionshanterarna implementerar alltså gränssnittet PlatformTransactionManager
. Vid deklarativa transaktioner behöver dock utvecklaren aldrig känna till detta gränssnitt eller använda hanteraren direkt, detta är helt inkapslat via spring-tx. PlatformTransactionManager
kan däremot användas direkt för programmatisk transaktionshantering som visas senare i denna artikel.
Nu när vi länkat ihop vår tjänst med en transaktionshanterare så måste vi se till att de metoder som exponeras via tjänsten blir transaktionshanterade. Detta sker via den aspekt som visas i applikationskontextet. Den första delen, det vill säga advicet txAdvice
, specificerar semantiken för olika metoder, i exemplet så kommer alla metoder som börjar på retrieve att hanteras som read-only
och kan därmed optimeras av den underliggande datakällan. Alla övriga metoder är inte read-only
och får spring-tx standardbetende som tidigare visas i tabellen ovan. Advicet länkas ihop med den aktuella transaktionshanteraren via attributet transaction-manager
.
Figuren nedan som är den sista delen i kontextet visar hur aspekten konfigureras. Advicet länkas ihop med en pointcut som ska gälla för alla metoder i gränssnittet PersonService
enligt standardsyntax för Spring AOP.
<aop:config>
<aop:pointcut
id="personServiceOperation"
expression=
"execution(* se.cygni.sample.spring.tx.PersonService.*(..))" />
<aop:advisor
advice-ref="txAdvice"
pointcut-ref="personServiceOperation" />
</aop:config>
Här skapas en pointcut vid namn personServiceOperation
som triggar på alla metodanrop till gränssnittet se.cygni.sample.spring.tx.PersonService
. Detta kan även generaliseras ytterligare genom att exempelvis ange *Service.*(..)
vilket då innebär aspekten kommer att triggas för alla metoder på alla gränssnitt som slutar på just ordet Service
.
Raden med advisor
länkar ihop transaktionsadvicet med pointcut:en och vi har därmed en fullständig konfiguration för spring-tx.
För att anropa tjänsten och använda transaktionsstödet krävs inga speciella åtgärder. Sätt upp en vanlig referens till personService
-bönan och använd den rakt av.
Exempel – Deklarativt transaktionsstöd via annotationer
Spring erbjuder även deklarativt transaktionsstöd via annotationer. Annotationerna sätts på den konkreta implementationen av tjänsten och alltså inte på själva gränssnittet. En enkel omskrivning av ovanstående exempel ser ut som följer.
@Transactional
public class PersonServiceImpl
extends SimpleJdbcDaoSupport implements PersonService {
public void create(Person person) {
getSimpleJdbcTemplate().update(
"insert into person " +
"(id, firstName, lastName, phoneNumber) " +
"values(?, ?, ?, ?)",
person.getId(),
person.getFirstName(),
person.getLastName(),
person.getPhoneNumber());
}
@Transactional(readOnly = true)
public Person retrieve(long id) {
...
Först och främst är klassen annoterad med @Transactional
. Detta innebär att alla publika metoder i klassen kommer att kunna hanteras av spring-tx och de de transaktionsattribut som gäller är de standardvärden som visats i tabellen ovan. Metoden retrieve
har en separat annotation som överrider transaktionsattributet readOnly
(standardvärdet är ju egentligen readOnly=false
).
Dessa annotationer är den enda kodförändring som krävs. Nu fattas bara konfigurationen i kontextet.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<bean id="personService"
class="se.cygni.sample.spring.tx.PersonServiceImpl">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/person" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
Observera att bönorna personService
, dataSource
och txManager
är samma som i det tidigare exemplet. Notera även att schemat för Spring AOP inte behövs längre eftersom ingen aspekt konfigureras i kontextet.
Den stora skillnaden gäller den sista bönan som visas i figuren nedan. Här deklareras att annotationsstöd för spring-tx ska användas och resten hanterar spring-tx åt dig. Ingen aspektkonfiguration krävs vilket leder till kortare XML-filer, färre potentiella felkällor och mindre krångel. Peka bara ut vilken transaktionshanterare som ska användas och sen är det bara att köra så det ryker. Faktum är att ifall transaktionshanterarens namn är "transactionManager"
så behövs inte detta attribut eftersom standardvärdet på attributet transaction-manager
är just "transactionManager"
.
<tx:annotation-driven transaction-manager="txManager"/>
Programmatiskt transaktionsstöd
Det programmatiska transaktionsstödet kan användas på två sätt. Antingen genom att använda PlatformTransactionManager
direkt vilket innebär en en väldigt låg abstraktionsnivå. Det andra sättet är att använda klassen TransactionTemplate
som följer templatingmönstret som tidigare diskuterats i artikeln om Spring JDBC.
Det första sättet – att använda PlatformTransactionManager
direkt – kommer inte att beskrivas i denna artikel och är ett arbetssätt som inte rekommenderas. Om programmatisk transaktionshantering ska användas rekommenderas användning av TransactionTemplate
. Observera att det absolut vanligaste sättet är dock att använda det deklarativa transaktionsstödet.
Exemplet nedan visar hur det programmatiska stödet kan användas. Vi antar att samma tjänst som i tidigare exempel finns tillgänglig – PersonServiceImpl
. Vi kapslar in denna tjänst via klassen TxAwarePersonServiceImpl
vars uppgift är att hantera själva transaktionen med hjälp av TransactionTemplate
. Den faktiska logiken, det vill säga JDBC-operationerna – sker fortfarande i originaltjänsten.
package se.cygni.sample.spring.tx;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
public class TxAwarePersonServiceImpl implements PersonService {
private TransactionTemplate transactionTemplate;
private PersonService personService;
public void setTransactionTemplate(
TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void setPersonService(PersonService personService) {
this.personService = personService;
}
public void create(final Person person) {
transactionTemplate.execute(
new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(
TransactionStatus status) {
personService.create(person);
}
});
}
public Person retrieve(final long id) {
Person person = (Person) transactionTemplate.execute(
new TransactionCallback() {
public Object doInTransaction(
TransactionStatus status) {
return personService.retrieve(id);
}
});
return person;
}
public void update(final Person person) {
transactionTemplate.execute(
new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(
TransactionStatus status) {
personService.update(person);
}
});
}
public void delete(final long id) {
transactionTemplate.execute(
new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(
TransactionStatus status) {
personService.delete(id);
}
});
}
}
Klassen TxAwarePersonServiceImpl
delegerar alla anrop vidare till originaltjänsten. Alla dessa anrop sker dock med hjälp av TransactionTemplate
. Applikationskontextet visas nedan, där kan man se att bönan personService
kapslar in originaltjänsten och behöver tillgång till en TransactionTemplate
. Själva template-klassen deklareras som en vanlig springböna med en referens till transaktionshanteraren och visas längst ner i filen.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<bean id="personService"
class="se.cygni.sample.spring.tx.TxAwarePersonServiceImpl">
<property name="transactionTemplate" ref="transactionTemplate"/>
<property name="personService">
<bean class="se.cygni.sample.spring.tx.PersonServiceImpl">
<property name="dataSource" ref="dataSource" />
</bean>
</property>
</bean>
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/person" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="txManager"/>
</bean>
</beans>
TransactionTemplate
använder – som namnet antyder – templatingmönstret, som tidigare diskuterats. All transaktionshantering sker via denna klass och den callback som måste implementeras heter TransactionCallback
och ser ut som i figuren nedan.
package org.springframework.transaction.support;
import org.springframework.transaction.TransactionStatus;
public interface TransactionCallback {
Object doInTransaction(TransactionStatus status);
}
För operationer som inte har något returvärde kan klassen TransactionCallbackWithoutResult
ärvas istället för att direkt implementera gränssnittet TransactionCallback
. Metoderna i klassen TransactionCallbackWithoutResult
och gränssnittet TransactionCallback
tar en parameter av typen TransactionStatus
som visas i figuren nedan.
package org.springframework.transaction;
public interface TransactionStatus extends SavepointManager {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
boolean isCompleted();
}
TransactionStatus
kan bland annat användas för att markera att en transaktion måste rullas tillbaka via metoden setRollbackOnly
.
Sammanfattning
Spring erbjuder ett kraftfullt stöd för transaktioner via modulen spring-tx. På ett enhetligt sätt erbjuds stöd för att kunna hantera transaktioner av olika slag som till exempel JTA, JDBC, Hibernate, JPA och JDO.
Det deklarativa transaktionsstödet är det absolut vanligaste och kan användas på två sätt, via XML eller via annotationer. Det senare alternativet innebär att väldigt lite konfiguration krävs, vilket är väldigt smakfullt.
Det programmatiska transaktionsstödet är också kraftfullt och där rekommenderas användning av templatingklassen TransactionTemplate
som hanterar all boilerplate-kod som utvecklaren annars typiskt måste skriva.