14 Commits

Author SHA1 Message Date
bb79777f0c docs: 修复文档错误 2026-06-17 21:06:40 +08:00
cf911c3756 release 1.0.0 2026-06-17 20:57:43 +08:00
f471c1a489 docs: 调整文档描述 2026-06-17 20:41:09 +08:00
f35cfac913 docs: 更新 DefaultBeanRowMapper 类文档注释,强调使用场景 2026-06-17 20:27:26 +08:00
76255bf3eb docs: 修改 README.md
- 移除标题中的emoji
- 新增“设计考量与边界”章节
2026-06-17 18:47:34 +08:00
aeb57dcdba refactor: 将中断控制逻辑内化到 BatchUpdateResult 中
- BatchUpdateResult 新增 quietly 字段,由 recordErrorBatch() 自行判断
  状态设置为 INTERRUPTED 或 COMPLETED_WITH_ERRORS
- 移除 interrupt() 方法,消除 recordErrorBatch → interrupt 的时序耦合
- 外部调用方仅需 break 控制循环,不再干预结果对象的状态流转
2026-06-17 17:27:13 +08:00
a0df4136f4 refactor!: 移除 plusone-commons 及 Guava 依赖,内化工具方法
完全移除对外部工具库 plusone-commons 和 Guava 的编译期依赖,
将所需功能内化实现,使项目成为真正的零依赖轻量级 JDBC 封装库。

- 新增 AssertTools: 断言工具(checkArgument / checkNotNull / checkState / checkCondition)
- 新增 NamingTools: 命名转换(camelToSnake)
- 新增 ThrowingConsumer / ThrowingPredicate: 可抛受检异常的函数式接口
- 新增 AssertToolsTests: 完整覆盖断言工具所有方法及边界场景
- pom.xml 移除 plusone-dependencies BOM,直接声明 jsr305 / test 依赖版本
- DefaultBeanRowMapper 使用 NamingTools.camelToSnake 替代 Guava CaseFormat
- ParamBuilder 内联 Optional 展开逻辑,去除 OptionalTools / CollectionTools
- JdbcOperationSupport 用 null/长度判断替代 ArrayTools.isEmpty/isNotEmpty
- NOTICE 移除第三方依赖声明;README 移除无依赖分支说明
- 测试:新增缩写映射(URL/XML/ID/HTML/HTTP)覆盖、TransactionException 构造器测试

BREAKING CHANGE: BatchUpdateStatus 不再实现 IWithIntCode 接口;
plusone-commons 和 Guava 不再作为传递依赖提供。
2026-06-17 17:21:31 +08:00
ad0fbb788e docs: 添加对无依赖分支(feature/no-dependencies)的特性和适用情况的说明 2026-06-06 00:59:40 +08:00
d740ad8467 docs: 优化 README 文档结构与内容
- 调整章节顺序
- 为所有二级及以下标题添加序号
- 移除示例代码注释中的序号,避免与标题序号冲突
- 新增「连接池集成」章节(HikariCP/Druid/DBCP 2),快速开始中添加引导提示
- 快速开始各小节末尾添加跳转指引,关联至对应详细章节
- 补充事务管理章节的内容
2026-06-06 00:43:06 +08:00
e577f72d4f docs: 新增 CHANGELOG.md 文件记录各版本更新内容 2026-06-05 22:31:27 +08:00
ef39c4323c test: 补充测试数据库初始化脚本的注释 2026-06-05 22:31:27 +08:00
d70e12e254 docs: 补充 ParamBuilderSimpleJdbcTemplate 的文档注释 2026-06-05 22:31:27 +08:00
721d0bbd5e test: 添加事务异常测试用例 2026-06-05 22:31:27 +08:00
0b3675d3a4 test: 添加 buildParamsTemporal 测试方法验证时间类型参数构建 2026-06-05 22:31:27 +08:00
21 changed files with 1628 additions and 154 deletions

110
CHANGELOG.md Normal file
View File

@@ -0,0 +1,110 @@
# Changelog
## [1.0.0] - 2026-06-17
### 重构
- 移除 `plusone-commons` 及 Guava 依赖,内化 `AssertTools``NamingTools``ThrowingConsumer``ThrowingPredicate` 工具类
-`batchUpdate` 中断时对 `BatchUpdateResult` 的变更逻辑内化到 `BatchUpdateResult`
### 测试
- 添加 `ParamBuilderTest#buildParamsTemporal` 测试方法验证时间类型参数构建
- 添加 `TransactionTest` 事务异常测试用例
- 补充测试数据库初始化脚本注释
### 文档
- 新增 `CHANGELOG.md` 文件记录各版本更新内容
- 优化 README 文档结构与内容
- 补充 `ParamBuilder``SimpleJdbcTemplate` 的文档注释
- 更新 `DefaultBeanRowMapper` 类文档注释,强调使用场景
---
## [1.0.0-RC3] - 2026-06-05
### 新增
- `ResultHandler` 新增静态工厂方法 `mapToList(RowMapper)`,消除各处 `ResultSet` 遍历重复代码
### 重构
- `JdbcOperationSupport` 中无参数 SQL 统一改用 `Statement` 执行,避免创建空参数 `PreparedStatement`
- `ParamBuilder#buildParams` 为常用数据类型(`CharSequence`/`Number`/`Boolean`/`Temporal`)添加短路处理,优化参数构建性能
### 测试
- 重构 `UpdateTest`,补全 `Statement` 路径覆盖
### 文档
- 更新 README.md 使用说明和示例
- 更新 `DefaultBeanRowMapper` 类文档以避免歧义
- 更新项目简介
---
## [1.0.0-RC2] - 2026-05-31
### 新增
- `TransactionTemplate``SimpleJdbcTemplate` 中分离为独立类,封装事务生命周期管理(开启/提交/回滚/恢复自动提交)
- `SimpleJdbcTemplate.transaction()` 暴露 `TransactionTemplate` 实例
- 新增 `ParamBuilderTest` 单元测试
- 新增 `Instant` 类型参数端到端测试
### 重构
- 简化 `ParamBuilder` 参数构建逻辑
- 优化批量更新批次内索引计算逻辑
### 文档
- 完善 `SimpleJdbcTemplate` 类文档注释
- 更新批量更新相关类(`BatchUpdateResult`/`BatchUpdateStatus`/`BatchUpdateErrorInfo`)的 JavaDoc
- 更新 README 使用说明和示例
### 其他
- 版权年份由固定范围更新为包含当前时间的表述
---
## [1.0.0-RC1] - 2026-05-29
项目首个正式发布的候选版本2023-07 从 plusone-commons 独立开发)。
### 新增
**核心框架**
- `SimpleJdbcTemplate` 核心模板类,封装 JDBC 连接管理、异常处理和资源释放
- `JdbcOperations` 接口规范,定义统一的数据库操作 API
- `JdbcOperationSupport` 静态工具类,封装底层 `PreparedStatement` / `Statement` 操作
**查询**
- `query` + `ResultHandler`:自定义 `ResultSet` 处理
- `queryList`:返回列表(支持 `RowMapper``Class``Map<String, Object>` 三种变体)
- `queryFirst`:返回 `Optional<T>`(支持 `RowMapper``Class``Map<String, Object>` 三种变体)
- `queryBoolean`:返回 `boolean`,结果集为空时返回 `false`
- 全部查询方法提供无参数重载
**更新**
- `update`:执行 INSERT / UPDATE / DELETE返回影响行数
- `updateAndReturnKeys`:执行 DML 并返回自动生成的主键,支持 `RowMapper` 映射
- `batchUpdate`:分批执行 DML支持非静默模式遇错中断和静默模式遇错继续
**批量更新结果**
- `BatchUpdateResult`:封装批次级粒度的执行结果(总数据量、批次计数、成功/失败/剩余批次)
- `BatchUpdateStatus` 枚举SUCCESS / COMPLETED_WITH_ERRORS / INTERRUPTED
- `BatchUpdateErrorInfo`:封装错误批次的异常详情
**参数构建**
- `ParamBuilder.buildParams`:构建 `Object[]` 参数数组,支持 `Optional` / `OptionalInt` / `OptionalLong` / `OptionalDouble` 自动拆箱
- `ParamBuilder.buildBatchParams`:批量构建 `List<Object[]>` 参数列表
**映射策略**
- `RowMapper` 函数式接口:`ResultSet` 单行映射
- `RowMapper.HASH_MAP_MAPPER`:将行数据映射为 `Map<String, Object>`
- `DefaultBeanRowMapper`:默认 Bean 映射实现,自动匹配 属性名(小驼峰)↔ 列名(小写蛇形),通过反射调用 setter
- `RowMapper.beanRowMapper(Class)` / `beanRowMapper(Class, Map)` 静态工厂方法
**事务**
- `TransactionException`:包装事务执行中的原始异常
- `commitIfTrue` 方法根据业务逻辑返回值true 提交 / false 回滚)控制事务
**测试**
- 基于 H2 内存数据库的集成测试体系
- 覆盖查询 / 更新 / 批量 / 事务 / RowMapper 全场景
**许可**
- Apache License 2.0

