Tomcat JNDI DataSource を redeploy で壊さない修正方法
Tomcat JNDI DataSource を redeploy で壊さない修正方法
問題
Tomcat 上でアプリを何回か redeploy / undeploy すると、probe で次のような状態になることがあります。
1
Cannot establish a database connection. Data source is closed
これはアプリ側が Tomcat 管理の JNDI DataSource を誤って close() している 可能性があります。
なぜ起きるのか
Spring の @Bean で JNDI から取ってきた DataSource をそのまま返すと、
アプリ終了時に Spring が bean の destroy 処理で close() を呼ぶ場合があります。
Tomcat が管理している DataSource は、Tomcat が生殺与奪を持つべきです。
アプリ側が閉じてはいけません。
悪い例
1
2
3
4
5
6
7
8
9
10
11
@Bean
@Primary
public DataSource dataSource(DataSourceProperties properties) {
if (StringUtils.hasText(properties.getJndiName())) {
JndiDataSourceLookup lookup = new JndiDataSourceLookup();
lookup.setResourceRef(true);
return lookup.getDataSource(properties.getJndiName());
}
return properties.initializeDataSourceBuilder().build();
}
この書き方だと、JNDI から取った Tomcat 管理 DataSource を Spring が直接持つ形になります。
修正方針
JNDI から取った DataSource は、そのまま返さずに close() を持たない wrapper で包みます。
ポイント:
- JNDI DataSource は Tomcat に閉じさせる
- Spring は wrapper を使うだけにする
- JDBC URL から Spring 自身が生成した DataSource は従来通りでよい
1. wrapper クラスを書く
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package tw.edu.ntnu.itc.tsnoticeapproval.config.db;
import org.springframework.jdbc.datasource.DelegatingDataSource;
import javax.sql.DataSource;
/**
* Spring から close されない DataSource wrapper。
*/
public class NonClosingDataSource extends DelegatingDataSource {
/**
* コンテナ管理 DataSource を包む。
*
* @param targetDataSource Tomcat 管理の DataSource
*/
public NonClosingDataSource(DataSource targetDataSource) {
super(targetDataSource);
}
}
2. JNDI のときだけ wrapper を返す
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package tw.edu.ntnu.itc.tsnoticeapproval.config.db;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Configuration(proxyBeanMethods = false)
public class PrimaryDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.ntnupmb")
public ConfigurableDataSourceProperties ntnupmbDataSourceProperties() {
return new ConfigurableDataSourceProperties();
}
/**
* JNDI 利用時は NonClosingDataSource で包む。
*/
@Bean
@Primary
public DataSource ntnupmbDataSource(
@Qualifier("ntnupmbDataSourceProperties") ConfigurableDataSourceProperties properties
) {
if (StringUtils.hasText(properties.getJndiName())) {
JndiDataSourceLookup lookup = new JndiDataSourceLookup();
lookup.setResourceRef(true);
return new NonClosingDataSource(lookup.getDataSource(properties.getJndiName()));
}
return properties.initializeDataSourceBuilder().build();
}
}
3. 第二 DataSource も同じ考え方
1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public DataSource ntnusalaryDataSource(
@Qualifier("ntnusalaryDataSourceProperties") ConfigurableDataSourceProperties properties
) {
if (StringUtils.hasText(properties.getJndiName())) {
JndiDataSourceLookup lookup = new JndiDataSourceLookup();
lookup.setResourceRef(true);
return new NonClosingDataSource(lookup.getDataSource(properties.getJndiName()));
}
return properties.initializeDataSourceBuilder().build();
}
4. Spring Boot の自動設定任せの場合
spring.datasource.jndi-name をそのまま使っているプロジェクトでは、
自動設定に任せず、明示的に DataSource bean を定義した方が安全です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package tw.edu.ntnu.itc.tsnoticedownload.config.db;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Configuration(proxyBeanMethods = false)
public class PrimaryDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource dataSource(DataSourceProperties properties) {
if (StringUtils.hasText(properties.getJndiName())) {
JndiDataSourceLookup lookup = new JndiDataSourceLookup();
lookup.setResourceRef(true);
return new NonClosingDataSource(lookup.getDataSource(properties.getJndiName()));
}
return properties.initializeDataSourceBuilder().build();
}
}
5. destroyMethod="" ではだめか
@Bean(destroyMethod = "") でも回避できる場面はあります。
ただしその方法だと、JNDI ではない自前生成 DataSource まで Spring が閉じなくなることがあります。
今回のように
- JNDI のときだけ閉じさせたくない
- 非 JNDI のときは Spring に閉じさせたい
という要件なら、wrapper 方式の方が意図が明確です。
6. この修正で何が変わるか
修正前:
- redeploy 時に Spring が JNDI DataSource を閉じる可能性がある
- 何回か deploy すると
Data source is closed
修正後:
- JNDI DataSource の life cycle は Tomcat が管理
- Spring は wrapper 経由で利用するだけ
- redeploy しても datasource を壊しにくくなる
7. それでも今すでに壊れている場合
この修正は 今後壊さないための修正 です。
すでに probe で Data source is closed になっているなら、通常は Tomcat 再起動が必要です。
手順:
- 修正を deploy
- Tomcat を再起動
- probe で
jdbc/ntnupmbを確認 - redeploy を数回試して再確認
最小完成例
NonClosingDataSource.java
1
2
3
4
5
6
7
8
9
import org.springframework.jdbc.datasource.DelegatingDataSource;
import javax.sql.DataSource;
public class NonClosingDataSource extends DelegatingDataSource {
public NonClosingDataSource(DataSource targetDataSource) {
super(targetDataSource);
}
}
PrimaryDataSourceConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Bean
@Primary
public DataSource dataSource(DataSourceProperties properties) {
if (StringUtils.hasText(properties.getJndiName())) {
JndiDataSourceLookup lookup = new JndiDataSourceLookup();
lookup.setResourceRef(true);
return new NonClosingDataSource(lookup.getDataSource(properties.getJndiName()));
}
return properties.initializeDataSourceBuilder().build();
}
This post is licensed under CC BY 4.0 by the author.