JDBC 原生開發中,ResultSet 手動映射到實體類、實體屬性手動綁定到 PreparedStatement 代碼冗長、易錯且難維護。引入 MyBatis/Hibernate 又過於重量級。本文展示一套基於註解 + 反射 + 緩存的輕量映射方案,核心代碼不足 200 行。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String value(); // 資料庫欄位名
}
實體類使用示例:
public class Work {
@Column("coop_type") private Integer coopType; // 駝峰 ↔ 下劃線
@Column("create_time") private String createTime;
private String subtitle; // 無註解則默認欄位名 = 屬性名
}
雙層 HashMap + 雙重檢查鎖實現線程安全緩存:
public class BeanMappingPool {
private static final Map<Class<?>, Map<String, Field>> MAPPING_POOLS = new ConcurrentHashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
public static Map<String, Field> getFieldsMap(Class<?> clazz) {
return MAPPING_POOLS.computeIfAbsent(clazz, k -> {
Map<String, Field> map = new HashMap<>();
// 向上遞歸掃描父類字段
for (Class<?> c = clazz; c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
f.setAccessible(true);
Column col = f.getAnnotation(Column.class);
String alias = (col != null) ? col.value() : f.getName();
map.put(alias, f);
}
}
return map;
});
}
}
public List<Object> query(String sql, Class<?> clazz, Object[] params) {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// 1. 綁定參數
bindParameters(ps, params);
// 2. 執行查詢
try (ResultSet rs = ps.executeQuery()) {
List<String> columns = getColumnNames(rs); // 取得欄位名列表
Map<String, Field> fieldMap = BeanMappingPool.getFieldsMap(clazz);
List<Object> result = new ArrayList<>();
while (rs.next()) {
Object entity = clazz.getDeclaredConstructor().newInstance();
for (String col : columns) {
Field field = fieldMap.get(col);
if (field != null) {
Object value = extractValue(rs, col, field.getType());
field.set(entity, value);
}
}
result.add(entity);
}
return result;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
關鍵創新:SQL 方言 name=?name 讓佔位符攜帶欄位別名
// 原始 SQL 方言
String sql = "UPDATE user SET name=?name, age=?age WHERE id=?id";
// 解析後:標準 SQL + 參數順序列表
// 標準 SQL: UPDATE user SET name=?, age=? WHERE id=?
// 參數順序: [name, age, id]
public boolean save(Object entity, String dialectSql) {
SqlMap sqlMap = PreparedSqlMappingPool.parse(dialectSql);
Map<String, Field> fieldMap = BeanMappingPool.getFieldsMap(entity.getClass());
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sqlMap.getStandardSql())) {
int idx = 1;
for (String paramAlias : sqlMap.getParamAliases()) {
Field field = fieldMap.get(paramAlias);
if (field == null) continue;
Object val = field.get(entity);
setParameter(ps, idx++, val, field.getType());
}
return ps.executeUpdate() > 0;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public class PreparedSqlMappingPool {
private static final Map<String, Pair<String, List<String>>> CACHE = new ConcurrentHashMap<>();
public static Pair<String, List<String>> parse(String dialectSql) {
return CACHE.computeIfAbsent(dialectSql, sql -> {
StringBuilder stdSql = new StringBuilder();
List<String> params = new ArrayList<>();
StringBuilder paramName = new StringBuilder();
boolean inParam = false;
for (char c : sql.toCharArray()) {
if (c == '?') {
stdSql.append('?');
inParam = true;
paramName.setLength(0);
} else if (inParam && Character.isJavaIdentifierPart(c)) {
paramName.append(c);
} else {
if (inParam && paramName.length() > 0) {
params.add(paramName.toString());
}
inParam = false;
stdSql.append(c);
}
}
// 末尾參數收尾
if (inParam && paramName.length() > 0) params.add(paramName.toString());
return Pair.of(stdSql.toString(), params);
});
}
}
完整代碼含類型處理器(Integer/Long/String/Date/Blob 等)、連接池整合、事務模板請參考附件下載。