あ、今日はクリスマス・イブですね〜
そんなことはお構いなしに・・・今回は、Spring Boot上で@Transactional
(アノテーション駆動のトランザクション管理)を使わずにトランザクション制御を行う方法を紹介します。
ってか@Transactional
使えばいいんじゃん・・・と思う方もいるかと思いますが、@Transaction
を指定できるのは自分達で作るコンポーネントだけなので、Springに依存しない3rdパーティー製のOSSライブラリなどのメソッドにトランザクションをかけることはできません(あたりまえですね・・・)。
どうすればいいの?
Springにはアノテーションを使用してトランザクション対象のメソッドを指定する方法に加えて、
- 指定したメソッド名(パターン)に一致するメソッドを対象にする
- 指定したクラスの指定したメソッド名(パターン)に一致するメソッドを対象にする
- 全てのメソッドを対象にする
方法などがサポートされています。
昔からSpringを使っていた方は、XMLを使ったBean定義で以下のようなトランザクション制御の宣言を見たことがあるのではないでしょうか?この例では、TxDemoApplication
クラスの全てのpublicメソッドをトランザクション制御対象にしています。
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
">
<tx:advice id="transactionAdvisor">
<tx:attributes>
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="txDemoApplicationPointcut" expression="execution(* com.example.TxDemoApplication.*(..))"/>
<aop:advisor advice-ref="transactionAdvisor" pointcut-ref="txDemoApplicationPointcut"/>
</aop:config>
</beans>
Spring Boot上でも、↑のようなXMLファイルを作成して以下のように@ImportResource
を使って読み込ませれば動きます。
@SpringBootApplication
@ImportResource("classpath:/transactionContext.xml")
public class TxDemoApplication implements CommandLineRunner {
// ...
}
が、しかし・・・今更XMLファイルに戻りたくないし・・・、Spring BootからSpringを使っている方(Java Configしか知らない方)は、XML使ったBean定義の方法なんてしらね〜よって方もいますよね
Java Configを使って同じことを実現しよう!!
Java Configを使って同じことを表現する場合は・・・以下のようなJava Configを作るだけです。
@Configuration
public class TransactionConfig {
@Bean
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
PlatformTransactionManager transactionManager) {
// トランザクション管理するメソッドとトランザクション制御に必要な属性値を指定する
MethodMapTransactionAttributeSource source = new MethodMapTransactionAttributeSource();
source.addTransactionalMethod(TxDemoApplication.class, "*", new RuleBasedTransactionAttribute());
// トランザクション制御を行うAOPのAdvisorを生成する
// トランザクション制御用のAdvice(TransactionInteceptor)の適用箇所を指定するポイントカットは、↑で指定したメソッドと連動する仕組みになっている
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(source);
advisor.setAdvice(new TransactionInterceptor(transactionManager, source));
return advisor;
}
}
Note:
この仕組みを利用する場合は、クラスパス内に
org.aspectj:aspectjweaver
のJARが必要になるので、依存ライブラリとしてorg.springframework.boot:spring-boot-starter-aop
を追加しましょう。pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
Warning:
Spring Bootを使うと、自動コンフィギュレーションの仕組みによって
@Transactional
(アノテーション駆動のトランザクション管理)を使ってトランザクション制御する仕組みも有効になっています。そのため・・・仮に↑のJava Configで指定したメソッドに@Transactional
が付与されていると、トランザクション制御用のAdvice(TransactionInteceptor
)が二重で適用されてしまうので注意しましょう。
TransactionAttributeSourceの種類
↑の例ではMethodMapTransactionAttributeSource
を使っていますが、SpringはTransactionAttributeSource
の実装クラスをいくつか用意しています。
クラス名 | 説明 |
---|---|
NameMatchTransactionAttributeSource |
指定したメソッド名(パターン)に一致するメソッドに対してトランザクション制御を適用する |
MethodMapTransactionAttributeSource |
指定したクラスの指定したメソッド名(パターン)に対してトランザクション制御を適用する |
MatchAlwaysTransactionAttributeSource |
全てのメソッドに対してトランザクション制御を適用する |
AnnotationTransactionAttributeSource |
Spring、JTA、EJBのアノテーションが付与されたクラス・メソッドに対してトランザクション制御を適用する (Spring Bootの自動コンフィギュレーションを利用するとこのクラスが使われます) |
CompositeTransactionAttributeSource |
複数のTransactionAttributeSource を集約してトランザクション制御を適用する |
適用対象クラスのフィルタ
BeanFactoryTransactionAttributeSourceAdvisor
には、AOPの適用対象クラスをフィルタするオプションが用意されています。たとえば・・・@Service
が付与されているBeanだけを適用対象にしたい場合は、以下のようなBean定義すればOKです。
@Configuration
public class TransactionConfig {
@Bean
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
PlatformTransactionManager transactionManager) {
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
source.addTransactionalMethod("*", new RuleBasedTransactionAttribute());
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(source);
advisor.setAdvice(new TransactionInterceptor(transactionManager, source));
advisor.setClassFilter(clazz -> AnnotationUtils.findAnnotation(clazz, Service.class) != null); // フィルタ条件を実装し、`setClassFilter`を呼び出す
return advisor;
}
}
検証アプリ
以下に、動作検証用に作成したサンプルアプリケーションを載せておきます。
検証環境
- Spring Boot 1.4.3.RELEASE
- Spring Framework 4.3.5.RELEASE
- AspectJ 1.8.9
プロジェクトの作成
プロジェクトは「SPRING INITIALIZR」上で依存ライブラリに「JDBC」「H2」「AOP」を選んでダウンロードしてください。
ログレベルの変更
トランザクションが適用されているか確認するために、Spring JDBCのログの出力モードをdebugにします。
logging.level.org.springframework.jdbc=debug
アプリの作成とBnea定義
次に、Spring Bootアプリケーション内にCommandRunner
を実装し、トランザクション制御のBean定義を行います。
package com.example;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor;
import org.springframework.transaction.interceptor.MethodMapTransactionAttributeSource;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;
@SpringBootApplication
public class TxDemoApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(TxDemoApplication.class, args);
}
private final JdbcOperations jdbcOperations;
public TxDemoApplication(JdbcOperations jdbcOperations) {
this.jdbcOperations = jdbcOperations;
}
@Override // このメソッドをトランザクション制御対象にするけど・・・@Transactionalは付与しない!!
public void run(String... args) throws Exception {
Integer value = jdbcOperations.queryForObject("SELECT 1", Integer.class);
System.out.println(value);
}
@Configuration
static class TransactionConfig {
@Bean
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
PlatformTransactionManager transactionManager) {
MethodMapTransactionAttributeSource source = new MethodMapTransactionAttributeSource();
source.addTransactionalMethod(TxDemoApplication.class, "*", new RuleBasedTransactionAttribute());
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(source);
advisor.setAdvice(new TransactionInterceptor(transactionManager, source));
return advisor;
}
}
}
アプリの実行
では、Spring Bootアプリケーションを実行してみましょう。
$ ./mvnw spring-boot:run
...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.4.3.RELEASE)
2016-12-24 16:20:30.713 INFO 58327 --- [ main] com.example.TxDemoApplication : Starting TxDemoApplication on Kazuki-no-MacBook-Pro.local with PID 58327 (/Users/shimizukazuki/Downloads/tx-demo/target/classes started by shimizukazuki in /Users/shimizukazuki/Downloads/tx-demo)
2016-12-24 16:20:30.715 INFO 58327 --- [ main] com.example.TxDemoApplication : No active profile set, falling back to default profiles: default
2016-12-24 16:20:30.748 INFO 58327 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@1e2e2b3: startup date [Sat Dec 24 16:20:30 JST 2016]; root of context hierarchy
2016-12-24 16:20:31.454 INFO 58327 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2016-12-24 16:20:31.469 DEBUG 58327 --- [ main] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.example.TxDemoApplication$$EnhancerBySpringCGLIB$$9f83f17d.run]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2016-12-24 16:20:31.609 DEBUG 58327 --- [ main] o.s.j.d.DataSourceTransactionManager : Acquired Connection [ProxyConnection[PooledConnection[conn9: url=jdbc:h2:mem:testdb user=SA]]] for JDBC transaction
2016-12-24 16:20:31.611 DEBUG 58327 --- [ main] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [ProxyConnection[PooledConnection[conn9: url=jdbc:h2:mem:testdb user=SA]]] to manual commit
2016-12-24 16:20:31.618 DEBUG 58327 --- [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL query [SELECT 1]
1
2016-12-24 16:20:31.638 DEBUG 58327 --- [ main] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2016-12-24 16:20:31.638 DEBUG 58327 --- [ main] o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [ProxyConnection[PooledConnection[conn9: url=jdbc:h2:mem:testdb user=SA]]]
2016-12-24 16:20:31.639 DEBUG 58327 --- [ main] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [ProxyConnection[PooledConnection[conn9: url=jdbc:h2:mem:testdb user=SA]]] after transaction
2016-12-24 16:20:31.639 DEBUG 58327 --- [ main] o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
2016-12-24 16:20:31.642 INFO 58327 --- [ main] com.example.TxDemoApplication : Started TxDemoApplication in 1.106 seconds (JVM running for 3.466)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.612 s
[INFO] Finished at: 2016-12-24T16:20:31+09:00
[INFO] Final Memory: 24M/315M
[INFO] ------------------------------------------------------------------------
2016-12-24 16:20:31.742 INFO 58327 --- [ Thread-1] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@1e2e2b3: startup date [Sat Dec 24 16:20:30 JST 2016]; root of context hierarchy
2016-12-24 16:20:31.744 INFO 58327 --- [ Thread-1] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
コンソールログを見ると、SQLの実装前後にトランザクションの開始・コミット・終了が行われていることがわかります
まとめ
利用頻度は多くないかもしれませんが・・・@Transactional
が付与されていないメソッドに対してトランザクション制御できるよ〜というお話でした。
基本的には、自分たちで作るコンポーネントに対するトランザクション制御は@Transactional
を付与して行うのがよいと思いますが、アプリケーションの要件によっては今回紹介した方法が有効になるケースもあるかもしれません。
さいごに
Happy Christmas 2016 !!