ColumnNameCamel 性能优化方案
背景
ColumnNameCamel 是下划线 ↔ 驼峰字段名转换器,调用频率极高:
- 每行 ResultSet 的每个字段都会经过
toFieldName - SQL 参数绑定、查询条件渲染也会调用
- ORM 框架底层高频路径
当前实现瓶颈
当前版本:ColumnNameCamel.java(原始版本)
toFieldName(下划线转驼峰)
// 问题 1: toCharArray() 拷贝整个字符数组
for (char c : dbKey.toCharArray()) { ... }
// 问题 2: 正则匹配 + 循环 replace — O(n²)
dbKey = dbKey.toLowerCase(Locale.ROOT); // 第 1 趟扫描
Matcher mat = toCamel.matcher(dbKey);
while (mat.find()) {
dbKey = dbKey.replace(...); // 每找到一次就全表扫描一次
}
return dbKey.replaceAll("_", ""); // 第 N+2 趟扫描
| 问题 | 影响 |
|---|---|
while(mat.find()) + String.replace() | O(n²),10 个下划线 = 10 次全串扫描 |
toCharArray() | 每次拷贝完整 char 数组 |
Character.isLowerCase/isDigit | Unicode 感知,比 ASCII 范围慢数倍 |
| 多次 String 分配 | toLowerCase → N 次 replace → replaceAll,每条都新建 String |
| 无缓存 | 相同列名跨行列重复计算 |
toDbColumnName(驼峰转下划线)
// 正则清洗特殊字符 + 正则插入下划线 + toUpperCase — 3 次分配
String cleaned = specialCharsPattern.matcher(beanKey).replaceAll("");
return toUnder.matcher(cleaned).replaceAll("_$1").toUpperCase(Locale.ROOT);
优化方案
参考实现:ColumnNameCamelTodo.java
1. 单趟 char 扫描(核心优化)
去掉 Pattern/Matcher 和 String.replace,改用一次循环 + char 数组直接输出。
输入 "user_name_info"
遍历每个字符:
'_' → 标记 nextUpper,跳过
字母 → 根据 nextUpper 决定大小写,输出到 char[]
数字 → 直接输出
输出 new String(buf, 0, j) — 一趟完成
效果:O(n²) → O(n),去掉了所有正则和中间 String。
2. 零分配快速路径
下划线和大写都检测不到时,直接返回入参(零分配)。
// 一趟扫描,纯 O(n) 检查
for (int i = 0; i < len; i++) {
if (c == '_' || (c >= 'A' && c <= 'Z')) needConvert = true;
}
if (!needConvert) return dbKey; // 最常见情况:"id", "name", "status"
3. ConcurrentHashMap 结果缓存
private static final ConcurrentMap<String, String> FIELD_CACHE = new ConcurrentHashMap<>(256);
- 列名集合有界(几百~几千),命中后零计算
- 设置 100K 软上限防止极端情况
4. ASCII 范围比较
// 快 — 直接整数字段比较
c >= 'a' && c <= 'z'
// 慢 — Unicode Character 方法
Character.isLowerCase(c)
数据库字段名全是 ASCII,不需要 Unicode 感知。
内存分析
10,000 字段缓存占用
| 对象 | 大小 |
|---|---|
| ConcurrentHashMap Node | ~32B |
| Key String (15 字符) + char[] | ~72B |
| Value String (13 字符) + char[] | ~68B |
| 单条小计 | ~172B |
| 10,000 条 | ~1.9 MB |
典型项目几百几千列名,对应 400KB1MB,完全可接受。
vs MemoryCache
列名缓存只做 String→String 映射,不需要:
- ❌ Element 包装(+24B + expired Long)
- ❌
ValUtil.toStr(key)转换开销 - ❌ Serializable 装箱拆箱
- ❌ 后台过期线程
- ❌ 多缓存分区命名
→ 直接 ConcurrentHashMap<String, String> 最轻量。
收益预估
| 方案 | toFieldName 相对耗时 |
|---|---|
| 原始版本(regex O(n²)) | 100 (基准) |
| 单趟转换(无缓存) | ~15 |
| 单趟转换 + ConcurrentHashMap | ~12 |
单趟转换解决 ~85% 的性能问题,缓存再解决剩余的。后面的微调(ThreadLocal 缓冲区、静态 HashMap)收益在个位数百分比。
测试要点
覆盖当前 ColumnNameCamelTest 和 ColumnNameCamelTest_Extended 中的行为:
- 基本下划线转驼峰:
user_name→userName - 全小写无下划线直接返回:
name→name - 大写带下划线:
USER_NAME→userName - 混合大小写无下划线:
UserName→username - 多下划线:
user__name→userName - 首尾下划线:
_user_name_→UserName - 下划线+数字:
user_1_name→user1Name - 特殊字符/Unicode 保持不变
- toDbColumnName:
userName→USER_NAME,aBbCc→A_BB_CC
相关文件
- 原始实现:
ColumnNameCamel.java - 优化参考:
ColumnNameCamelTodo.java - 单元测试:
ColumnNameCamelTest.java、ColumnNameCamelTest_Extended.java - 调用链路:
ResultMapRowMapper.java→DbConvertUtil.java→ColumnNameCamel.java