shardingshpere客户端封装之泛型踩坑

环境:jdk11
感觉也可以叫shardingsphere客户端踩坑记录-_-!!!

问题现场

报错很明确,类型转换错误,日志如下

### Cause: java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap')
	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
	... 49 more
Caused by: java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap')
	at ....orm.algorithms.DefaultShardingAlgorithm.doSharding(DefaultShardingAlgorithm.java:23)
	at org.apache.shardingsphere.sharding.route.strategy.type.standard.StandardShardingStrategy.doSharding(StandardShardingStrategy.java:68)
	at org.apache.shardingsphere.sharding.route.strategy.type.standard.StandardShardingStrategy.doSharding(StandardShardingStrategy.java:57)
	at org.apache.shardingsphere.sharding.route.engine.type.standard.ShardingStandardRoutingEngine.routeDataSources(ShardingStandardRoutingEngine.java:207)
	at org.apache.shardingsphere.sharding.route.engine.type.standard.ShardingStandardRoutingEngine.route0(ShardingStandardRoutingEngine.java:195)
	at org.apache.shardingsphere.sharding.route.engine.type.standard.ShardingStandardRoutingEngine.routeByShardingConditionsWithCondition(ShardingStandardRoutingEngine.java:118)
	at org.apache.shardingsphere.sharding.route.engine.type.standard.ShardingStandardRoutingEngine.routeByShardingConditions(ShardingStandardRoutingEngine.java:111)
	at org.apache.shardingsphere.sharding.route.engine.type.standard.ShardingStandardRoutingEngine.getDataNodes(ShardingStandardRoutingEngine.java:88)
	at org.apache.shardingsphere.sharding.route.engine.type.standard.ShardingStandardRoutingEngine.route(ShardingStandardRoutingEngine.java:70)
	at org.apache.shardingsphere.sharding.route.engine.ShardingSQLRouter.createRouteContext(ShardingSQLRouter.java:56)
	at org.apache.shardingsphere.sharding.route.engine.ShardingSQLRouter.createRouteContext(ShardingSQLRouter.java:44)
	at org.apache.shardingsphere.infra.route.engine.impl.PartialSQLRouteExecutor.route(PartialSQLRouteExecutor.java:62)
	at org.apache.shardingsphere.infra.route.engine.SQLRouteEngine.route(SQLRouteEngine.java:53)
	at org.apache.shardingsphere.infra.context.kernel.KernelProcessor.route(KernelProcessor.java:54)
	at org.apache.shardingsphere.infra.context.kernel.KernelProcessor.generateExecutionContext(KernelProcessor.java:46)
	at org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement.createExecutionContext(ShardingSpherePreparedStatement.java:378)
	at org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement.execute(ShardingSpherePreparedStatement.java:286)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.apache.ibatis.logging.jdbc.PreparedStatementLogger.invoke(PreparedStatementLogger.java:59)
	at com.sun.proxy.$Proxy468.execute(Unknown Source)
	at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:64)
	at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
	at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
	at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
	at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
	at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)
	at com.sun.proxy.$Proxy466.query(Unknown Source)
	at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:108)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
	at com.sun.proxy.$Proxy466.query(Unknown Source)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
	... 55 more

问题原因

公司封装了sharding sphere客户端,抛出异常的代码第23行是: Long index = shardingValue.getValue() % availableTargetNames.size();
完整源代码如下:

public class DefaultShardingAlgorithm implements StandardShardingAlgorithm<Long> {
    public static final String TYPE = "DEFAULT_ALGORITHM";

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
        throw new UnsupportedOperationException("inline sharding algorithm can not tackle with range query");
    }

    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
        List<String> tables = Lists.newArrayList(availableTargetNames);
        Long index = shardingValue.getValue() % availableTargetNames.size();
        return tables.get(index.intValue());
    }

    @Override
    public void init() {

    }

    @Override
    public String getType() {
        return TYPE;
    }
}

问题定位很简单,增加断点发现入参的shardingValue.getValue()为Integer,该值取自数据库字段,数据库类型为int,因此出现了类型转换异常,sharding sphere回调doSharding方法入口实现代码如下:

public final class StandardShardingStrategy implements ShardingStrategy {
    
    private final String shardingColumn;
    
    @Getter
    private final StandardShardingAlgorithm<?> shardingAlgorithm;
    
