跳到主要内容

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/isDigitUnicode 感知,比 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)收益在个位数百分比。


测试要点

覆盖当前 ColumnNameCamelTestColumnNameCamelTest_Extended 中的行为:

  • 基本下划线转驼峰:user_nameuserName
  • 全小写无下划线直接返回:namename
  • 大写带下划线:USER_NAMEuserName
  • 混合大小写无下划线:UserNameusername
  • 多下划线:user__nameuserName
  • 首尾下划线:_user_name_UserName
  • 下划线+数字:user_1_nameuser1Name
  • 特殊字符/Unicode 保持不变
  • toDbColumnName:userNameUSER_NAMEaBbCcA_BB_CC

相关文件

  • 原始实现:ColumnNameCamel.java
  • 优化参考:ColumnNameCamelTodo.java
  • 单元测试:ColumnNameCamelTest.javaColumnNameCamelTest_Extended.java
  • 调用链路:ResultMapRowMapper.javaDbConvertUtil.javaColumnNameCamel.java