You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Java泛型与反射实现Spring SimpleJdbcCall通用Model映射

通用Model映射器实现方案(基于反射+Spring SimpleJdbcCall)

看起来你正在尝试构建一个基于反射的通用DAO层,来避免为每个Model编写重复的RowMapper代码——这个思路非常棒,能大幅减少冗余代码!我来帮你一步步解决反射实例化、属性映射和类型适配这三个核心问题,直接上可运行的实现方案:

一、核心思路拆解

我们需要在RowMapper的lambda实现里完成三件事:

  1. 通过反射创建目标Model实例
  2. 匹配ResultSet列与Model的Setter方法
  3. 根据属性类型调用ResultSet对应的取值方法

二、完整实现代码

首先补充DAO抽象类的实现,同时添加工具方法处理列名转换和类型映射:

@Repository
public abstract class DAO<T extends ModelBase> {
    private final DataSource dataSource;
    // 类型映射表:Java属性类型 → ResultSet对应的取值方法名
    private static final Map<Class<?>, String> TYPE_TO_RESULT_SET_METHOD = new HashMap<>();
    
    static {
        // 初始化常用类型的映射关系
        TYPE_TO_RESULT_SET_METHOD.put(String.class, "getString");
        TYPE_TO_RESULT_SET_METHOD.put(Integer.class, "getInt");
        TYPE_TO_RESULT_SET_METHOD.put(int.class, "getInt");
        TYPE_TO_RESULT_SET_METHOD.put(Long.class, "getLong");
        TYPE_TO_RESULT_SET_METHOD.put(long.class, "getLong");
        TYPE_TO_RESULT_SET_METHOD.put(Boolean.class, "getBoolean");
        TYPE_TO_RESULT_SET_METHOD.put(boolean.class, "getBoolean");
        TYPE_TO_RESULT_SET_METHOD.put(java.util.Date.class, "getDate");
        TYPE_TO_RESULT_SET_METHOD.put(java.sql.Date.class, "getDate");
        // 可根据需求扩展:BigDecimal、Float、Double等类型
    }

    DAO(final DataSource dataSource) {
        this.dataSource = dataSource;
    }

    protected List<T> buildModels(SqlParameterSource sqlSource, Class<T> modelClass) {
        final String RESULT_SET = "RESULT_SET";
        return new SimpleJdbcCall(dataSource)
                .withCatalogName("someCatalog")
                .withSchemaName("someSchema")
                .withProcedureName(getProcForModel(modelClass))
                .returningResultSet(RESULT_SET, (ResultSet rs, int rowNum) -> {
                    try {
                        // 1. 创建Model实例(假设Model有无参构造)
                        T model = modelClass.getDeclaredConstructor().newInstance();
                        // 获取ResultSet元数据,遍历所有列
                        ResultSetMetaData metaData = rs.getMetaData();
                        int columnCount = metaData.getColumnCount();

                        for (int i = 1; i <= columnCount; i++) {
                            String columnName = metaData.getColumnName(i);
                            // 2. 将数据库列名(通常下划线格式)转为驼峰式属性名
                            String propertyName = convertColumnToProperty(columnName);
                            // 找到对应的Setter方法
                            Method setterMethod = findSetterMethod(modelClass, propertyName);
                            if (setterMethod == null) {
                                // 无对应Setter时可跳过或打日志提示
                                continue;
                            }
                            // 3. 根据属性类型调用ResultSet对应的取值方法
                            Class<?> paramType = setterMethod.getParameterTypes()[0];
                            Object value = getResultSetValue(rs, columnName, paramType);
                            // 调用Setter赋值
                            setterMethod.invoke(model, value);
                        }
                        return model;
                    } catch (Exception e) {
                        throw new RuntimeException("Failed to map ResultSet to Model: " + modelClass.getName(), e);
                    }
                }).execute(sqlSource).get(RESULT_SET);
    }

    // 数据库列名转驼峰(例如USER_NAME → userName)
    private String convertColumnToProperty(String columnName) {
        if (columnName == null || columnName.isEmpty()) {
            return columnName;
        }
        StringBuilder sb = new StringBuilder();
        boolean nextUpperCase = false;
        for (char c : columnName.toCharArray()) {
            if (c == '_') {
                nextUpperCase = true;
            } else {
                sb.append(nextUpperCase ? Character.toUpperCase(c) : Character.toLowerCase(c));
                nextUpperCase = false;
            }
        }
        return sb.toString();
    }

    // 查找对应属性的Setter方法,支持递归查找父类
    private Method findSetterMethod(Class<?> modelClass, String propertyName) {
        String setterName = "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
        try {
            // 遍历当前类的方法
            for (Method method : modelClass.getDeclaredMethods()) {
                if (method.getName().equals(setterName) && method.getParameterCount() == 1) {
                    return method;
                }
            }
            // 当前类找不到则递归查找父类
            Class<?> superClass = modelClass.getSuperclass();
            if (superClass != null && !superClass.equals(Object.class)) {
                return findSetterMethod(superClass, propertyName);
            }
        } catch (Exception e) {
            // 忽略异常,返回null
        }
        return null;
    }

    // 根据属性类型从ResultSet获取对应值,处理类型转换与空值
    private Object getResultSetValue(ResultSet rs, String columnName, Class<?> targetType) throws SQLException {
        String methodName = TYPE_TO_RESULT_SET_METHOD.get(targetType);
        if (methodName == null) {
            // 未匹配类型默认用getString处理,也可抛出自定义异常
            return rs.getString(columnName);
        }
        try {
            // 反射调用ResultSet的取值方法
            Method method = ResultSet.class.getMethod(methodName, String.class);
            Object value = method.invoke(rs, columnName);
            // 处理基本类型空值:比如getInt返回0但实际数据库为null,包装类型转为null
            if (value != null && targetType.isPrimitive()) {
                return value;
            }
            // 特殊处理java.util.Date与java.sql.Date的转换
            if (targetType.equals(java.util.Date.class) && value instanceof java.sql.Date) {
                return new java.util.Date(((java.sql.Date) value).getTime());
            }
            return value == null ? null : value;
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            throw new SQLException("Failed to get value from ResultSet for column: " + columnName, e);
        }
    }

    // 抽象方法:由子类提供Model对应的存储过程名
    protected abstract String getProcForModel(Class<T> modelClass);
}

三、关键细节说明

  1. Model实例化:这里假设所有Model子类都有无参构造,若使用带参构造,可修改modelClass.getDeclaredConstructor(...)传入参数类型,再从ResultSet中获取对应值完成实例化。
  2. 列名与属性名映射:convertColumnToProperty方法处理了常见的下划线转驼峰规则,若有自定义命名规则,直接修改该方法逻辑即可。
  3. 类型适配:TYPE_TO_RESULT_SET_METHOD覆盖了常用Java类型,可根据业务需求扩展;同时处理了java.util.Date与java.sql.Date的类型转换,避免类型不匹配问题。
  4. 异常处理:将反射与JDBC操作异常统一包装为RuntimeException,上层调用者可统一处理,也可改为自定义异常。

四、优化建议

  • 缓存反射结果:用Map<Class<?>, Map<String, Method>>缓存每个Model类的Setter方法,避免每次映射重复查找,提升性能。
  • 注解支持:添加@Column注解指定属性对应的数据库列名,增强灵活性(比如属性名与列名不一致时)。
  • 构造函数注入:若不想依赖无参构造,可分析构造函数参数名与列名的对应关系,实现构造函数注入属性。

内容的提问来源于stack exchange,提问作者resu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:48:55