© Marten Deinum, Daniel Rubio, and Josh Long 2017

Marten Deinum, Daniel Rubio and Josh Long, Spring 5 Recipes, https://doi.org/10.1007/978-1-4842-2790-9_10

10. Spring Transaction Management

Marten Deinum, Daniel Rubio2 and Josh Long3

(1)Meppel, Drenthe, The Netherlands

(2)F. Bahia, Ensenada, Baja California, Mexico

(3)Apartment 205, Canyon Country, California, USA

In this chapter, you will learn about the basic concept of transactions and Spring’s capabilities in the area of transaction management. Transaction management is an essential technique in enterprise applications to ensure data integrity and consistency. Spring, as an enterprise application framework, provides an abstract layer on top of different transaction management APIs. As an application developer, you can use Spring’s transaction management facilities without having to know much about the underlying transaction management APIs.

Like the bean-managed transaction (BMT) and container-managed transaction (CMT) approaches in EJB, Spring supports both programmatic and declarative transaction management. The aim of Spring’s transaction support is to provide an alternative to EJB transactions by adding transaction capabilities to POJOs.

Programmatic transaction management is achieved by embedding transaction management code in your business methods to control the commit and rollback of transactions. You usually commit a transaction if a method completes normally and roll back a transaction if a method throws certain types of exceptions. With programmatic transaction management, you can define your own rules to commit and roll back transactions.

However, when managing transactions programmatically, you have to include transaction management code in each transactional operation. As a result, the boilerplate transaction code is repeated in each of these operations. Moreover, it’s hard for you to enable and disable transaction management for different applications. If you have a solid understanding of AOP, you may already have noticed that transaction management is a kind of crosscutting concern.

Declarative transaction management is preferable to programmatic transaction management in most cases. It’s achieved by separating transaction management code from your business methods via declarations. Transaction management, as a kind of crosscutting concern, can be modularized with the AOP approach. Spring supports declarative transaction management through the Spring AOP framework. This can help you to enable transactions for your applications more easily and define a consistent transaction policy. Declarative transaction management is less flexible than programmatic transaction management.

Programmatic transaction management allows you to control transactions through your code—explicitly starting, committing, and joining them as you see fit. You can specify a set of transaction attributes to define your transactions at a fine level of granularity. The transaction attributes supported by Spring include the propagation behavior, isolation level, rollback rules, transaction timeout, and whether or not the transaction is read-only. These attributes allow you to further customize the behavior of your transactions.

Upon finishing this chapter, you will be able to apply different transaction management strategies in your application. Moreover, you will be familiar with different transaction attributes to finely define your transactions.

Programmatic transaction management is a good idea in certain cases where you don’t feel the addition of Spring proxies is worth the trouble or negligible performance loss. Here, you might access the native transaction yourself and control the transaction manually. A more convenient option that avoids the overhead of Spring proxies is the TransactionTemplate class, which provides a template method around which a transactional boundary is started and then committed.

10-1. Avoid Problems with Transaction Management

Transaction management is an essential technique in enterprise application development to ensure data integrity and consistency. Without transaction management, your data and resources may be corrupted and left in an inconsistent state. Transaction management is particularly important for recovering from unexpected errors in a concurrent and distributed environment.

In simple words, a transaction is a series of actions treated as a single unit of work. These actions should either complete entirely or take no effect at all. If all the actions go well, the transaction should be committed permanently. In contrast, if any of them goes wrong, the transaction should be rolled back to the initial state as if nothing had happened.

The concept of transactions can be described with four key properties : atomicity, consistency, isolation, and durability (ACID) .

  • Atomicity: A transaction is an atomic operation that consists of a series of actions. The atomicity of a transaction ensures that the actions either complete entirely or take no effect at all.

  • Consistency: Once all actions of a transaction have completed, the transaction is committed. Then your data and resources will be in a consistent state that conforms to business rules.

  • Isolation: Because there may be many transactions processing with the same data set at the same time, each transaction should be isolated from others to prevent data corruption.

  • Durability: Once a transaction has completed, its result should be durable to survive any system failure (imagine if the power to your machine was cut right in the middle of a transaction’s commit). Usually, the result of a transaction is written to persistent storage.

To understand the importance of transaction management, let’s begin with an example about purchasing books from an online bookshop. First, you have to create a new schema for this application in your database. We have chosen to use PostgreSQL as the database to use for these samples. The source code for this chapter contains a bin directory with two scripts: one (postgres.sh) to download a Docker container and start a default Postgres instance and a second one (psql.sh) to connect to the running Postgres instance. See Table 10-1 for the connection properties to use in your Java application.

Table 10-1. JDBC Properties for Connecting to the Application Database

Property

Value

Driver class

org.postgresql.Driver

URL

jdbc:postgresql://localhost:5432/bookstore

Username

postgres

Password

password

Note

The sample code for this chapter provides scripts in the bin directory to start and connect to a Docker-based PostgreSQL instance. To start the instance and create the database, follow these steps:

  1. Execute binpostgres.sh, which will download and start the Postgres Docker container.

  2. Execute binpsql.sh, which will connect to the running Postgres container.

  3. Execute CREATE DATABASE bookstore to create the database to use for the samples.

For your bookshop application, you need a place to store the data. You’ll create a simple database to manage books and accounts.

The entity relational (ER) diagram for the tables looks like Figure 10-1.

A314861_4_En_10_Fig1_HTML.jpg
Figure 10-1. BOOK_STOCK describes how many given BOOKs exist.

Now, let’s create the SQL for the preceding model. Execute the binpsql.sh command to connect to the running container and open the psql tool.

Paste the following SQL into the shell and verify its success:

CREATE TABLE BOOK (
    ISBN         VARCHAR(50)    NOT NULL,
    BOOK_NAME    VARCHAR(100)   NOT NULL,
    PRICE        INT,
    PRIMARY KEY (ISBN)
);


