首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

手寫輕量級 ORM:利用註解與反射實現 ResultSet 自動映射

     編輯:Java 相關知識

痛點背景

JDBC 原生開發中,ResultSet 手動映射到實體類、實體屬性手動綁定到 PreparedStatement 代碼冗長、易錯且難維護。引入 MyBatis/Hibernate 又過於重量級。本文展示一套基於註解 + 反射 + 緩存的輕量映射方案,核心代碼不足 200 行。

核心設計:@Column 註解解決欄位名不一致

@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;  // 無註解則默認欄位名 = 屬性名
}

映射緩存池:Class → (別名 → Field)

雙層 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;
        });
    }
}

ResultSet → 實體對象自動填充

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);
    }
}

實體 → PreparedStatement 反向綁定

關鍵創新: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);
    }
}

SQL 方言解析器核心邏輯

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);
        });
    }
}

方案優勢總結

  • 零依賴:僅需 JDK,無第三方 JAR
  • 高性能:反射緩存 + PreparedStatement 重用
  • 靈活映射:註解別名解決命名風格差異
  • 可擴展:輕鬆新增類型處理器、攔截器、分頁插件

完整代碼含類型處理器(Integer/Long/String/Date/Blob 等)、連接池整合、事務模板請參考附件下載。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved