Post

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 再起動が必要です。

手順:

  1. 修正を deploy
  2. Tomcat を再起動
  3. probe で jdbc/ntnupmb を確認
  4. 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.