You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Java 8升级至21后SimpleJdbcCall.execute执行Oracle存储过程时的类型转换异常问题

Java 8升级至21后SimpleJdbcCall.execute执行Oracle存储过程时的类型转换异常问题

这种核心遗留模块升级遇到的兼容性问题真的让人头大,尤其是上游传参和存储过程都动不得的情况,我给你几个可行的解决方向,都是基于修改现有代码的方案:


方案一:基于存储过程元数据的参数预转换(最推荐,针对性强)

你已经尝试获取存储过程元数据了,只是之前的方法有点小问题,我们可以优化它,然后在构建参数源前主动把字符串类型的参数转换成存储过程期望的数值类型:

第一步:修复并优化参数元数据获取方法

先把硬编码的schema改成传入参数,并且只过滤出输入/输入输出类型的参数(毕竟我们只需要处理传入的参数):

private Map<String, Integer> getProcedureParameterTypes(String catalog, String schema, String procedureName) {
    Map<String, Integer> paramTypes = new HashMap<>();
    try (Connection conn = dataSource.getConnection()) {
        DatabaseMetaData metaData = conn.getMetaData();
        // 替换硬编码的"MAMBO"为传入的schema参数
        try (ResultSet rs = metaData.getProcedureColumns(catalog, schema, procedureName, "%")) {
            while (rs.next()) {
                // COLUMN_TYPE: 1=IN参数, 2=INOUT参数, 3=OUT参数,只处理需要传入的参数
                int paramDirection = rs.getInt("COLUMN_TYPE");
                if (paramDirection == 1 || paramDirection == 2) {
                    String paramName = rs.getString("COLUMN_NAME");
                    int sqlType = rs.getInt("DATA_TYPE");
                    paramTypes.put(paramName.toLowerCase(), sqlType);
                }
            }
        }
    } catch (SQLException e) {
        log.error("获取存储过程{}的参数元数据失败", procedureName, e);
        throw new RuntimeException(e);
    }
    return paramTypes;
}

第二步:构建参数前主动转换类型

在创建MapSqlParameterSource之前,遍历原始参数,根据元数据的SQL类型把字符串值转成对应的数值类型(比如"100.0"转成BigDecimal或者整数):

// 获取当前存储过程的参数类型映射
Map<String, Integer> paramTypes = getProcedureParameterTypes(catalog, schema, spName);

// 构建转换后的参数Map
Map<String, Object> convertedParams = new HashMap<>(paramMap.size());
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
    String paramName = entry.getKey().toLowerCase();
    Object rawValue = entry.getValue();

    // 只处理字符串类型的参数,且该参数在存储过程元数据中存在
    if (rawValue instanceof String strValue && paramTypes.containsKey(paramName)) {
        int sqlType = paramTypes.get(paramName);
        try {
            if (sqlType == Types.NUMERIC || sqlType == Types.DECIMAL) {
                // Oracle NUMBER类型用BigDecimal处理最稳妥,兼容整数和小数场景
                convertedParams.put(entry.getKey(), new BigDecimal(strValue));
            } else if (sqlType == Types.INTEGER || sqlType == Types.BIGINT) {
                // 如果是整数类型,先转成BigDecimal再取整,避免直接转字符串报错
                BigDecimal bdValue = new BigDecimal(strValue);
                convertedParams.put(entry.getKey(), bdValue.toBigInteger());
            } else {
                // 其他类型保持原始值
                convertedParams.put(entry.getKey(), rawValue);
            }
        } catch (NumberFormatException e) {
            log.error("参数{}的值{}转换为SQL类型{}失败", entry.getKey(), strValue, sqlType, e);
            throw new IllegalArgumentException("参数格式错误:" + entry.getKey(), e);
        }
    } else {
        // 非字符串参数或无元数据的参数直接保留
        convertedParams.put(entry.getKey(), rawValue);
    }
}

// 用转换后的参数创建SqlParameterSource
SqlParameterSource parameters = new MapSqlParameterSource().addValues(convertedParams);

这个方案的好处是只针对当前存储过程的参数做转换,不会影响其他模块,而且基于元数据的转换更准确,适配不同的参数类型。


方案二:自定义Spring JDBC类型处理器(全局配置,需谨慎)

如果多个存储过程都有类似问题,可以通过注册自定义TypeHandler来全局处理字符串到数值类型的转换:

第一步:编写自定义类型处理器

public class StringToNumericTypeHandler extends BaseTypeHandler<Number> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Number parameter, JdbcType jdbcType) throws SQLException {
        // 处理字符串类型的参数(这里判断如果传入的是字符串,就转成BigDecimal)
        if (parameter instanceof String strValue) {
            ps.setBigDecimal(i, new BigDecimal(strValue));
        } else {
            // 非字符串参数按默认逻辑处理
            ps.setObject(i, parameter);
        }
    }

    // 以下方法针对结果集读取,我们只处理参数输入,直接返回BigDecimal即可
    @Override
    public Number getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getBigDecimal(columnName);
    }

    @Override
    public Number getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getBigDecimal(columnIndex);
    }

    @Override
    public Number getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getBigDecimal(columnIndex);
    }
}

第二步:注册类型处理器到SimpleJdbcCall

在创建SimpleJdbcCall的时候,把自定义处理器注册进去:

SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(new JdbcTemplate(dataSource))
    .withProcedureName(spName);

// 注册自定义类型处理器,针对NUMERIC/DECIMAL类型的字符串参数
TypeHandlerRegistry registry = simpleJdbcCall.getJdbcTemplate().getTypeHandlerRegistry();
registry.register(String.class, Types.NUMERIC, new StringToNumericTypeHandler());
registry.register(String.class, Types.DECIMAL, new StringToNumericTypeHandler());

这个方案适合批量处理类似问题,但要注意全局配置可能影响其他正常参数的转换,需要充分测试。


方案三:自定义Spring转换服务(全局宽松转换)

Spring JDBC的类型转换依赖ConversionService,我们可以添加自定义转换器来放宽字符串到数值的转换规则:

// 创建自定义转换服务
DefaultConversionService conversionService = new DefaultConversionService();

// 添加字符串转BigDecimal的转换器,兼容"100.0"这种格式
conversionService.addConverter(String.class, BigDecimal.class, BigDecimal::new);

// 添加字符串转Integer的转换器,自动处理"100.0"转整数
conversionService.addConverter(String.class, Integer.class, str -> new BigDecimal(str).intValue());

// 添加字符串转Long的转换器
conversionService.addConverter(String.class, Long.class, str -> new BigDecimal(str).longValue());

// 把转换服务设置到JdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setConversionService(conversionService);

// 用这个JdbcTemplate创建SimpleJdbcCall
SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(jdbcTemplate)
    .withProcedureName(spName);

这个方案的本质是让Spring的类型转换逻辑回到Java8时期的宽松模式,但同样要注意全局影响,需要测试所有相关的数据库操作。


为什么Java8没问题,Java21出问题?

主要是因为Java21对应的Spring版本(比如Spring 6.x,要求Java17+)的类型转换逻辑更严格了,遵循了更规范的类型匹配规则;而Java8时期的Spring 5.x版本会自动尝试各种隐式转换,哪怕字符串格式是小数形式也能转成整数。


备注:内容来源于stack exchange,提问作者Pranav Rk

火山引擎 最新活动