    ...
    // 1. shardingConditionValues类型ArrayList,value=ArrayList<Integer>,泛型为:ListShardingConditionValue<Integer>
    @Override
    public Collection<String> doSharding(final Collection<String> availableTargetNames, final Collection<ShardingConditionValue> shardingConditionValues, final ConfigurationProperties props) {
        ShardingConditionValue shardingConditionValue = shardingConditionValues.iterator().next();
        // 2. shardingConditionValue类型为ListShardingConditionValue,泛型为:Integer
        Collection<String> shardingResult = shardingConditionValue instanceof ListShardingConditionValue
                ? doSharding(availableTargetNames, (ListShardingConditionValue) shardingConditionValue) : doSharding(availableTargetNames, (RangeShardingConditionValue) shardingConditionValue);
        Collection<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
        result.addAll(shardingResult);
        return result;
    }
    
    @SuppressWarnings("unchecked")
    private Collection<String> doSharding(final Collection<String> availableTargetNames, final ListShardingConditionValue<?> shardingValue) {
        Collection<String> result = new LinkedList<>();
        // 3. shardingConditionValue类型为ListShardingConditionValue,泛型为:Integer
        for (Comparable<?> each : shardingValue.getValues()) {
            String target;
            // 4. shardingAlgorithm是StandardShardingAlgorithm<?>,泛型只要是Comparable子类即可,
            // 实际实现类为我们自定义实现DefaultShardingAlgorithm,泛型为Long,泛型为Long,实际shardingConditionValue泛型为Integer
            target = shardingAlgorithm.doSharding(availableTargetNames, new PreciseShardingValue(shardingValue.getTableName(), shardingValue.getColumnName(), each));
            if (null != target && availableTargetNames.contains(target)) {
                result.add(target);
            } else if (null != target && !availableTargetNames.contains(target)) {
                throw new ShardingSphereException(String.format("Route table %s does not exist, available actual table: %s", target, availableTargetNames));
            }
        }
        return result;
    }
    ...
}

堆栈截图如下
134952

可以看到回调StandardShardingAlgorithm时,对StandardShardingAlgorithm的泛型与入参的ShardingValue是没有约束的。并且也不会有编译异常,因此很容易出现客户端使用时的泛型类型与实际分库key类型不一致的异常

小结

实现框架的接口为Long泛型,实际字段传入的是Integer类型,使用sharding value与integer类型的size取余数运算时,结果为Integer,而实现方法中基于方法的泛型Long进行预算,期望结果为Long类型,导致类型转换失败异常

总结

jdk对于泛型有强制校验,例如,如果我们入参为List,那么传入List,编译无法通过,但是由于框架StandardShardingStrategy策略类的shardingAlgorithm泛型与PreciseShardingValue泛型类型未约束,导致存在不兼容问题,由于未约束,我们甚至可以传入任何Comparable子类型的参数给我们的实际实现类(限制泛型为Long),代码如下

public static void main(String[] args) {
    DefaultShardingAlgorithm defaultShardingAlgorithm = new DefaultShardingAlgorithm();
    // 编译错误
    defaultShardingAlgorithm.doSharding(Lists.newArrayList("test"), new PreciseShardingValue<>("myTable", "myColumn", 1));
    defaultShardingAlgorithm.doSharding(Lists.newArrayList("test"), new PreciseShardingValue<>("myTable", "myColumn", 1L));
    StandardShardingAlgorithm<?> standardShardingAlgorithm = defaultShardingAlgorithm;
    // 不论参数是Integer,还是BigDecimal,都不会编译异常
    standardShardingAlgorithm.doSharding(Lists.newArrayList("test"), new PreciseShardingValue("myTable", "myColumn", "haha"));
    standardShardingAlgorithm.doSharding(Lists.newArrayList("test"), new PreciseShardingValue("myTable", "myColumn", new BigDecimal(1)));
}

思考

  1. 编译不会异常,运行时报错,例如我们当前场景,如果测试没有覆盖到各种分片key的类型,则会有线上故障的风险
  2. 客户端使用不友好,自定义实现类实现了框架的StandardShardingAlgorithm接口,但是框架没有保障泛型约束,需要客户端自己兼容,很不友好
  3. 当调用的实例方法存在泛型,并且入参存在泛型时,需要保障泛型类型的一致性,实际代码中大量使用@SuppressWarnings(“rawtypes”)注解规避类型约束,带来的风险与体验在没遇到时,不痛不痒,遇到时可就头疼了
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值