CREATE TABLE BOOK_STOCK (
    ISBN     VARCHAR(50)    NOT NULL,
    STOCK    INT            NOT NULL,
    PRIMARY KEY (ISBN),
    CONSTRAINT positive_stock CHECK (STOCK >= 0)
);


CREATE TABLE ACCOUNT (
    USERNAME    VARCHAR(50)    NOT NULL,
    BALANCE     INT            NOT NULL,
    PRIMARY KEY (USERNAME),
    CONSTRAINT positive_balance CHECK (BALANCE >= 0)
);

A real-world application of this type would probably feature a price field with a decimal type, but using an int makes the programming simpler to follow, so leave it as an int.

The BOOK table stores basic book information such as the name and price, with the book ISBN as the primary key. The BOOK_STOCK table keeps track of each book’s stock. The stock value is restricted by a CHECK constraint to be a positive number. Although the CHECK constraint type is defined in SQL-99, not all database engines support it. At the time of this writing, this limitation is mainly true of MySQL because Sybase, Derby, HSQL, Oracle, DB2, SQL Server, Access, PostgreSQL, and FireBird all support it. If your database engine doesn’t support CHECK constraints, please consult its documentation for similar constraint support. Finally, the ACCOUNT table stores customer accounts and their balances. Again, the balance is restricted to be positive.

The operations of your bookshop are defined in the following BookShop interface . For now, there is only one operation: purchase().

package com.apress.springrecipes.bookshop;

public interface BookShop {

    void purchase(String isbn, String username);
}

Because you will implement this interface with JDBC, you need to create the following JdbcBookShop class . To better understand the nature of transactions, let’s implement this class without the help of Spring’s JDBC support.

package com.apress.springrecipes.bookshop;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;


import javax.sql.DataSource;

public class JdbcBookShop implements BookShop {

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }


    public void purchase(String isbn, String username) {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();


            PreparedStatement stmt1 = conn.prepareStatement(
                "SELECT PRICE FROM BOOK WHERE ISBN = ?");
            stmt1.setString(1, isbn);
            ResultSet rs = stmt1.executeQuery();
            rs.next();
            int price = rs.getInt("PRICE");
            stmt1.close();


            PreparedStatement stmt2 = conn.prepareStatement(
                "UPDATE BOOK_STOCK SET STOCK = STOCK - 1 "+
                "WHERE ISBN = ?");
            stmt2.setString(1, isbn);
            stmt2.executeUpdate();
            stmt2.close();


            PreparedStatement stmt3 = conn.prepareStatement(
                "UPDATE ACCOUNT SET BALANCE = BALANCE - ? "+
                "WHERE USERNAME = ?");
            stmt3.setInt(1, price);
            stmt3.setString(2, username);
            stmt3.executeUpdate();
            stmt3.close();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {}
            }
        }
    }
}

For the purchase() operation, you have to execute three SQL statements in total. The first is to query the book price. The second and third update the book stock and account balance accordingly. Then, you can declare a bookshop instance in the Spring IoC container to provide purchasing services. For simplicity’s sake, you can use DriverManagerDataSource, which opens a new connection to the database for every request.

Note

To access a PostgreSQL database, you have to add the Postgres client library to your CLASSPATH.

package com.apress.springrecipes.bookshop.config;

import com.apress.springrecipes.bookshop.BookShop;
import com.apress.springrecipes.bookshop.JdbcBookShop;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;


import javax.sql.DataSource;

@Configuration
public class BookstoreConfiguration {


    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(org.postgresql.Driver.class.getName());
        dataSource.setUrl("jdbc:postgresql://localhost:5432/bookstore");
        dataSource.setUsername("postgres");
        dataSource.setPassword("password");
        return dataSource;
    }


    @Bean
    public BookShop bookShop() {
        JdbcBookShop bookShop = new JdbcBookShop();
        bookShop.setDataSource(dataSource());
        return bookShop;
    }
}

To demonstrate the problems that can arise without transaction management, suppose you have the data shown in Tables 10-2, 10-3, and 10-4 entered in your bookshop database .

Table 10-2. Sample Data in the BOOK Table for Testing Transactions

ISBN

BOOK_NAME

PRICE

0001

The First Book

30

Table 10-3. Sample Data in the BOOK_STOCK Table for Testing Transactions

ISBN

STOCK

0001

10

Table 10-4. Sample Data in the ACCOUNT Table for Testing Transactions

USERNAME

BALANCE

user1

20

Then, write the following Main class for purchasing the book with ISBN 0001 by the user user1. Because that user’s account has only $20, the funds are not sufficient to purchase the book.

package com.apress.springrecipes.bookshop;

import com.apress.springrecipes.bookshop.config.BookstoreConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class Main {

    public static void main(String[] args) throws Throwable {

        ApplicationContext context =
            new AnnotationConfigApplicationContext(BookstoreConfiguration.class);


        BookShop bookShop = context.getBean(BookShop.class);
        bookShop.purchase("0001", "user1");


    }
}

When you run this application, you will encounter a SQLException, because the CHECK constraint of the ACCOUNT table has been violated. This is an expected result because you were trying to debit more than the account balance.

However, if you check the stock for this book in the BOOK_STOCK table, you will find that it was accidentally deducted by this unsuccessful operation! The reason is that you executed the second SQL statement to deduct the stock before you got an exception in the third statement.

As you can see, the lack of transaction management causes your data to be left in an inconsistent state. To avoid this inconsistency, your three SQL statements for the purchase() operation should be executed within a single transaction. Once any of the actions in a transaction fail, the entire transaction should be rolled back to undo all changes made by the executed actions.

Manage Transactions with JDBC Commit and Rollback

When using JDBC to update a database, by default each SQL statement will be committed immediately after its execution. This behavior is known as autocommit. However, it does not allow you to manage transactions for your operations. JDBC supports the primitive transaction management strategy of explicitly calling the commit() and rollback() methods on a connection. But before you can do that, you must turn off autocommit, which is turned on by default.

