环境: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;
}
...
}
堆栈截图如下
可以看到回调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)));
}
思考
- 编译不会异常,运行时报错,例如我们当前场景,如果测试没有覆盖到各种分片key的类型,则会有线上故障的风险
- 客户端使用不友好,自定义实现类实现了框架的StandardShardingAlgorithm接口,但是框架没有保障泛型约束,需要客户端自己兼容,很不友好
- 当调用的实例方法存在泛型,并且入参存在泛型时,需要保障泛型类型的一致性,实际代码中大量使用@SuppressWarnings(“rawtypes”)注解规避类型约束,带来的风险与体验在没遇到时,不痛不痒,遇到时可就头疼了