24
NOTICE
View File

@@ -5,36 +5,20 @@ This product is licensed under the Apache License, Version 2.0.
You may obtain a copy of the License at:
https://www.apache.org/licenses/LICENSE-2.0
================================================================================
Third-Party Dependencies
================================================================================
1. plusone-commons (xyz.zhouxy.plusone:plusone-commons)
Copyright ZhouXY
Licensed under the Apache License, Version 2.0
2. Google Guava (com.google.guava:guava)
Copyright (C) The Guava Authors
Licensed under the Apache License, Version 2.0
3. JSR-305 Annotations (com.google.code.findbugs:jsr305)
Copyright (C) FindBugs
Licensed under the Apache License, Version 2.0
================================================================================
Test Dependencies (Not included in distribution)
================================================================================
4. JUnit Jupiter (org.junit.jupiter:junit-jupiter)
1. JUnit Jupiter (org.junit.jupiter:junit-jupiter)
Copyright 2015-2026 JUnit Team
Licensed under the Eclipse Public License 2.0
5. Logback (ch.qos.logback:logback-classic)
2. Logback (ch.qos.logback:logback-classic)
Copyright (C) 1999-2026, QOS.ch
Licensed under the Eclipse Public License 1.0
and GNU Lesser General Public License 2.1
6. H2 Database (com.h2database:h2)
3. H2 Database (com.h2database:h2)
Copyright 1999-2026 H2 Database Project
Licensed under the MPL 2.0 and EPL 1.0
@@ -42,6 +26,6 @@ Test Dependencies (Not included in distribution)
Notes
================================================================================
- This project is a lightweight JDBC wrapper designed for legacy projects
- This project is a lightweight JDBC wrapper designed for legacy projects
without ORM frameworks.
- For learning and reference purposes only.

248
README.md
View File