package com.apress.springrecipes.bookshop;
...
public class JdbcBookShop implements BookShop {
    ...
    public void purchase(String isbn, String username) {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            ...
            conn.commit();
        } catch (SQLException e) {
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException e1) {}
            }
            throw new RuntimeException(e);
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {}
            }
        }
    }
}

The autocommit behavior of a database connection can be altered by calling the setAutoCommit() method. By default, autocommit is turned on to commit each SQL statement immediately after its execution. To enable transaction management, you must turn off this default behavior and commit the connection only when all the SQL statements have been executed successfully. If any of the statements go wrong, you must roll back all changes made by this connection.

Now, if you run your application again, the book stock will not be deducted when the user’s balance is insufficient to purchase the book.

Although you can manage transactions by explicitly committing and rolling back JDBC connections, the code required for this purpose is boilerplate code that you have to repeat for different methods. Moreover, this code is JDBC specific, so once you have chosen another data access technology, it needs to be changed also. Spring’s transaction support offers a set of technology-independent facilities, including transaction managers (e.g., org.springframework.transaction.PlatformTransactionManager), a transaction template (e.g., org.springframework.transaction.support.TransactionTemplate), and transaction declaration support, to simplify your transaction management tasks.

10-2. Choose a Transaction Manager Implementation

Problem

Typically, if your application involves only a single data source, you can simply manage transactions by calling the commit() and rollback() methods on a database connection. However, if your transactions extend across multiple data sources or you prefer to make use of the transaction management capabilities provided by your Java EE application server, you may choose the Java Transaction API (JTA). In addition, you may have to call different proprietary transaction APIs for different object-relational mapping frameworks such as Hibernate and JPA.

As a result, you have to deal with different transaction APIs for different technologies. It would be hard for you to switch from one set of APIs to another.

Solution

Spring abstracts a general set of transaction facilities from different transaction management APIs. As an application developer, you can simply utilize Spring’s transaction facilities without having to know much about the underlying transaction APIs. With these facilities, your transaction management code will be independent of any specific transaction technology.

Spring’s core transaction management abstraction is based on the interface PlatformTransactionManager. It encapsulates a set of technology-independent methods for transaction management. Remember that a transaction manager is needed no matter which transaction management strategy (programmatic or declarative) you choose in Spring. The PlatformTransactionManager interface provides three methods for working with transactions:

  • TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException

  • void commit(TransactionStatus status) throws TransactionException;

  • void rollback(TransactionStatus status) throws TransactionException;

How It Works

PlatformTransactionManager is a general interface for all Spring transaction managers. Spring has several built-in implementations of this interface for use with different transaction management APIs.

  • If you have to deal with only a single data source in your application and access it with JDBC, DataSourceTransactionManager should meet your needs.

  • If you are using JTA for transaction management on a Java EE application server, you should use JtaTransactionManager to look up a transaction from the application server. Additionally, JtaTransactionManager is appropriate for distributed transactions (transactions that span multiple resources). Note that while it’s common to use a JTA transaction manager to integrate the application server’s transaction manager, there’s nothing stopping you from using a stand-alone JTA transaction manager such as Atomikos.

  • If you are using an object-relational mapping framework to access a database, you should choose a corresponding transaction manager for this framework, such as HibernateTransactionManager or JpaTransactionManager.

Figure 10-2 shows the common implementations of the PlatformTransactionManager interface in Spring.

A314861_4_En_10_Fig2_HTML.gif
Figure 10-2. Common implementations of the PlatformTransactionManager interface

A transaction manager is declared in the Spring IoC container as a normal bean. For example, the following bean configuration declares a DataSourceTransactionManager instance. It requires the dataSource property to be set so that it can manage transactions for connections made by this data source.

@Bean
public DataSourceTransactionManager transactionManager() {
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager()
    transactionManager.setDataSource(dataSource());
    return transactionManager;
}

10-3. Manage Transactions Programmatically with the Transaction Manager API

Problem

You need to precisely control when to commit and roll back transactions in your business methods, but you don’t want to deal with the underlying transaction API directly.

Solution

Spring’s transaction manager provides a technology-independent API that allows you to start a new transaction (or obtain the currently active transaction) by calling the getTransaction() method and manage it by calling the commit() and rollback() methods. Because PlatformTransactionManager is an abstract unit for transaction management, the methods you called for transaction management are guaranteed to be technology independent.

How It Works

To demonstrate how to use the transaction manager API, let’s create a new class, TransactionalJdbcBookShop, which will make use of the Spring JDBC template. Because it has to deal with a transaction manager, you add a property of type PlatformTransactionManager and allow it to be injected via a setter method.

package com.apress.springrecipes.bookshop;

import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;


public class TransactionalJdbcBookShop extends JdbcDaoSupport implements BookShop {

    private PlatformTransactionManager transactionManager;

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }


    public void purchase(String isbn, String username) {
        TransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);


        try {
            int price = getJdbcTemplate().queryForObject(
                "SELECT PRICE FROM BOOK WHERE ISBN = ?", Integer.class, isbn);


            getJdbcTemplate().update(
                "UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?", isbn);


            getJdbcTemplate().update(
                "UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?", price, username);


            transactionManager.commit(status);
        } catch (DataAccessException e) {
            transactionManager.rollback(status);
            throw e;
        }
    }


}

Before you start a new transaction, you have to specify the transaction attributes in a transaction definition object of type TransactionDefinition. For this example, you can simply create an instance of DefaultTransactionDefinition to use the default transaction attributes.

Once you have a transaction definition, you can ask the transaction manager to start a new transaction with that definition by calling the getTransaction() method. Then, it will return a TransactionStatus object to keep track of the transaction status. If all the statements execute successfully, you ask the transaction manager to commit this transaction by passing in the transaction status. Because all exceptions thrown by the Spring JDBC template are subclasses of DataAccessException, you ask the transaction manager to roll back the transaction when this kind of exception is caught.

In this class, you have declared the transaction manager property of the general type PlatformTransactionManager. Now, you have to inject an appropriate transaction manager implementation. Because you are dealing with only a single data source and accessing it with JDBC, you should choose DataSourceTransactionManager. Here, you also wire a dataSource object because the class is a subclass of Spring’s JdbcDaoSupport, which requires it.

