Regular TestConfigDao For Runtime Boundaries
這次整理 TEST_CONFIG 查詢時,真正要處理的不是「哪個專案要不要自己查測試站設定」,而是 runtime 邊界。
以前公司把測試站判斷集中在 PmbBase,Web/JNDI 專案都走同一套邏輯。這有一個很實際的好處:測試站的 WAR 可以由管理者原封不動搬到正式機,不需要重新打包,也能確定測試與正式是同一包程式。
但專案開始升 JDK 17、也開始出現 Spring CLI、純 CLI 之後,狀況就不一樣了。共用函式庫如果仍假設所有呼叫端都有 Tomcat JNDI,就會把 Web runtime 的前提帶進 CLI;反過來,如果每個 CLI 都自己查 TEST_CONFIG,SQL 與 IS_TEST 判斷又會散掉。
所以比較正規的分工是:
- TsLib 集中
TEST_CONFIG的 SQL 與判斷語意 - 各 runtime 只提供自己的連線來源
- 寄信 policy、系統紀錄、filter 等呼叫端只吃判斷結果或 policy,不各自重寫
TEST_CONFIG
問題長相
原本容易寫成這樣:
1
boolean isTest = new TestConfigDao().isTest();
如果這個 TestConfigDao 來自 PmbBase,它通常隱含 Web/JNDI 前提。對 TsMgr 這種傳統 WAR 還可以,但對 Spring CLI 就不自然,因為 Spring CLI 的 connection 是 Spring DataSource 管的。
另一個方向是每個專案自己寫:
1
jdbcTemplate.queryForObject("SELECT IS_TEST FROM TEST_CONFIG", String.class);
這樣短期看起來很快,但幾個專案一多,IS_TEST 是不是空白算 false、沒有資料列算 false、SQL 失敗怎麼處理,就會開始不一致。
正規 TestConfigDao
最後比較合理的核心長這樣:
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
public class TestConfigDao {
private final DbConnectionProvider dataSource;
public TestConfigDao() {
this(new JndiNtnupmbDataSource());
}
public TestConfigDao(DbConnectionProvider dataSource) {
this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
}
public boolean isTest() throws DbConnFailException {
String sql = "SELECT IS_TEST FROM TEST_CONFIG";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
if (!rs.next()) {
return false;
}
String isTest = rs.getString("IS_TEST");
return isTest != null && !isTest.isBlank();
} catch (SQLException ex) {
throw new DbConnFailException("查詢 TEST_CONFIG 失敗: " + ex.getMessage());
}
}
}
這段的重點不是 SELECT 有多短,而是邊界清楚:
TestConfigDao知道TEST_CONFIG表與IS_TEST欄位DbConnectionProvider把 Web/JNDI、純 CLI、Spring CLI 的連線來源隔開- SQL 失敗與連線失敗都變成
DbConnFailException
為什麼不是 Spring DataAccessException
在 Oracle SQLException Javadoc 這邊有講到:
1
public class SQLException extends Exception
也就是 JDBC 原生 SQL 失敗是 checked exception。
在 Oracle Java Tutorial 的 Catch or Specify Requirement 這邊有講到:
1
All exceptions are checked exceptions, except for those indicated by Error, RuntimeException, and their subclasses.
所以如果 TsLib 選擇走純 JDBC,不引入 Spring,那 SQLException 本來就會要求你在 DAO 邊界處理。
另一方面,Spring JDBC 的選擇不同。在 Spring Framework JDBC Core 這邊有講到:
1
do not propagate SQLException but rather DataAccessException
而 Spring DataAccessException Javadoc 顯示它的類別階層是:
1
public abstract class DataAccessException extends NestedRuntimeException
也就是 Spring JDBC 的慣例是把 SQL 失敗轉成 runtime exception。
但 TsLib 目前不適合為了 TEST_CONFIG 把 Spring 帶進來。TestConfigDao 在 TsLib 內用純 JDBC,並把查詢失敗收斂成既有的 DbConnFailException,會比半套混用 checked 與 runtime 更一致。
三種 runtime 怎麼接
classDiagram
class TestConfigDao {
-DbConnectionProvider dataSource
+isTest() boolean
}
class DbConnectionProvider {
<<interface>>
+getConnection() Connection
}
class JndiNtnupmbDataSource
class PlainCliConnectionProvider
class SpringDataSourceAdapter
TestConfigDao --> DbConnectionProvider : uses
JndiNtnupmbDataSource ..|> DbConnectionProvider
PlainCliConnectionProvider ..|> DbConnectionProvider
SpringDataSourceAdapter ..|> DbConnectionProvider
Web/JNDI 可以直接用預設建構子:
1
boolean isTest = new TestConfigDao().isTest();
純 CLI 用自己從啟動參數建立的 connection provider:
1
MailDeliveryPolicy policy = MailDeliveryPolicies.fromTestConfig(ntnupmbConfig);
Spring CLI 用 Adapter 把 Spring DataSource 接到 TsLib 介面:
1
2
3
4
5
6
7
8
DbConnectionProvider provider = () -> {
try {
return dataSource.getConnection();
} catch (SQLException ex) {
throw new DbConnFailException("資料庫連線失敗: " + ex.getMessage());
}
};
MailDeliveryPolicy policy = MailDeliveryPolicies.fromTestConfig(provider);
這不是放棄 Strategy Pattern。Strategy 仍然在 mail sender 那層:MailDeliveryPolicy 決定正式寄送或測試寄送。TestConfigDao 則是 Strategy 建立前要用到的設定查詢。
改完後的正軌
第一步是把 TsLib 的 TestConfigDao 寫正:SQL、欄位語意、空白值語意、例外語意都集中在這裡。
第二步是把還直接碰 PmbBase TestConfigDao 的地方收回來。這裡目前至少有兩條:
TsLib.mail.MailDeliveryPolicies.legacyJndiTestConfig()TsLib.logging.ITCLogWriter
第三步才是檢查各 runtime:
| runtime | 應該負責 | 不應該負責 |
|---|---|---|
| Web/JNDI | 提供 JNDI connection 或明確 policy | 重寫 TEST_CONFIG SQL |
| 純 CLI | 從參數建立 connection provider | 自己解讀 IS_TEST |
| Spring CLI | 把 DataSource 轉成 DbConnectionProvider | 把 Spring DAO 複製一份到專案內 |
最後再回頭看 TsMgr。TsMgr 可以逐步從 legacy JNDI policy 改成明確傳入 mail policy,但這應該和它的 filter、listener、controller 如何取得測試站判斷一起整理。目標不是追求形式一致,而是讓「誰查 TEST_CONFIG、誰決定寄信 policy、誰只提供 connection」這三件事分開。
結論
TestConfigDao 不應該是每個專案各寫一份的小工具,也不應該把 Spring、Tomcat JNDI、純 CLI 全塞在同一個類別裡判斷。
比較正規的做法是:TsLib 集中 TEST_CONFIG 的查詢語意;runtime 提供 connection;mail sender 使用 policy。這樣才能同時保留公司原本「同一包 WAR 從測試搬到正式」的部署習慣,又讓 JDK 17、Spring CLI、純 CLI 專案不被舊 Web/JNDI 前提綁住。