@@ -6,7 +6,7 @@
---
## 核心特性
## 1. 核心特性
- **轻量无依赖**:基于原生 JDBC 封装,无第三方重量级依赖。
- **API 简洁**:提供丰富的快捷方法,大幅减少样板代码。
@@ -16,13 +16,23 @@
---
## 📦 快速开始
## 2. 设计考量与边界
### 环境要求
`Simple JDBC` 不追求大而全,在功能设计上保持克制。以下是本项目的一些设计考量:
- 明确**不支持存储过程**:专注于基础 CRUD。
- **不提供分页 API**:不同数据库的分页方言差异巨大,且实际业务中的分页查询往往不是简单的 `LIMIT OFFSET`(存在如游标分页、延迟关联等深度优化空间)。为了保持轻量与灵活,分页 SQL 交由开发者根据具体数据库与业务场景自行编写。
- **不提供缓存支持**:数据缓存应当被视为一个独立的关注点,通常交由更高层的抽象模块来处理。
---
## 3. 快速开始
### 3.1 环境要求
- **JDK 8** 或更高版本
### 添加 Maven 依赖
### 3.2 添加 Maven 依赖
将以下配置添加到您的 `pom.xml` 中:
@@ -34,16 +44,18 @@
</dependency>
```
### 初始化
### 3.3 初始化
```java
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
```
### 1. 查询操作
> 💡 关于与数据库连接池(如 HikariCP、Druid、DBCP 2 等)的集成方式,请参见「[连接池集成](#8-连接池集成)」章节。
### 3.4 查询操作
```java
// 1.1 基础查询(使用 ResultHandler 处理全部结果)
// 基础查询(使用 ResultHandler 处理全部结果)
List<Account> accounts = jdbcTemplate.query(
"SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"),
@@ -63,21 +75,21 @@ List<Account> accounts = jdbcTemplate.query(
}
);
// 1.2 查询列表(单列)
// 查询列表(单列)
List<String> usernames = jdbcTemplate.queryList(
"SELECT username FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"),
String.class
);
// 1.3 查询列表(使用内置 Bean 映射)
// 查询列表(使用内置 Bean 映射)
List<Account> mappedAccounts = jdbcTemplate.queryList(
"SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"),
RowMapper.beanRowMapper(Account.class)
);
// 1.4 查询列表(使用自定义 RowMapper 映射)
// 查询列表(使用自定义 RowMapper 映射)
List<Account> customMappedAccounts = jdbcTemplate.queryList(
"SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"),
@@ -91,7 +103,7 @@ List<Account> customMappedAccounts = jdbcTemplate.queryList(
)
);
// 1.5 查询单行数据
// 查询单行数据
Optional<Account> account = jdbcTemplate.queryFirst(
"SELECT * FROM account WHERE deleted = 0 AND id = ?",
buildParams(10000L),
@@ -102,29 +114,31 @@ Optional<Account> account = jdbcTemplate.queryFirst(
)
);
// 1.6 查询 Boolean 值
// 查询 Boolean 值
boolean exists = jdbcTemplate.queryBoolean(
"SELECT EXISTS(SELECT 1 FROM account WHERE deleted = 0 AND id = ?)",
buildParams(10000L)
);
// 1.7 无参数 SQL 可直接省略 params 参数
// 无参数 SQL 可直接省略 params 参数
List<Account> allAccounts = jdbcTemplate.queryList(
"SELECT * FROM account WHERE deleted = 0",
RowMapper.beanRowMapper(Account.class)
);
```
### 2. 更新操作
> 📖 完整的方法列表与映射策略说明请参见「[4. 数据查询](#4-数据查询-query)」章节。
### 3.5 更新操作
```java
// 2.1 执行常规 DML
// 执行常规 DML
int affectedRows = jdbcTemplate.update(
"UPDATE account SET deleted = 1 WHERE id = ?",
buildParams(10000L)
);
// 2.2 执行 DML 并获取生成的主键
// 执行 DML 并获取生成的主键
// 注:按 JDBC 规范可获取自增 ID能否获取其他值取决于具体数据库及其 JDBC Driver 实现。
List<Pair<Long, LocalDateTime>> keys = jdbcTemplate.updateAndReturnKeys(
"INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)",
@@ -136,10 +150,12 @@ List<Pair<Long, LocalDateTime>> keys = jdbcTemplate.updateAndReturnKeys(
);
```
### 3. 批量更新操作
> 📖 完整的方法列表与批量更新结果说明请参见「[5. 数据更新](#5-数据更新-update)」章节。
### 3.6 批量更新操作
```java
// 3.1 默认模式:遇错即中断
// 默认模式:遇错即中断
BatchUpdateResult result = jdbcTemplate.batchUpdate(
"INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)",
buildBatchParams(accountList, account -> buildParams(
@@ -150,7 +166,7 @@ BatchUpdateResult result = jdbcTemplate.batchUpdate(
100 // 每 100 条数据为一个批次
);
// 3.2 静默模式:遇错不中断,全部执行完毕后统一检查
// 静默模式:遇错不中断,全部执行完毕后统一检查
BatchUpdateResult quietResult = jdbcTemplate.batchUpdate(
"INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)",
buildBatchParams(accountList, account -> buildParams(
@@ -162,7 +178,7 @@ BatchUpdateResult quietResult = jdbcTemplate.batchUpdate(
true // quietly = true遇错不中断
);
// 3.3 检查批量更新结果
// 检查批量更新结果
if (quietResult.getStatus() == BatchUpdateStatus.COMPLETED_WITH_ERRORS) {
for (int idx : quietResult.getErrorBatchIndexes()) {
BatchUpdateErrorInfo err = quietResult.getBatchUpdateErrorInfo(idx);
@@ -171,10 +187,12 @@ if (quietResult.getStatus() == BatchUpdateStatus.COMPLETED_WITH_ERRORS) {
}
```
### 4. 事务管理
> 📖 批量更新的详细结果说明请参见「[5.2 批量更新结果](#52-批量更新结果-batchupdateresult)」章节。
### 3.7 事务管理
```java
// 4.1 自动提交/回滚事务
// 自动提交/回滚事务
jdbcTemplate.transaction().execute(jdbc -> {
...
jdbc.update(...);
@@ -184,7 +202,7 @@ jdbcTemplate.transaction().execute(jdbc -> {
// 内部无异常抛出则自动提交,抛出异常则自动回滚
});
// 4.2 根据返回值控制事务
// 根据返回值控制事务
jdbcTemplate.transaction().commitIfTrue(jdbc -> {
...
jdbc.update(...);
@@ -206,44 +224,13 @@ jdbcTemplate.transaction().commitIfTrue(jdbc -> {
});
```
---
## 🛠️ 参数构建
为避免与数组产生歧义并规范 API 设计,`JdbcOperations` 中的所有方法均不使用可变长参数Varargs而是统一使用 `Object[]` 作为参数传递。您可以使用内置的 `ParamBuilder` 快速构建参数。
### 1. 构建单条参数列表
使用 `ParamBuilder.buildParams(...)` 构建 `Object[]`。该方法会自动将 `Optional` 值进行拆箱处理。
```java
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
buildParams("admin%", "0000"); // 返回 Object[]{"admin%", "0000"}
buildParams(Optional.of("hello")); // 返回 Object[]{"hello"}
buildParams(Optional.empty()); // 返回 Object[]{null}
```
### 2. 批量构建参数列表
使用 `ParamBuilder.buildBatchParams(collection, func)` 将集合中的每个元素转换为 `Object[]`,最终返回 `List<Object[]>`
```java
import static xyz.zhouxy.jdbc.ParamBuilder.buildBatchParams;
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
List<Object[]> batchParams = buildBatchParams(accountList, account -> buildParams(
account.getUsername(),
account.getPassword(),
account.getOrgNo()
));
```
> 📖 事务方法的详细说明请参见「[6. 事务管理](#6-事务管理-transaction)」章节。
---
## 🔍 数据查询 (Query)
## 4. 数据查询 (Query)
### 查询方法列表
### 4.1 查询方法列表
| 方法签名 | 说明 |
| :--- | :--- |
@@ -258,7 +245,7 @@ List<Object[]> batchParams = buildBatchParams(accountList, account -> buildParam
*💡 提示:以上方法均有省略 `params` 的重载(如 `queryList(sql, rowMapper)`),适用于不含占位符的 SQL 语句。*
### 结果映射策略
### 4.2 结果映射策略
- **`ResultHandler`**:处理完整的 `ResultSet`,允许自定义逻辑将结果集映射为任意类型(包括集合)。
- **`RowMapper`**:将 `ResultSet` 中的单行数据映射为 Java 对象。内置以下默认实现:
@@ -269,11 +256,11 @@ List<Object[]> batchParams = buildBatchParams(accountList, account -> buildParam
---
## ✏️ 数据更新 (Update)
## 5. 数据更新 (Update)
所有更新方法同样提供了无参重载。
### 更新方法列表
### 5.1 更新方法列表
| 方法签名 | 说明 |
| :--- | :--- |
@@ -282,7 +269,7 @@ List<Object[]> batchParams = buildBatchParams(accountList, account -> buildParam
| `batchUpdate(sql, params, batchSize)` | 分批执行 DML遇到错误立即中断。 |
| `batchUpdate(sql, params, batchSize, quietly)` | 分批执行 DML`quietly=true`,则遇到错误不中断,直至全部执行完毕。 |
### 批量更新结果 (`BatchUpdateResult`)
### 5.2 批量更新结果 (BatchUpdateResult)
`batchUpdate` 方法返回 `BatchUpdateResult` 对象,包含以下信息:
@@ -300,16 +287,151 @@ List<Object[]> batchParams = buildBatchParams(accountList, account -> buildParam
---
## 🔄 事务管理 (Transaction)
## 6. 事务管理 (Transaction)
通过 `TransactionTemplate` 管理事务,可直接实例化或通过 `SimpleJdbcTemplate.transaction()` 获取。
事务模板的核心机制如下:
- **共享连接**:事务内的所有 JDBC 操作共享同一个数据库连接,确保操作在同一事务上下文中执行。
- **自动提交管理**:进入事务时关闭连接的自动提交模式,事务结束后恢复原始状态。
- **异常处理**:操作中抛出异常时自动执行回滚,并将原始异常包装为 `TransactionException` 抛出。
### 6.1 获取事务模板
```java
// 方式一:通过 SimpleJdbcTemplate 获取
TransactionTemplate tx = jdbcTemplate.transaction();
// 方式二:直接实例化
TransactionTemplate tx = new TransactionTemplate(dataSource);
```
### 6.2 事务方法
- **`execute(consumer)`**:执行事务。传入 `ThrowingConsumer<JdbcOperations>`,若内部代码无异常抛出则自动提交,发生异常则回滚。
- **`commitIfTrue(predicate)`**:执行事务。传入 `ThrowingPredicate<JdbcOperations>`,根据返回值决定事务走向:返回 `true` 提交,返回 `false` 或抛出异常则回滚。
---
## ⚠️ 注意事项与适用场景
## 7. 参数构建
为避免与数组产生歧义并规范 API 设计,`JdbcOperations` 中的所有方法均不使用可变长参数Varargs而是统一使用 `Object[]` 作为参数传递。您可以使用内置的 `ParamBuilder` 快速构建参数。
### 7.1 构建单条参数列表
使用 `ParamBuilder.buildParams(...)` 构建 `Object[]`。该方法会自动将 `Optional` 值进行拆箱处理。
```java
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
buildParams("admin%", "0000"); // 返回 Object[]{"admin%", "0000"}
buildParams(Optional.of("hello")); // 返回 Object[]{"hello"}
buildParams(Optional.empty()); // 返回 Object[]{null}
```
### 7.2 批量构建参数列表
使用 `ParamBuilder.buildBatchParams(collection, func)` 将集合中的每个元素转换为 `Object[]`,最终返回 `List<Object[]>`
```java
import static xyz.zhouxy.jdbc.ParamBuilder.buildBatchParams;
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
List<Object[]> batchParams = buildBatchParams(accountList, account -> buildParams(
account.getUsername(),
account.getPassword(),
account.getOrgNo()
));
```
---
## 8. 连接池集成
`SimpleJdbcTemplate` 仅依赖标准的 `javax.sql.DataSource` 接口,可与任何主流数据库连接池集成。以下为常用连接池的配置示例。
### 8.1 使用 HikariCP
> [HikariCP](https://github.com/brettwooldridge/HikariCP) 是目前性能最优的数据库连接池之一,推荐在新项目中优先使用。
```xml
<!-- Maven 依赖 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
```
```java
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
// 配置 HikariCP 连接池
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/your_database");
config.setUsername("your_username");
config.setPassword("your_password");
// 其他连接池参数按需配置
DataSource dataSource = new HikariDataSource(config);
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
```
### 8.2 使用 Druid
> [Druid](https://github.com/alibaba/druid) 是阿里巴巴开源的数据库连接池,提供了内置的监控和扩展能力,在中文社区中广泛采用。
```xml
<!-- Maven 依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
```
```java
import com.alibaba.druid.pool.DruidDataSource;
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/your_database");
dataSource.setUsername("your_username");
dataSource.setPassword("your_password");
// 其他连接池参数按需配置
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
```
### 8.3 使用 DBCP 2
> [DBCP 2](https://commons.apache.org/proper/commons-dbcp/) 是 Apache Commons 提供的数据库连接池,适用于需要依赖轻量的场景。
```xml
<!-- Maven 依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>${dbcp2.version}</version>
</dependency>
```
```java
import org.apache.commons.dbcp2.BasicDataSource;
BasicDataSource dataSource = new BasicDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/your_database");
dataSource.setUsername("your_username");
dataSource.setPassword("your_password");
// 其他连接池参数按需配置
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
```
---
## 9. 注意事项与适用场景
1. **风险提示**:本项目定位为轻量级工具,相较于成熟的 ORM 框架(如 MyBatis、Hibernate其功能覆盖面和生态相对有限。**在生产环境使用前,请务必进行充分的测试,使用风险自行承担。**
2. **线程安全**`SimpleJdbcTemplate` 本身无内部状态,是**线程安全**的。但请确保其底层依赖的 `DataSource`(如 HikariCP、Druid 等连接池)已正确配置并保证线程安全。

30
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>xyz.zhouxy.jdbc</groupId>
<artifactId>simple-jdbc</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.0.0</version>
<name>Simple JDBC</name>
<description>对 JDBC 的简单封装。</description>
@@ -16,7 +16,11 @@
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plusone-commons.version>1.1.0-RC2</plusone-commons.version>
<!-- Dependency Versions -->
<jsr305.version>3.0.2</jsr305.version>
<junit-jupiter.version>5.14.4</junit-jupiter.version>
<logback.version>1.3.16</logback.version>
<h2.version>2.2.224</h2.version>
</properties>
<licenses>
@@ -40,43 +44,35 @@
<url>https://gitea.zhouxy.xyz/plusone/simple-jdbc</url>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-dependencies</artifactId>
<version>${plusone-commons.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-commons</artifactId>
<version>${plusone-commons.version}</version>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>${jsr305.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>release</id>

View File

@@ -51,6 +51,11 @@ public class BatchUpdateResult {
*/
private final int batchSize;
/**
* 是否静默模式
*/
private final boolean quietly;
/**
* 本次分批更新的状态
*/
@@ -75,10 +80,11 @@ public class BatchUpdateResult {
*/
private int completeBatchCount;
BatchUpdateResult(int total, int batchCount, int batchSize) {
BatchUpdateResult(int total, int batchCount, int batchSize, boolean quietly) {
this.total = total;
this.batchCount = batchCount;
this.batchSize = batchSize;
this.quietly = quietly;
this.allUpdateCounts = new HashMap<>(batchCount);
this.allErrorsInfo = new HashMap<>(batchCount);
@@ -101,17 +107,15 @@ public class BatchUpdateResult {
this.allUpdateCounts.put(batchIndex, updateCounts);
this.allErrorsInfo.put(batchIndex, new BatchUpdateErrorInfo(batchIndex, cause));
if (this.status == BatchUpdateStatus.SUCCESS) {
this.status = BatchUpdateStatus.COMPLETED_WITH_ERRORS;
if (this.quietly) {
this.status = BatchUpdateStatus.COMPLETED_WITH_ERRORS;
}
else {
this.status = BatchUpdateStatus.INTERRUPTED;
}
}
}
/**
* 中断
*/
void interrupt() {
this.status = BatchUpdateStatus.INTERRUPTED;
}
/**
* 获取指定批次更新结果
*

View File

@@ -15,8 +15,6 @@
*/
package xyz.zhouxy.jdbc;
import xyz.zhouxy.plusone.commons.base.IWithIntCode;
/**
* 批量更新状态
*
@@ -27,7 +25,7 @@ import xyz.zhouxy.plusone.commons.base.IWithIntCode;
* @see BatchUpdateResult
* @see BatchUpdateResult#getStatus()
*/
public enum BatchUpdateStatus implements IWithIntCode {
public enum BatchUpdateStatus {
/**
* 成功
@@ -62,9 +60,10 @@ public enum BatchUpdateStatus implements IWithIntCode {
}
/**
* {@inheritDoc}
* 获取状态码
*
* @return 状态码
*/
@Override
public int getCode() {
return code;
}

View File

@@ -34,28 +34,26 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.base.CaseFormat;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
import xyz.zhouxy.jdbc.util.NamingTools;
/**
* DefaultBeanRowMapper
*
* <p>
* 将 {@link ResultSet} 转换为 Java Bean 的 {@link RowMapper} 的基础实现。
* </p>
* <i>仅在对性能不敏感的场景下使用。</i>
*
* <p>
* 说明:
* <ul>
* <li>使用反射获取类型信息,也是使用反射调用无参构造器和 {@code setter} 方法。</li>
* <li>{@code propertyColMap} 未指定的列名和属性名的映射时,默认 JavaBean 的属性名为小驼峰,列名为小写蛇形命名。</li>
* <li>从{@link ResultSet} 中获取属性值时,使用 {@link ResultSet#getObject(String, Class)} 获取。</li>
* <li>使用 {@link ResultSet#getObject(String, Class)} 从 {@link ResultSet} 中获取属性值。</li>
* <li>JavaBean 属性仅支持引用类型,不支持基本数据类型。</li>
* <li>实际使用中还是建议针对目标类型自定义 {@link RowMapper}。</li>
* <li><b>实际使用中建议针对目标类型自定义 {@link RowMapper}。</b></li>
* </ul>
* @author ZhouXY
*
* @author ZhouXY
* @since 1.0.0
*/
public class DefaultBeanRowMapper<T> implements RowMapper<T> {
@@ -90,7 +88,6 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
* @return DefaultBeanRowMapper 对象
* @throws SQLException 创建 {@code DefaultBeanRowMapper} 出现错误的异常时抛出
*/
@StaticFactoryMethod(DefaultBeanRowMapper.class)
public static <T> DefaultBeanRowMapper<T> of(Class<T> beanType) throws SQLException {
return of(beanType, null);
}
@@ -104,7 +101,6 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
* @return {@code DefaultBeanRowMapper} 对象
* @throws SQLException 创建 {@code DefaultBeanRowMapper} 出现错误的异常时抛出
*/
@StaticFactoryMethod(DefaultBeanRowMapper.class)
public static <T> DefaultBeanRowMapper<T> of(Class<T> beanType, @Nullable Map<String, String> propertyColMap)
throws SQLException {
try {
@@ -167,14 +163,14 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
// Bean 的属性名为小驼峰,对应的列名为下划线
Function<? super PropertyDescriptor, String> keyMapper;
if (propertyColMap == null || propertyColMap.isEmpty()) {
keyMapper = p -> CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, p.getName());
keyMapper = p -> NamingTools.camelToSnake(p.getName());
}
else {
keyMapper = p -> {
String propertyName = p.getName();
String colName = propertyColMap.get(propertyName);
return colName != null ? colName
: CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, propertyName);
: NamingTools.camelToSnake(propertyName);
};
}
return Arrays.stream(propertyDescriptors)

View File

@@ -16,8 +16,8 @@
package xyz.zhouxy.jdbc;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import static xyz.zhouxy.jdbc.util.AssertTools.checkArgument;
import static xyz.zhouxy.jdbc.util.AssertTools.checkArgumentNotNull;
import java.sql.BatchUpdateException;
import java.sql.Connection;
@@ -37,8 +37,6 @@ import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.util.ArrayTools;
/**
* JdbcOperationSupport
*
@@ -162,7 +160,7 @@ class JdbcOperationSupport {
throws SQLException {
assertConnectionNotNull(conn);
assertSqlNotNull(sql);
if (ArrayTools.isNotEmpty(params)) {
if (params != null && params.length > 0) {
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
fillStatement(stmt, params);
return stmt.executeUpdate();
@@ -191,7 +189,7 @@ class JdbcOperationSupport {
assertConnectionNotNull(conn);
assertSqlNotNull(sql);
assertRowMapperNotNull(rowMapper);
if (ArrayTools.isNotEmpty(params)) {
if (params != null && params.length > 0) {
try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
fillStatement(stmt, params);
stmt.executeUpdate();
@@ -234,13 +232,13 @@ class JdbcOperationSupport {
assertSqlNotNull(sql);
checkArgument(batchSize > 0, "The batch size must be greater than 0.");
if (params == null || params.isEmpty()) {
return new BatchUpdateResult(0, 0, batchSize);
return new BatchUpdateResult(0, 0, batchSize, quietly);
}
final int paramsSize = params.size();
final int batchCount = (paramsSize + batchSize - 1) / batchSize;
final BatchUpdateResult result = new BatchUpdateResult(paramsSize, batchCount, batchSize);
final BatchUpdateResult result = new BatchUpdateResult(paramsSize, batchCount, batchSize, quietly);
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
// 表示第几条数据1, 2, 3, ..., paramsSize
@@ -265,7 +263,6 @@ class JdbcOperationSupport {
final int[] updateCounts = getUpdateCountsOnError(indexInBatch, e);
result.recordErrorBatch(batchIndex, updateCounts, e);
if (!quietly) {
result.interrupt();
break;
}
}
@@ -308,7 +305,7 @@ class JdbcOperationSupport {
@Nullable Object[] params,
@Nonnull ResultHandler<T> resultHandler)
throws SQLException {
if (ArrayTools.isNotEmpty(params)) {
if (params != null && params.length > 0) {
try (PreparedStatement stmt = createPreparedStatementInternal(conn, sql, params);
ResultSet rs = stmt.executeQuery()) {
return resultHandler.handle(rs);

View File

@@ -29,10 +29,7 @@ import java.util.OptionalLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import xyz.zhouxy.plusone.commons.collection.CollectionTools;
import xyz.zhouxy.plusone.commons.util.ArrayTools;
import xyz.zhouxy.plusone.commons.util.AssertTools;
import xyz.zhouxy.plusone.commons.util.OptionalTools;
import xyz.zhouxy.jdbc.util.AssertTools;
/**
* ParamBuilder
@@ -45,10 +42,28 @@ import xyz.zhouxy.plusone.commons.util.OptionalTools;
* @since 1.0.0
*/
public class ParamBuilder {
/**
* 空参数数组常量
*
* <p>
* 用于表示无参数的 SQL 操作
*/
public static final Object[] EMPTY_OBJECT_ARRAY = {};
/**
* 构建 SQL 参数数组
*
* <p>
* 将传入的参数转换为 {@code Object[]},用于 {@link PreparedStatement} 的参数填充。
* 支持自动拆箱 {@link Optional}、{@link OptionalInt}、{@link OptionalLong}、{@link OptionalDouble}。
* 对于 {@link CharSequence}、{@link Number}、{@link Boolean}、{@link Temporal} 类型不做转换直接透传。
* 如果传入的 {@code params} 为 {@code null} 或空,则返回 {@link #EMPTY_OBJECT_ARRAY}。
*
* @param params SQL 参数列表(可变参数)
* @return 参数数组
*/
public static Object[] buildParams(final Object... params) {
if (ArrayTools.isEmpty(params)) {
if (params == null || params.length == 0) {
return EMPTY_OBJECT_ARRAY;
}
return Arrays.stream(params)
@@ -73,24 +88,37 @@ public class ParamBuilder {
return param;
}
if (param instanceof Optional) {
return OptionalTools.orElseNull((Optional<?>) param);
return ((Optional<?>) param).orElse(null);
}
if (param instanceof OptionalInt) {
return OptionalTools.toInteger((OptionalInt) param);
return ((OptionalInt) param).isPresent() ? ((OptionalInt) param).getAsInt() : null;
}
if (param instanceof OptionalLong) {
return OptionalTools.toLong((OptionalLong) param);
return ((OptionalLong) param).isPresent() ? ((OptionalLong) param).getAsLong() : null;
}
if (param instanceof OptionalDouble) {
return OptionalTools.toDouble((OptionalDouble) param);
return ((OptionalDouble) param).isPresent() ? ((OptionalDouble) param).getAsDouble() : null;
}
return param;
}
/**
* 批量构建参数列表
*
* <p>
* 将集合中的每个元素通过 {@code func} 映射为 {@code Object[]}
* 最终返回 {@code List<Object[]>},用于 {@code batchUpdate} 批量操作。
*
* @param <T> 集合元素类型
* @param c 待转换的集合
* @param func 转换函数,将集合元素转换为参数数组
* @return 参数数组列表
* @throws NullPointerException 如果 {@code c} 或 {@code func} 为 {@code null}
*/
public static <T> List<Object[]> buildBatchParams(final Collection<T> c, final Function<T, Object[]> func) {
AssertTools.checkNotNull(c, "The collection can not be null.");
AssertTools.checkNotNull(func, "The func can not be null.");
if (CollectionTools.isEmpty(c)) {
if (c.isEmpty()) {
return Collections.emptyList();
}
return c.stream().map(func).collect(Collectors.toList());

View File

@@ -26,7 +26,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.sql.DataSource;
import xyz.zhouxy.plusone.commons.util.AssertTools;
import xyz.zhouxy.jdbc.util.AssertTools;
/**
* JDBC 操作的模板类,对原生 JDBC 进行轻量封装,提供查询、更新、批量操作等便捷方法。
@@ -59,6 +59,11 @@ public class SimpleJdbcTemplate implements JdbcOperations {
@Nonnull
private final TransactionTemplate transactionTemplate;
/**
* 构造一个 {@code SimpleJdbcTemplate} 实例
*
* @param dataSource 数据源,用于获取数据库连接;不可为 {@code null}
*/
public SimpleJdbcTemplate(@Nonnull DataSource dataSource) {
AssertTools.checkNotNull(dataSource);
this.dataSource = dataSource;
@@ -199,6 +204,14 @@ public class SimpleJdbcTemplate implements JdbcOperations {
// #region - transaction
/**
* 获取事务模板
*
* <p>
* 返回的 {@link TransactionTemplate} 与当前模板共享同一个 {@link DataSource}。
*
* @return 事务模板
*/
public TransactionTemplate transaction() {
return this.transactionTemplate;
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2026-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc;
/**
* 可抛出受检异常的函数式接口。
*
* <p>
* 类似于 {@link java.util.function.Consumer},但 {@code accept} 方法允许抛出受检异常。
* </p>
*
* @param <T> 输入类型
* @param <E> 允许抛出的异常类型
* @author ZhouXY
* @since 1.1.0
*/
@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
/**
* 对给定参数执行此操作。
*
* @param t 输入参数
* @throws E 异常
*/
void accept(T t) throws E;
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2026-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc;
/**
* 可抛出受检异常的谓词函数式接口。
*
* <p>
* 类似于 {@link java.util.function.Predicate},但 {@code test} 方法允许抛出受检异常。
* </p>
*
* @param <T> 输入类型
* @param <E> 允许抛出的异常类型
* @author ZhouXY
* @since 1.1.0
*/
@FunctionalInterface
public interface ThrowingPredicate<T, E extends Exception> {
/**
* 对给定参数执行此谓词判断。
*
* @param t 输入参数
* @return 谓词判断结果
* @throws E 异常
*/
boolean test(T t) throws E;
}

View File

@@ -26,9 +26,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.sql.DataSource;
import xyz.zhouxy.plusone.commons.function.ThrowingConsumer;
import xyz.zhouxy.plusone.commons.function.ThrowingPredicate;
import xyz.zhouxy.plusone.commons.util.AssertTools;
import xyz.zhouxy.jdbc.util.AssertTools;
/**
* 事务模板,提供事务执行能力。
@@ -64,7 +62,12 @@ public class TransactionTemplate {
@Nonnull
private final DataSource dataSource;
public TransactionTemplate(@Nonnull DataSource dataSource) {
/**
* 构造一个 {@code TransactionTemplate} 实例
*
* @param dataSource 数据源,用于获取数据库连接;不可为 {@code null}
*/
public TransactionTemplate(DataSource dataSource) {
AssertTools.checkNotNull(dataSource);
this.dataSource = dataSource;
}

View File

@@ -0,0 +1,334 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc.util;
import java.util.function.Supplier;
/**
* 断言工具
*
* <p>
* 本工具类不封装过多判断逻辑,鼓励充分使用项目中的工具类进行逻辑判断。
*
* <pre>
* checkArgument(StringUtils.hasText(str), "The argument cannot be blank.");
* checkState(ArrayUtils.isNotEmpty(result), "The result cannot be empty.");
* checkCondition(!CollectionUtils.isEmpty(roles),
* () -&gt; new InvalidInputException("The roles cannot be empty."));
* checkCondition(RegexTools.matches(email, PatternConsts.EMAIL),
* "must be a well-formed email address");
* </pre>
*
* @author ZhouXY
*/
public class AssertTools {
// ================================
// #region - Argument
// ================================
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition) {
if (!condition) {
throw new IllegalArgumentException();
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessage 异常信息
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition, String errorMessage) {
if (!condition) {
throw new IllegalArgumentException(errorMessage);
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessageSupplier 异常信息
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition, Supplier<String> errorMessageSupplier) {
if (!condition) {
throw new IllegalArgumentException(errorMessageSupplier.get());
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition,
String errorMessageTemplate, Object... errorMessageArgs) {
if (!condition) {
throw new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - Argument
// ================================
// ================================
// #region - ArgumentNotNull
// ================================
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(T obj) {
if (obj == null) {
throw new IllegalArgumentException();
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessage 异常信息
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(T obj, String errorMessage) {
if (obj == null) {
throw new IllegalArgumentException(errorMessage);
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageSupplier 异常信息
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(T obj, Supplier<String> errorMessageSupplier) {
if (obj == null) {
throw new IllegalArgumentException(errorMessageSupplier.get());
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(T obj,
String errorMessageTemplate, Object... errorMessageArgs) {
if (obj == null) {
throw new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs));
}
return obj;
}
// ================================
// #endregion - ArgumentNotNull
// ================================
// ================================
// #region - State
// ================================
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition) {
if (!condition) {
throw new IllegalStateException();
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessage 异常信息
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition, String errorMessage) {
if (!condition) {
throw new IllegalStateException(errorMessage);
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessageSupplier 异常信息
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition, Supplier<String> errorMessageSupplier) {
if (!condition) {
throw new IllegalStateException(errorMessageSupplier.get());
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition,
String errorMessageTemplate, Object... errorMessageArgs) {
if (!condition) {
throw new IllegalStateException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - State
// ================================
// ================================
// #region - NotNull
// ================================
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(T obj) {
if (obj == null) {
throw new NullPointerException();
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessage 异常信息
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(T obj, String errorMessage) {
if (obj == null) {
throw new NullPointerException(errorMessage);
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageSupplier 异常信息
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(T obj, Supplier<String> errorMessageSupplier) {
if (obj == null) {
throw new NullPointerException(errorMessageSupplier.get());
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(T obj,
String errorMessageTemplate, Object... errorMessageArgs) {
if (obj == null) {
throw new NullPointerException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - NotNull
// ================================
// ================================
// #region - Condition
// ================================
/**
* 当条件不满足时抛出异常。
*
* @param <T> 异常类型
* @param condition 条件
* @param e 异常
* @throws T 当条件不满足时抛出异常
*/
public static <T extends Exception> void checkCondition(boolean condition, Supplier<T> e)
throws T {
if (!condition) {
throw e.get();
}
}
// ================================
// #endregion
// ================================
// ================================
// #region - constructor
// ================================
private AssertTools() {
throw new IllegalStateException("Utility class");
}
// ================================
// #endregion
// ================================
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2026-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc.util;
/**
* 字符串工具
*
* @author ZhouXY
* @since 1.0.0
*/
public class NamingTools {
/**
* 将小驼峰命名转换为小写下划线命名snake_case
*
* <p>转换规则:
* <ul>
* <li>小写→大写边界插入下划线:{@code userName → user_name}</li>
* <li>连续大写缩写视为整体,在其末尾小写边界插入下划线:{@code XMLParser → xml_parser}</li>
* <li>纯小写保持不变:{@code username → username}</li>
* <li>{@code null} 或空字符串返回原值</li>
* </ul>
*
* @param camelCase 小驼峰命名字符串,可空
* @return snake_case 命名字符串;{@code null} 输入返回 {@code null}
*/
public static String camelToSnake(String camelCase) {
if (camelCase == null || camelCase.isEmpty()) {
return camelCase;
}
StringBuilder sb = new StringBuilder(camelCase.length() * 2);
int len = camelCase.length();
for (int i = 0; i < len; i++) {
char c = camelCase.charAt(i);
if (isUpperCaseAscii(c)) {
if (shouldInsertUnderscore(camelCase, i)) {
sb.append('_');
}
sb.append((char) (c + 32)); // 转小写
} else {
sb.append(c);
}
}
return sb.toString();
}
private static boolean shouldInsertUnderscore(String str, int index) {
if (index == 0) {
return false;
}
char prev = str.charAt(index - 1);
char next = (index + 1 < str.length()) ? str.charAt(index + 1) : 0;
boolean prevIsBoundary = !isUpperCaseAscii(prev);
boolean nextIsLower = isLowerCaseAscii(next);
return prevIsBoundary || nextIsLower;
}
private static boolean isUpperCaseAscii(char c) {
return c >= 'A' && c <= 'Z';
}
private static boolean isLowerCaseAscii(char c) {
return c >= 'a' && c <= 'z';
}
// ================================
// #region - constructor
// ================================
private NamingTools() {
throw new IllegalStateException("Utility class");
}
// ================================
// #endregion
// ================================
}

View File

@@ -6,6 +6,7 @@ import static xyz.zhouxy.jdbc.ParamBuilder.*;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -16,8 +17,6 @@ import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import xyz.zhouxy.jdbc.BatchUpdateErrorInfo;
import xyz.zhouxy.jdbc.BatchUpdateResult;
import xyz.zhouxy.jdbc.BatchUpdateStatus;
@@ -170,7 +169,7 @@ class BatchUpdateTest extends BaseH2Test {
// #region - 包含错误数据
// ================================
final List<User> userListContainingInvalidData = Lists.newArrayList(
final List<User> userListContainingInvalidData = Arrays.asList(
// batch 0
new User("test_0001", "test_0001@example.com", 1, 1L, true),
new User("test_0002", "test_0002@example.com", 1, 1L, true),

View File

@@ -10,6 +10,9 @@ import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.function.Function;
import org.junit.jupiter.api.DisplayName;
@@ -160,6 +163,29 @@ class ParamBuilderTest {
// #endregion
// ====================================================================
// ====================================================================
// #region - buildParamsTemporal 时间类型
// --------------------------------------------------------------------
@Test
@DisplayName("buildParamsTemporal 类型LocalDate / LocalTime / LocalDateTime")
void testBuildParamsTemporal() {
LocalDate date = LocalDate.of(2024, 6, 15);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime dateTime = LocalDateTime.of(date, time);
Object[] result = buildParams(date, time, dateTime);
assertEquals(3, result.length);
assertSame(date, result[0]);
assertSame(time, result[1]);
assertSame(dateTime, result[2]);
}
// --------------------------------------------------------------------
// #endregion
// ====================================================================
// ====================================================================
// #region - buildBatchParams
// --------------------------------------------------------------------

View File

@@ -157,6 +157,63 @@ class RowMapperTest extends BaseH2Test {
assertEquals("alice", user.get().getUsername());
}
// ==================== DefaultBeanRowMapper 连续大写缩写映射 ====================
@Test
@DisplayName("DefaultBeanRowMapper连续大写缩写属性正确映射为 snake_case")
void testDefaultBeanRowMapperAcronymMapping() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
// 创建测试表,列名使用 snake_case
template.update("CREATE TABLE acronym_test ("
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY,"
+ "home_url VARCHAR(100),"
+ "xml_parser VARCHAR(100),"
+ "parse_url VARCHAR(100),"
+ "user_id VARCHAR(100),"
+ "parse_html VARCHAR(100),"
+ "multi_http_client VARCHAR(100))");
template.update(
"INSERT INTO acronym_test (home_url, xml_parser, parse_url, user_id, parse_html, multi_http_client)"
+ " VALUES (?, ?, ?, ?, ?, ?)",
new Object[]{"https://example.com", "SAXParser", "/api/v1",
"user-001", "<div>test</div>", "ApacheHttpClient"});
RowMapper<AcronymBean> rowMapper = RowMapper.beanRowMapper(AcronymBean.class);
Optional<AcronymBean> result = template.queryFirst(
"SELECT * FROM acronym_test WHERE id = ?",
new Object[]{1L}, rowMapper);
assertTrue(result.isPresent());
AcronymBean bean = result.get();
assertEquals("https://example.com", bean.getHomeURL());
assertEquals("SAXParser", bean.getXmlParser());
assertEquals("/api/v1", bean.getParseURL());
assertEquals("user-001", bean.getUserID());
assertEquals("<div>test</div>", bean.getParseHTML());
assertEquals("ApacheHttpClient", bean.getMultiHttpClient());
logger.info("缩写映射: homeURL={}, xmlParser={}, parseURL={}, userID={}, parseHTML={}, multiHttpClient={}",
bean.getHomeURL(), bean.getXmlParser(), bean.getParseURL(),
bean.getUserID(), bean.getParseHTML(), bean.getMultiHttpClient());
}
@Test
@DisplayName("DefaultBeanRowMapper纯小写属性名映射为同名列")
void testDefaultBeanRowMapperAllLowercaseMapping() throws SQLException {
// 通过 User Bean 验证纯小写属性映射username → username, email → email
SimpleJdbcTemplate template = createTemplate();
RowMapper<User> rowMapper = RowMapper.beanRowMapper(User.class);
Optional<User> user = template.queryFirst(
"SELECT username, email FROM users WHERE username = ?",
new Object[]{"alice"}, rowMapper);
assertTrue(user.isPresent());
assertEquals("alice", user.get().getUsername());
assertEquals("alice@example.com", user.get().getEmail());
}
// ==================== HASH_MAP_MAPPER ====================
@Test
@@ -253,4 +310,74 @@ class RowMapperTest extends BaseH2Test {
this.name = name;
}
}
/**
* 包含连续大写缩写属性的 Bean用于验证 camelToSnake 的缩写处理。
*
* <p>覆盖场景:
* <ul>
* <li>homeURL — 缩写在末尾(三字母 URL</li>
* <li>xmlParser — 缩写在前(三字母 XML</li>
* <li>parseURL — 缩写在末尾</li>
* <li>userID — 两字母缩写在末尾ID</li>
* <li>parseHTML — 四字母缩写在末尾HTML</li>
* <li>multiHttpClient — 缩写夹在词中HTTP</li>
* </ul>
*/
public static class AcronymBean {
private String homeURL;
private String xmlParser;
private String parseURL;
private String userID;
private String parseHTML;
private String multiHttpClient;
public String getHomeURL() {
return homeURL;
}
public void setHomeURL(String homeURL) {
this.homeURL = homeURL;
}
public String getXmlParser() {
return xmlParser;
}
public void setXmlParser(String xmlParser) {
this.xmlParser = xmlParser;
}
public String getParseURL() {
return parseURL;
}
public void setParseURL(String parseURL) {
this.parseURL = parseURL;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getParseHTML() {
return parseHTML;
}
public void setParseHTML(String parseHTML) {
this.parseHTML = parseHTML;
}
public String getMultiHttpClient() {
return multiHttpClient;
}
public void setMultiHttpClient(String multiHttpClient) {
this.multiHttpClient = multiHttpClient;
}
}
}

View File

@@ -231,4 +231,32 @@ class TransactionTest extends BaseH2Test {
assertThrows(Exception.class, () ->
template.transaction().execute(null));
}
// ==================== TransactionException ====================
@Test
@DisplayName("TransactionException单参构造器仅 cause")
void testTransactionExceptionSingleArg() {
RuntimeException cause = new RuntimeException("原始异常");
TransactionException ex = new TransactionException(cause);
assertEquals("Transaction failed during execution", ex.getMessage());
assertSame(cause, ex.getCause());
}
@Test
@DisplayName("TransactionException双参构造器")
void testTransactionExceptionWithMessage() {
RuntimeException cause = new RuntimeException("原始异常");
TransactionException ex = new TransactionException("自定义消息", cause);
assertEquals("自定义消息", ex.getMessage());
assertSame(cause, ex.getCause());
}
@Test
@DisplayName("TransactionExceptionnull cause")
void testTransactionExceptionNullCause() {
TransactionException ex = new TransactionException(null);
assertEquals("Transaction failed during execution", ex.getMessage());
assertNull(ex.getCause());
}
}

View File

@@ -0,0 +1,522 @@
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc.test.util;
import static org.junit.jupiter.api.Assertions.*;
import static xyz.zhouxy.jdbc.util.AssertTools.*;
import java.lang.reflect.Constructor;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import xyz.zhouxy.jdbc.util.AssertTools;
class AssertToolsTests {
// #region - Argument
@Test
void testCheckArgument_true() {
checkArgument(true);
}
@Test
void testCheckArgument_true_withMessage() {
final String IGNORE_ME = "IGNORE_ME"; // NOSONAR
checkArgument(true, IGNORE_ME);
}
@Test
void testCheckArgument_true_withNullMessage() {
final String IGNORE_ME = null; // NOSONAR
checkArgument(true, IGNORE_ME);
}
@Test
void testCheckArgument_true_withMessageSupplier() {
checkArgument(true, () -> "Error message: " + LocalDate.now());
}
@Test
void testCheckArgument_true_withNullMessageSupplier() {
final Supplier<String> IGNORE_ME = null; // NOSONAR
checkArgument(true, IGNORE_ME);
}
@Test
void testCheckArgument_true_withMessageFormat() {
LocalDate today = LocalDate.now();
checkArgument(true, "String format: %s", today);
}
@Test
void testCheckArgument_true_withNullMessageFormat() {
checkArgument(true, null, LocalDate.now());
}
@Test
void testCheckArgument_false() {
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgument_false_withMessage() {
final String message = "testCheckArgument_false_withMessage";
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false, message));
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgument_false_withNullMessage() {
final String message = null;
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false, message));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgument_false_withMessageSupplier() {
final LocalDate today = LocalDate.now();
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false, () -> "Error message: " + today));
assertEquals("Error message: " + today, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgument_false_withNullMessageSupplier() {
Supplier<String> messageSupplier = null;
assertThrows(NullPointerException.class,
() -> checkArgument(false, messageSupplier));
}
@Test
void testCheckArgument_false_withMessageFormat() {
LocalDate today = LocalDate.now();
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false, "String format: %s", today));
assertEquals(String.format("String format: %s", today), e.getMessage());
}
@Test
void testCheckArgument_false_withNullMessageFormat() {
LocalDate today = LocalDate.now();
assertThrows(NullPointerException.class,
() -> checkArgument(false, null, today));
}
// #endregion - Argument
// #region - ArgumentNotNull
@Test
void testCheckArgumentNotNull_notNull() {
final Object object = new Object();
assertEquals(object, checkArgumentNotNull(object));
}
@Test
void testCheckArgumentNotNull_notNull_withMessage() {
final Object object = new Object();
final String IGNORE_ME = "IGNORE_ME"; // NOSONAR
assertEquals(object, checkArgumentNotNull(object, IGNORE_ME));
}
@Test
void testCheckArgumentNotNull_notNull_withNullMessage() {
final Object object = new Object();
final String IGNORE_ME = null; // NOSONAR
assertEquals(object, checkArgumentNotNull(object, IGNORE_ME));
}
@Test
void testCheckArgumentNotNull_notNull_withMessageSupplier() {
final Object object = new Object();
assertEquals(object, checkArgumentNotNull(object, () -> "Error message: " + LocalDate.now()));
}
@Test
void testCheckArgumentNotNull_notNull_withNullMessageSupplier() {
final Object object = new Object();
final Supplier<String> IGNORE_ME = null; // NOSONAR
assertEquals(object, checkArgumentNotNull(object, IGNORE_ME));
}
@Test
void testCheckArgumentNotNull_notNull_withMessageFormat() {
final Object object = new Object();
LocalDate today = LocalDate.now();
assertEquals(object, checkArgumentNotNull(object, "String format: %s", today));
}
@Test
void testCheckArgumentNotNull_notNull_withNullMessageFormat() {
final Object object = new Object();
assertEquals(object, checkArgumentNotNull(object, null, LocalDate.now()));
}
@Test
void testCheckArgumentNotNull_null() {
final Object object = null;
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgumentNotNull_null_withMessage() {
final Object object = null;
final String message = "testCheckArgumentNotNull_null_withMessage";
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object, message));
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgumentNotNull_null_withNullMessage() {
final Object object = null;
final String message = null;
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object, message));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgumentNotNull_null_withMessageSupplier() {
final Object object = null;
final LocalDate today = LocalDate.now();
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object, () -> "Error message: " + today));
assertEquals("Error message: " + today, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgumentNotNull_null_withNullMessageSupplier() {
final Object object = null;
Supplier<String> messageSupplier = null;
assertThrows(NullPointerException.class,
() -> checkArgumentNotNull(object, messageSupplier));
}
@Test
void testCheckArgumentNotNull_null_withMessageFormat() {
final Object object = null;
LocalDate today = LocalDate.now();
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object, "String format: %s", today));
assertEquals(String.format("String format: %s", today), e.getMessage());
}
@Test
void testCheckArgumentNotNull_null_withNullMessageFormat() {
final Object object = null;
LocalDate today = LocalDate.now();
assertThrows(NullPointerException.class,
() -> checkArgumentNotNull(object, null, today));
}
// #endregion - ArgumentNotNull
// #region - State
@Test
void testCheckState_true() {
checkState(true);
}
@Test
void testCheckState_true_withMessage() {
final String IGNORE_ME = "IGNORE_ME"; // NOSONAR
checkState(true, IGNORE_ME);
}
@Test
void testCheckState_true_withNullMessage() {
final String IGNORE_ME = null; // NOSONAR
checkState(true, IGNORE_ME);
}
@Test
void testCheckState_true_withMessageSupplier() {
checkState(true, () -> "Error message: " + LocalDate.now());
}
@Test
void testCheckState_true_withNullMessageSupplier() {
final Supplier<String> IGNORE_ME = null; // NOSONAR
checkState(true, IGNORE_ME);
}
@Test
void testCheckState_true_withMessageFormat() {
LocalDate today = LocalDate.now();
checkState(true, "String format: %s", today);
}
@Test
void testCheckState_true_withNullMessageFormat() {
checkState(true, null, LocalDate.now());
}
@Test
void testCheckState_false() {
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckState_false_withMessage() {
final String message = "testCheckState_false_withMessage";
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false, message));
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckState_false_withNullMessage() {
final String message = null;
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false, message));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckState_false_withMessageSupplier() {
final LocalDate today = LocalDate.now();
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false, () -> "Error message: " + today));
assertEquals("Error message: " + today, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckState_false_withNullMessageSupplier() {
Supplier<String> messageSupplier = null;
assertThrows(NullPointerException.class,
() -> checkState(false, messageSupplier));
}
@Test
void testCheckState_false_withMessageFormat() {
LocalDate today = LocalDate.now();
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false, "String format: %s", today));
assertEquals(String.format("String format: %s", today), e.getMessage());
}
@Test
void testCheckState_false_withNullMessageFormat() {
LocalDate today = LocalDate.now();
assertThrows(NullPointerException.class,
() -> checkState(false, null, today));
}
// #endregion - State
// #region - NotNull
@Test
void testCheckNotNull_notNull() {
final Object object = new Object();
checkNotNull(object);
}
@Test
void testCheckNotNull_notNull_withMessage() {
final Object object = new Object();
final String IGNORE_ME = "IGNORE_ME"; // NOSONAR
checkNotNull(object, IGNORE_ME);
}
@Test
void testCheckNotNull_notNull_withNullMessage() {
final Object object = new Object();
final String IGNORE_ME = null; // NOSONAR
checkNotNull(object, IGNORE_ME);
}
@Test
void testCheckNotNull_notNull_withMessageSupplier() {
final Object object = new Object();
checkNotNull(object, () -> "Error message: " + LocalDate.now());
}
@Test
void testCheckNotNull_notNull_withNullMessageSupplier() {
final Object object = new Object();
final Supplier<String> IGNORE_ME = null; // NOSONAR
checkNotNull(object, IGNORE_ME);
}
@Test
void testCheckNotNull_notNull_withMessageFormat() {
final Object object = new Object();
LocalDate today = LocalDate.now();
checkNotNull(object, "String format: %s", today);
}
@Test
void testCheckNotNull_notNull_withNullMessageFormat() {
final Object object = new Object();
checkNotNull(object, null, LocalDate.now());
}
@Test
void testCheckNotNull_null() {
final Object object = null;
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckNotNull_null_withMessage() {
final Object object = null;
final String message = "testCheckNotNull_null_withMessage";
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object, message));
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckNotNull_null_withNullMessage() {
final Object object = null;
final String message = null;
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object, message));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckNotNull_null_withMessageSupplier() {
final Object object = null;
final LocalDate today = LocalDate.now();
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object, () -> "Error message: " + today));
assertEquals("Error message: " + today, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckNotNull_null_withNullMessageSupplier() {
final Object object = null;
Supplier<String> messageSupplier = null;
assertThrows(NullPointerException.class,
() -> checkNotNull(object, messageSupplier));
}
@Test
void testCheckNotNull_null_withMessageFormat() {
final Object object = null;
LocalDate today = LocalDate.now();
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object, "String format: %s", today));
assertEquals(String.format("String format: %s", today), e.getMessage());
}
@Test
void testCheckNotNull_null_withNullMessageFormat() {
final Object object = null;
LocalDate today = LocalDate.now();
assertThrows(NullPointerException.class,
() -> checkNotNull(object, null, today));
}
// #endregion - NotNull
// #region - Condition
static final class MyException extends RuntimeException {}
@Test
void testCheckCondition() {
checkCondition(true, MyException::new);
final MyException me = new MyException();
MyException e = assertThrows(MyException.class, () -> checkCondition(false, () -> me));
assertEquals(me, e);
}
// #endregion - Condition
// ================================
// #region - invoke constructor
// ================================
@Test
void test_constructor_isNotAccessible_ThrowsIllegalStateException() {
Constructor<?>[] constructors = AssertTools.class.getDeclaredConstructors();
Arrays.stream(constructors)
.forEach(constructor -> {
assertFalse(constructor.isAccessible());
constructor.setAccessible(true);
Throwable cause = assertThrows(Exception.class, constructor::newInstance)
.getCause();
assertInstanceOf(IllegalStateException.class, cause);
assertEquals("Utility class", cause.getMessage());
});
}
// ================================
// #endregion - invoke constructor
// ================================
}

View File

@@ -1,3 +1,4 @@
-- 测试用户表,覆盖全部列类型(数值/字符串/布尔/时间),用于验证 RowMapper 和查询功能
DROP TABLE IF EXISTS users;
CREATE TABLE users (
@@ -12,19 +13,25 @@ CREATE TABLE users (
work_start_time TIME
);
-- 唯一约束用于测试 batchUpdate 的重复键错误场景
ALTER TABLE users ADD CONSTRAINT uk_username UNIQUE (username);
-- 完整数据行(全部字段非空)
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('alice', 'alice@example.com', 28, 15000, TRUE, '2024-01-15 09:30:00', '1996-05-20', '08:00:00');
-- 完整数据行
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('bob', 'bob@example.com', 35, 25000, TRUE, '2023-11-01 14:00:00', '1989-03-12', '09:30:00');
-- null 数据行email、age、birth_date、work_start_time 均为 null用于测试空字段映射
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('charlie', NULL, NULL, 5000, FALSE, '2025-03-10 11:15:00', NULL, NULL);
-- 部分 null 数据行balance 为 nullcreated_at 为 null用于测试字段级 null 映射
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('diana', 'diana@example.com', 42, NULL, TRUE, NULL, '1982-11-08', '07:45:00');
-- 完整数据行
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('eve', 'eve@example.com', 31, 8000, TRUE, '2024-07-22 16:45:00', '1993-01-30', '10:00:00');