@Configuration
public class BookstoreConfiguration {
...
    @Bean
    public DataSourceTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }


    @Bean
    public BookShop bookShop() {
        TransactionalJdbcBookShop bookShop = new TransactionalJdbcBookShop();
        bookShop.setDataSource(dataSource());
        bookShop.setTransactionManager(transactionManager());
        return bookShop;
    }
}

10-4. Manage Transactions Programmatically with a Transaction Template

Problem

Suppose that you have a code block, but not the entire body, of a business method that has the following transaction requirements:

  • Start a new transaction at the beginning of the block.

  • Commit the transaction after the block completes successfully.

  • Roll back the transaction if an exception is thrown in the block.

If you call Spring’s transaction manager API directly, the transaction management code can be generalized in a technology-independent manner. However, you may not want to repeat the boilerplate code for each similar code block.

Solution

As with the JDBC template, Spring also provides a TransactionTemplate to help you control the overall transaction management process and transaction exception handling. You just have to encapsulate your code block in a callback class that implements the TransactionCallback<T> interface and pass it to the TransactionTemplate’s execute method for execution. In this way, you don’t need to repeat the boilerplate transaction management code for this block. The template objects that Spring provides are lightweight and usually can be discarded or re-created with no performance impact. A JDBC template can be re-created on the fly with a DataSource reference, for example, and so too can a TransactionTemplate be re-created by providing a reference to a transaction manager. You can, of course, simply create one in your Spring application context, too.

How It Works

A TransactionTemplate is created on a transaction manager just as a JDBC template is created on a data source. A transaction template executes a transaction callback object that encapsulates a transactional code block. You can implement the callback interface either as a separate class or as an inner class. If it’s implemented as an inner class, you have to make the method arguments final for it to access.

package com.apress.springrecipes.bookshop;

import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;


public class TransactionalJdbcBookShop extends JdbcDaoSupport implements BookShop {

    private PlatformTransactionManager transactionManager;

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }


    public void purchase(final String isbn, final String username) {

        TransactionTemplate transactionTemplate =
            new TransactionTemplate(transactionManager);


        transactionTemplate.execute(new TransactionCallbackWithoutResult() {

            protected void doInTransactionWithoutResult(
                TransactionStatus status) {


                int price = getJdbcTemplate().queryForObject(
                    "SELECT PRICE FROM BOOK WHERE ISBN = ?", Integer.class, isbn);


                getJdbcTemplate().update(
                    "UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?", isbn );


                getJdbcTemplate().update(
                    "UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?", price, username);
            }
        });
    }
}

A TransactionTemplate can accept a transaction callback object that implements either the TransactionCallback<T> or an instance of the one implementer of that interface provided by the framework, the TransactionCallbackWithoutResult class. For the code block in the purchase() method for deducting the book stock and account balance, there’s no result to be returned, so TransactionCallbackWithoutResult is fine. For any code blocks with return values, you should implement the TransactionCallback<T> interface instead. The return value of the callback object will finally be returned by the template’s T execute() method. The main benefit is that the responsibility of starting, rolling back, or committing the transaction has been removed.

During the execution of the callback object, if it throws an unchecked exception (e.g., RuntimeException and DataAccessException fall into this category) or if you explicitly called setRollbackOnly() on the TransactionStatus argument in the doInTransactionWithoutResult method, the transaction will be rolled back. Otherwise, it will be committed after the callback object completes.

In the bean configuration file, the bookshop bean still requires a transaction manager to create a TransactionTemplate.

@Configuration
public class BookstoreConfiguration {
...
    @Bean
    public DataSourceTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }


    @Bean
    public BookShop bookShop() {
        TransactionalJdbcBookShop bookShop = new TransactionalJdbcBookShop();
        bookShop.setDataSource(dataSource());
        bookShop.setTransactionManager(transactionManager());
        return bookShop;
    }
}

You can also have the IoC container inject a transaction template instead of creating it directly. Because a transaction template handles all transactions, there’s no need for your class to refer to the transaction manager anymore.

package com.apress.springrecipes.bookshop;
...
import org.springframework.transaction.support.TransactionTemplate;


public class TransactionalJdbcBookShop extends JdbcDaoSupport implements
    BookShop {


    private TransactionTemplate transactionTemplate;

    public void setTransactionTemplate(
        TransactionTemplate transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }


    public void purchase(final String isbn, final String username) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                ...
            }
        });
    }
}

Then you define a transaction template in the bean configuration file and inject it, instead of the transaction manager, into your bookshop bean. Notice that the transaction template instance can be used for more than one transactional bean because it is a thread-safe object. Finally, don’t forget to set the transaction manager property for your transaction template.

package com.apress.springrecipes.bookshop.config;
...
import org.springframework.transaction.support.TransactionTemplate;


@Configuration
public class BookstoreConfiguration {
...
    @Bean
    public DataSourceTransactionManager transactionManager() { ... }


    @Bean
    public TransactionTemplate transactionTemplate() {
        TransactionTemplate transactionTemplate = new TransactionTemplate();
        transactionTemplate.setTransactionManager(transactionManager());
        return transactionTemplate;
    }


    @Bean
    public BookShop bookShop() {
        TransactionalJdbcBookShop bookShop = new TransactionalJdbcBookShop();
        bookShop.setDataSource(dataSource());
        bookShop.setTransactionTemplate(transactionTemplate());
        return bookShop;
    }
}

10-5. Manage Transactions Declaratively with the @Transactional Annotation

Problem

Declaring transactions in the bean configuration file requires knowledge of AOP concepts such as pointcuts, advices, and advisors. Developers who lack this knowledge might find it hard to enable declarative transaction management .

Solution

Spring allows you to declare transactions simply by annotating your transactional methods with @Transactional and adding the @EnableTransactionManegement annotation to your configuration class.

How It Works

To define a method as transactional, you can simply annotate it with @Transactional. Note that you should only annotate public methods because of the proxy-based limitations of Spring AOP.

package com.apress.springrecipes.bookshop;

import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.transaction.annotation.Transactional;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {

    @Transactional
    public void purchase(final String isbn, final String username) {


        int price = getJdbcTemplate().queryForObject(
            "SELECT PRICE FROM BOOK WHERE ISBN = ?", Integer.class, isbn);


        getJdbcTemplate().update(
            "UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?", isbn);


        getJdbcTemplate().update(
            "UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?", price, username);
    }
}

Note that because you are extending JdbcDaoSupport, you no longer need the setter for the DataSource; remove it from your DAO class.

You may apply the @Transactional annotation at the method level or the class level. When applying this annotation to a class, all the public methods within this class will be defined as transactional. Although you can apply @Transactional to interfaces or method declarations in an interface, it’s not recommended because it may not work properly with class-based proxies (i.e., CGLIB proxies).

In the Java configuration class, you only have to add the @EnableTransactionManagement annotation. That’s all you need to make it work. Spring will advise methods with @Transactional, or methods in a class with @Transactional, from beans declared in the IoC container . As a result, Spring can manage transactions for these methods.

@Configuration
@EnableTransactionManagement
public class BookstoreConfiguration {  ... }

10-6. Set the Propagation Transaction Attribute

Problem

When a transactional method is called by another method, it is necessary to specify how the transaction should be propagated. For example, the method may continue to run within the existing transaction, or it may start a new transaction and run within its own transaction.

Solution

A transaction’s propagation behavior can be specified by the propagation transaction attribute. Spring defines seven propagation behaviors, as shown in Table 10-5. These behaviors are defined in the org.springframework.transaction.TransactionDefinition interface. Note that not all types of transaction managers support all of these propagation behaviors. Their behavior is contingent on the underlying resource. Databases, for example, may support varying isolation levels, which constrains what propagation behaviors the transaction manager can support.

Table 10-5. Propagation Behaviors Supported by Spring

Propagation

Description

REQUIRED

If there’s an existing transaction in progress, the current method should run within this transaction. Otherwise, it should start a new transaction and run within its own transaction.

REQUIRES_NEW

The current method must start a new transaction and run within its own transaction. If there’s an existing transaction in progress, it should be suspended.

SUPPORTS

If there’s an existing transaction in progress, the current method can run within this transaction. Otherwise, it is not necessary to run within a transaction.

NOT_SUPPORTED

The current method should not run within a transaction. If there’s an existing transaction in progress, it should be suspended.

MANDATORY

The current method must run within a transaction. If there’s no existing transaction in progress, an exception will be thrown.

NEVER

The current method should not run within a transaction. If there’s an existing transaction in progress, an exception will be thrown.

NESTED

If there’s an existing transaction in progress, the current method should run within the nested transaction (supported by the JDBC 3.0 savepoint feature) of this transaction. Otherwise, it should start a new transaction and run within its own transaction. This feature is unique to Spring (whereas the previous propagation behaviors have analogs in Java EE transaction propagation). The behavior is useful for situations such as batch processing, in which you’ve got a long-running process (imagine processing 1 million records) and you want to chunk the commits on the batch. So, you commit every 10,000 records. If something goes wrong, you roll back the nested transaction and you’ve lost only 10,000 records worth of work (as opposed to the entire 1 million).

How It Works

Transaction propagation happens when a transactional method is called by another method. For example, suppose a customer would like to check out all books to purchase at the bookshop cashier. To support this operation, you define the Cashier interface as follows:

package com.apress.springrecipes.bookshop;
...
public interface Cashier {


    public void checkout(List<String> isbns, String username);
}

You can implement this interface by delegating the purchases to a bookshop bean by calling its purchase() method multiple times. Note that the checkout() method is made transactional by applying the @Transactional annotation.

package com.apress.springrecipes.bookshop;
...
import org.springframework.transaction.annotation.Transactional;


public class BookShopCashier implements Cashier {

    private BookShop bookShop;

    public void setBookShop(BookShop bookShop) {
        this.bookShop = bookShop;
    }


    @Transactional
    public void checkout(List<String> isbns, String username) {
        for (String isbn : isbns) {
            bookShop.purchase(isbn, username);
        }
    }
}

Then define a cashier bean in your bean configuration file and refer to the bookshop bean for purchasing books.

@Configuration
@EnableTransactionManagement()
public class BookstoreConfiguration {
...


    @Bean
    public Cashier cashier() {
        BookShopCashier cashier = new BookShopCashier();
        cashier.setBookShop(bookShop());
        return cashier;
    }
}

To illustrate the propagation behavior of a transaction, enter the data shown in Tables 10-6, 10-7, and 10-8 in your bookshop database .

Table 10-6. Sample Data in the BOOK Table for Testing Propagation Behaviors

ISBN

BOOK_NAME

PRICE

0001

The First Book

30

0002

The Second Book

50

Table 10-7. Sample Data in the BOOK_STOCK Table for Testing Propagation Behaviors

ISBN

STOCK

0001

10

0002

10

Table 10-8. Sample Data in the ACCOUNT Table for Testing Propagation Behaviors

USERNAME

BALANCE

user1

40

Use the REQUIRED Propagation Behavior

When the user user1 checks out two books from the cashier, the balance is sufficient to purchase the first book but not the second.

package com.apress.springrecipes.bookshop.spring;
...
public class Main {


    public static void main(String[] args) {
        ...
        Cashier cashier = context.getBean(Cashier.class);
        List<String> isbnList = Arrays.asList(new String[] { "0001", "0002"});
        cashier.checkout(isbnList, "user1");
    }
}

When the bookshop’s purchase() method is called by another transactional method, such as checkout(), it will run within the existing transaction by default. This default propagation behavior is called REQUIRED. That means there will be only one transaction whose boundary is the beginning and ending of the checkout() method. This transaction will be committed only at the end of the checkout() method. As a result, the user can purchase none of the books.

Figure 10-3 illustrates the REQUIRED propagation behavior.

A314861_4_En_10_Fig3_HTML.gif
Figure 10-3. The REQUIRED transaction propagation behavior

However, if the purchase() method is called by a nontransactional method and there’s no existing transaction in progress, it will start a new transaction and run within its own transaction. The propagation transaction attribute can be defined in the @Transactional annotation. For example, you can set the REQUIRED behavior for this attribute as follows. In fact, this is unnecessary, because it’s the default behavior.

package com.apress.springrecipes.bookshop.spring;
...
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
    @Transactional(propagation = Propagation.REQUIRED)
    public void purchase(String isbn, String username) {
        ...
    }
}
package com.apress.springrecipes.bookshop.spring;
...
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

public class BookShopCashier implements Cashier {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void checkout(List<String> isbns, String username) {
        ...
    }
}

Use the REQUIRES_NEW Propagation Behavior

Another common propagation behavior is REQUIRES_NEW. This indicates that the method must start a new transaction and run within its new transaction. If there’s an existing transaction in progress, it should be suspended first (for example, with the checkout method on BookShopCashier, with a propagation of REQUIRED).

package com.apress.springrecipes.bookshop.spring;
...
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void purchase(String isbn, String username) {
        ...
    }
}

In this case, there will be three transactions started in total. The first transaction is started by the checkout() method, but when the first purchase() method is called, the first transaction will be suspended, and a new transaction will be started. At the end of the first purchase() method, the new transaction completes and commits. When the second purchase() method is called, another new transaction will be started. However, this transaction will fail and roll back. As a result, the first book will be purchased successfully, while the second will not. Figure 10-4 illustrates the REQUIRES_NEW propagation behavior.

A314861_4_En_10_Fig4_HTML.gif
Figure 10-4. The REQUIRES_NEW transaction propagation behavior

10-7. Set the Isolation Transaction Attribute

Problem

When multiple transactions of the same application or different applications are operating concurrently on the same data set, many unexpected problems may arise. You must specify how you expect your transactions to be isolated from one another.

Solution

The problems caused by concurrent transactions can be categorized into four types.

  • Dirty read: For the two transactions T1 and T2, T1 reads a field that has been updated by T2 but not yet committed. Later, if T2 rolls back, the field read by T1 will be temporary and invalid.

  • Nonrepeatable read: For the two transactions T1 and T2, T1 reads a field and then T2 updates the field. Later, if T1 reads the same field again, the value will be different.

  • Phantom read: For the two transactions T1 and T2, T1 reads some rows from a table, and then T2 inserts new rows into the table. Later, if T1 reads the same table again, there will be additional rows.

  • Lost updates: For the two transactions T1 and T2, they both select a row for update and, based on the state of that row, make an update to it. Thus, one overwrites the other when the second transaction to commit should have waited until the first one committed before performing its selection.

In theory, transactions should be completely isolated from each other (i.e., serializable) to avoid all the mentioned problems. However, this isolation level will have great impact on performance because transactions have to run in serial order. In practice, transactions can run in lower isolation levels in order to improve performance.

A transaction’s isolation level can be specified by the isolation transaction attribute. Spring supports five isolation levels, as shown in Table 10-9. These levels are defined in the org.springframework.transaction.TransactionDefinition interface.

Table 10-9. Isolation Levels Supported by Spring

Isolation

Description

DEFAULT

Uses the default isolation level of the underlying database. For most databases, the default isolation level is READ_COMMITTED.

READ_UNCOMMITTED

Allows a transaction to read uncommitted changes by other transactions. The dirty read, nonrepeatable read, and phantom read problems may occur.

READ_COMMITTED

Allows a transaction to read only those changes that have been committed by other transactions. The dirty read problem can be avoided, but the nonrepeatable read and phantom read problems may still occur.

REPEATABLE_READ

Ensures that a transaction can read identical values from a field multiple times. For the duration of this transaction, updates made by other transactions to this field are prohibited. The dirty read and nonrepeatable read problems can be avoided, but the phantom read problem may still occur.

SERIALIZABLE

Ensures that a transaction can read identical rows from a table multiple times. For the duration of this transaction, inserts, updates, and deletes made by other transactions to this table are prohibited. All the concurrency problems can be avoided, but the performance will be low.

Note

Transaction isolation is supported by the underlying database engine but not an application or a framework. However, not all database engines support all these isolation levels. You can change the isolation level of a JDBC connection by calling the setTransactionIsolation() method on the java.sql.Connection interface.

How It Works

To illustrate the problems caused by concurrent transactions , let’s add two new operations to your bookshop for increasing and checking the book stock.

package com.apress.springrecipes.bookshop;

public interface BookShop {
    ...
    public void increaseStock(String isbn, int stock);
    public int checkStock(String isbn);
}

Then, you implement these operations as follows. Note that these two operations should also be declared as transactional.

package com.apress.springrecipes.bookshop;

import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {

    @Transactional
    public void purchase(String isbn, String username) {
        int price = getJdbcTemplate().queryForObject(
            "SELECT PRICE FROM BOOK WHERE ISBN = ?", Integer.class, isbn);


        getJdbcTemplate().update(
            "UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?", isbn );


        getJdbcTemplate().update(
            "UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?", price, username);
    }


    @Transactional
    public void increaseStock(String isbn, int stock) {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " - Prepare to increase book stock");


        getJdbcTemplate().update("UPDATE BOOK_STOCK SET STOCK = STOCK + ? WHERE ISBN = ?", stock, isbn);

        System.out.println(threadName + " - Book stock increased by " + stock);
        sleep(threadName);


        System.out.println(threadName + " - Book stock rolled back");
        throw new RuntimeException("Increased by mistake");
    }


    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public int checkStock(String isbn) {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " - Prepare to check book stock");


        int stock = getJdbcTemplate().queryForObject("SELECT STOCK FROM BOOK_STOCK WHERE ISBN = ?", Integer.class, isbn);

        System.out.println(threadName + " - Book stock is " + stock);
        sleep(threadName);


        return stock;
    }


    private void sleep(String threadName) {
        System.out.println(threadName + " - Sleeping");


        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
        }


        System.out.println(threadName + " - Wake up");
    }
}

To simulate concurrency, your operations need to be executed by multiple threads. You can track the current status of the operations through the println statements. For each operation, you print a couple of messages to the console around the SQL statement’s execution. The messages should include the thread name for you to know which thread is currently executing the operation.

After each operation executes the SQL statement, you ask the thread to sleep for ten seconds. As you know, the transaction will be committed or rolled back immediately once the operation completes. Inserting a sleep statement can help to postpone the commit or rollback. For the increase() operation, you eventually throw a RuntimeException to cause the transaction to roll back. Let’s look at a simple client that runs these examples.

Before you start with the isolation-level examples, enter the data from Tables 10-10 and 10-11 into your bookshop database . (Note that the ACCOUNT table isn’t needed in this example.)

Table 10-10. Sample Data in the BOOK Table for Testing Isolation Levels

ISBN

BOOK_NAME

PRICE

0001

The First Book

30

Table 10-11. Sample Data in the BOOK_STOCK Table for Testing Isolation Levels

ISBN

STOCK

0001

10

Use the READ_UNCOMMITTED and READ_COMMITTED Isolation Levels

READ_UNCOMMITTED is the lowest isolation level that allows a transaction to read uncommitted changes made by other transactions. You can set this isolation level in the @Transaction annotation of your checkStock() method.

package com.apress.springrecipes.bookshop.spring;
...
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
    ...
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public int checkStock(String isbn) {
        ...
    }
}

You can create some threads to experiment on this transaction isolation level. In the following Main class, there are two threads you are going to create. Thread 1 increases the book stock, while thread 2 checks the book stock. Thread 1 starts 5 seconds before thread 2.

package com.apress.springrecipes.bookshop.spring;
...
public class Main {


    public static void main(String[] args) {
        ...
        final BookShop bookShop = context.getBean(BookShop.class);


        Thread thread1 = new Thread(() -> {
            try {
                bookShop.increaseStock("0001", 5);
            } catch (RuntimeException e) {}
        }, "Thread 1");


        Thread thread2 = new Thread(() -> {
            bookShop.checkStock("0001");
        }, "Thread 2");


        thread1.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {}
            thread2.start();
    }
}

If you run the application, you will get the following result:

Thread 1―Prepare to increase book stock
Thread 1―Book stock increased by 5
Thread 1―Sleeping
Thread 2―Prepare to check book stock
Thread 2―Book stock is 15
Thread 2―Sleeping
Thread 1―Wake up
Thread 1―Book stock rolled back
Thread 2―Wake up

First, thread 1 increased the book stock and then went to sleep. At that time, thread 1’s transaction had not yet been rolled back. While thread 1 was sleeping, thread 2 started and attempted to read the book stock. With the READ_UNCOMMITTED isolation level, thread 2 would be able to read the stock value that had been updated by an uncommitted transaction.

However, when thread 1 wakes up, its transaction will be rolled back because of a RuntimeException, so the value read by thread 2 is temporary and invalid. This problem is known as a dirty read because a transaction may read values that are “dirty.”

To avoid the dirty read problem, you should raise the isolation level of checkStock() to READ_COMMITTED.

package com.apress.springrecipes.bookshop.spring;
...
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
    ...
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public int checkStock(String isbn) {
        ...
    }
}

If you run the application again, thread 2 won’t be able to read the book stock until thread 1 has rolled back the transaction. In this way, the dirty read problem can be avoided by preventing a transaction from reading a field that has been updated by another uncommitted transaction.

Thread 1―Prepare to increase book stock
Thread 1―Book stock increased by 5
Thread 1―Sleeping
Thread 2―Prepare to check book stock
Thread 1―Wake up
Thread 1―Book stock rolled back
Thread 2―Book stock is 10
Thread 2―Sleeping
Thread 2―Wake up

For the underlying database to support the READ_COMMITTED isolation level, it may acquire an update lock on a row that was updated but not yet committed. Then, other transactions must wait to read that row until the update lock is released, which happens when the locking transaction commits or rolls back.

Use the REPEATABLE_READ Isolation Level

Now, let’s restructure the threads to demonstrate another concurrency problem. Swap the tasks of the two threads so that thread 1 checks the book stock before thread 2 increases the book stock.

package com.apress.springrecipes.bookshop.spring;
...
public class Main {


    public static void main(String[] args) {
        ...
        final BookShop bookShop = (BookShop) context.getBean("bookShop");


        Thread thread1 = new Thread(() -> {
            public void run() {
                bookShop.checkStock("0001");
            }
        }, "Thread 1");


        Thread thread2 = new Thread(() -> {
            try {
                bookShop.increaseStock("0001", 5);
            } catch (RuntimeException e) {}
        }, "Thread 2");


        thread1.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {}
            thread2.start();
    }
}

If you run the application, you will get the following result:

Thread 1―Prepare to check book stock
Thread 1―Book stock is 10
Thread 1―Sleeping
Thread 2―Prepare to increase book stock
Thread 2―Book stock increased by 5
Thread 2―Sleeping
Thread 1―Wake up
Thread 2―Wake up
Thread 2―Book stock rolled back

First, thread 1 read the book stock and then went to sleep. At that time, thread 1’s transaction had not yet been committed. While thread 1 was sleeping, thread 2 started and attempted to increase the book stock. With the READ_COMMITTED isolation level, thread 2 would be able to update the stock value that was read by an uncommitted transaction.

However, if thread 1 reads the book stock again, the value will be different from its first read. This problem is known as a nonrepeatable read because a transaction may read different values for the same field.

To avoid the nonrepeatable read problem, you should raise the isolation level of checkStock() to REPEATABLE_READ.

package com.apress.springrecipes.bookshop.spring;
...
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
    ...
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public int checkStock(String isbn) {
        ...
    }
}

If you run the application again, thread 2 won’t be able to update the book stock until thread 1 has committed the transaction. In this way, the nonrepeatable read problem can be avoided by preventing a transaction from updating a value that has been read by another uncommitted transaction.

Thread 1―Prepare to check book stock
Thread 1―Book stock is 10
Thread 1―Sleeping
Thread 2―Prepare to increase book stock
Thread 1―Wake up
Thread 2―Book stock increased by 5
Thread 2―Sleeping
Thread 2―Wake up
Thread 2―Book stock rolled back

For the underlying database to support the REPEATABLE_READ isolation level, it may acquire a read lock on a row that was read but not yet committed. Then, other transactions must wait to update the row until the read lock is released, which happens when the locking transaction commits or rolls back.

Use the SERIALIZABLE Isolation Level

After a transaction has read several rows from a table, another transaction inserts new rows into the same table. If the first transaction reads the same table again, it will find additional rows that are different from the first read. This problem is known as a phantom read. Actually, a phantom read is very similar to a nonrepeatable read but involves multiple rows.

To avoid the phantom read problem, you should raise the isolation level to the highest: SERIALIZABLE. Notice that this isolation level is the slowest because it may acquire a read lock on the full table. In practice, you should always choose the lowest isolation level that can satisfy your requirements.

10-8. Set the Rollback Transaction Attribute

Problem

By default, only unchecked exceptions (i.e., of type RuntimeException and Error) will cause a transaction to roll back, while checked exceptions will not. Sometimes, you may want to break this rule and set your own exceptions for rolling back.

Solution

The exceptions that cause a transaction to roll back or not can be specified by the rollback transaction attribute. Any exceptions not explicitly specified in this attribute will be handled by the default rollback rule (i.e., rolling back for unchecked exceptions and not rolling back for checked exceptions).

How It Works

A transaction’s rollback rule can be defined in the @Transactional annotation via the rollbackFor and noRollbackFor attributes. These two attributes are declared as Class[], so you can specify more than one exception for each attribute.

package com.apress.springrecipes.bookshop.spring;
...
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
    ...
    @Transactional(
        propagation = Propagation.REQUIRES_NEW,
        rollbackFor = IOException.class,
        noRollbackFor = ArithmeticException.class)
    public void purchase(String isbn, String username) throws Exception {
        throw new ArithmeticException();
    }
}

10-9. Set the Timeout and Read-Only Transaction Attributes

Problem

Because a transaction may acquire locks on rows and tables, a long transaction will tie up resources and have an impact on overall performance. Besides, if a transaction only reads but does not update data, the database engine could optimize this transaction. You can specify these attributes to increase the performance of your application.

Solution

The timeout transaction attribute (an integer that describes seconds) indicates how long your transaction can survive before it is forced to roll back. This can prevent a long transaction from tying up resources. The read-only attribute indicates that this transaction will only read but not update data. The read-only flag is just a hint to enable a resource to optimize the transaction, and a resource might not necessarily cause a failure if a write is attempted.

How It Works

The timeout and read-only transaction attributes can be defined in the @Transactional annotation. Note that the timeout is measured in seconds.

package com.apress.springrecipes.bookshop.spring;
...
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;


public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
    ...
    @Transactional(
        isolation = Isolation.REPEATABLE_READ,
        timeout = 30,
        readOnly = true)
    public int checkStock(String isbn) {
        ...
    }
}

10-10. Manage Transactions with Load-Time Weaving

Problem

By default, Spring’s declarative transaction management is enabled via its AOP framework. However, as Spring AOP can only advise public methods of beans declared in the IoC container , you are restricted to managing transactions within this scope using Spring AOP. Sometimes you may want to manage transactions for nonpublic methods, or methods of objects created outside the Spring IoC container (e.g., domain objects).

Solution

Spring provides an AspectJ aspect named AnnotationTransactionAspect that can manage transactions for any methods of any objects, even if the methods are nonpublic or the objects are created outside the Spring IoC container. This aspect will manage transactions for any methods with the @Transactional annotation. You can choose either AspectJ’s compile-time weaving or load-time weaving to enable this aspect.

How It Works

To weave this aspect into your domain classes at load time, you have to put the @EnableLoadTimeWeaving annotation on your configuration class. To enable Spring’s AnnotationTransactionAspect for transaction management, you just define the @EnableTransactionManagement annotation and set its mode attribute to ASPECTJ. The @EnableTransactionManagement annotation takes two values for the mode attribute: ASPECTJ and PROXY. ASPECTJ stipulates that the container should use load-time or compile-time weaving to enable the transaction advice. This requires the spring-instrument JAR to be on the classpath, as well as the appropriate configuration at load time or compile time.

Alternatively, PROXY stipulates that the container should use the Spring AOP mechanisms. It’s important to note that the ASPECTJ mode doesn’t support the configuration of the @Transactional annotation on interfaces. Then the transaction aspect will automatically get enabled. You also have to provide a transaction manager for this aspect. By default, it will look for a transaction manager whose name is transactionManager.

package com.apress.springrecipes.bookshop;

Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
@EnableLoadTimeWeaving
public class BookstoreConfiguration { ... }
Note

To use the Spring aspect library for AspectJ, you have to include the spring-aspects module on your CLASSPATH. To enable load-time weaving, you also have to include a Java agent, which is available in the spring-instrument module.

For a simple Java application, you can weave this aspect into your classes at load time with the Spring agent specified as a VM argument.

java -javaagent:lib/spring-instrument-5.0.0.RELEASE.jar -jar recipe_10_10_i.jar

Summary

This chapter discussed transactions and why you should use them. You explored the approach taken for transaction management historically in Java EE and then learned how the approach the Spring framework offers differs. You explored the explicit use of transactions in your code as well as the implicit use with annotation-driven aspects. You set up a database and used transactions to enforce valid state in the database.

In the next chapter, you will explore Spring Batch. Spring Batch provides infrastructure and components that can be used as the foundation for batch processing jobs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset