1、场景
select语句返回两列数据,需要以kv的格式接收,于是自定义实现了一个CustMap拦截器,只要使用了CustMap,就会对sql的返回结果拦截重新封装为Map<K, V>结构返回,测试环境能够正常使用,上线到生产环境后就一直报TooManyResultsException,Expect one result or null to be return by selectOne…
2、分析
Mybatis的动态代理(此处不介绍,有需要的可以自行搜索了解),底层默认是selectOne方法,返回为List需要选择selectList执行,怎么选择的自行研究源码。
3、解决
将Map<K, V>使用List封装List<Map<K, V>>或者直接使用实例List<实例>,这两种方式都可以避免此问题。
备注:未定位到测试环境和生产环境(各个组件版本一致)两种不同结果的原因,有思路的可以留言。
/**
* 自定义Map注解
* 将mybatis的select查询的两列值转为map的kv值
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CustomMap {
/**
* 是否允许key重复。如果不允许,而实际结果出现了重复,会抛出org.springframework.dao.DuplicateKeyException。
*/
boolean isAllowKeyRepeat() default true;
/**
* 对于相同的key,是否允许value不同(在允许key重复的前提下)。如果允许,则按查询结果,后面的覆盖前面的;如果不允许,则会抛出org.springframework.dao.DuplicateKeyException。
*/
boolean isAllowValueDifferentWithSameKey() default false;
}
@Intercepts(@Signature(method = "handleResultSets", type = ResultSetHandler.class, args = {Statement.class}))
public class CustomMapResultPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MetaObject metaStatementHandler = ReflectUtil.getRealTarget(invocation);
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("mappedStatement");
// 当前类
String className = StringUtils.substringBeforeLast(mappedStatement.getId(), ".");
// 当前方法
String currentMethodName = StringUtils.substringAfterLast(mappedStatement.getId(), ".");
// 获取当前Method
Method currentMethod = findMethod(className, currentMethodName);
if (currentMethod == null || currentMethod.getAnnotation(CustomMap.class) == null) {
// 如果当前Method没有注解CustomMap
return invocation.proceed();
}
// 如果有CustomMap注解,则这里对结果进行拦截并转换
CustomMap customMap = currentMethod.getAnnotation(CustomMap.class);
Statement statement = (Statement) invocation.getArgs()[0];
// 获取返回Map里key-value的类型
Pair<Class<?>, Class<?>> kvTypePair = getKVTypeOfReturnMap(currentMethod);
// 获取各种TypeHander的注册器
TypeHandlerRegistry typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
return result2Map(statement, typeHandlerRegistry, kvTypePair, customMap);
}
@Override
public Object plugin(Object obj) {
return Plugin.wrap(obj, this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* 找到与指定函数名匹配的Method。
*/
private Method findMethod(String className, String targetMethodName) throws Throwable {
// 该类所有声明的方法
Method[] methods = Class.forName(className).getDeclaredMethods();
for (Method method : methods) {
if (StringUtils.equals(method.getName(), targetMethodName)) {
return method;
}
}
return null;
}
/**
* 获取函数返回Map中key-value的类型
*
* @return left为key的类型,right为value的类型
*/
private Pair<Class<?>, Class<?>> getKVTypeOfReturnMap(Method method) {
Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) returnType;
if (!Map.class.equals(parameterizedType.getRawType())) {
throw new RuntimeException(
"[ERROR-CustomMap-return-Map-type]使用CustomMap,返回类型必须是java.util.Map类型!!!method=" + method);
}
return new Pair<>((Class<?>) parameterizedType.getActualTypeArguments()[0],
(Class<?>) parameterizedType.getActualTypeArguments()[1]);
}
return new Pair<>(null, null);
}
/**
* 将查询结果映射成Map,其中第一个字段作为key,第二个字段作为value.
*
* @param typeHandlerRegistry MyBatis里typeHandler的注册器,方便转换成用户指定的结果类型
* @param kvTypePair 函数指定返回Map key-value的类型
*/
private Object result2Map(Statement statement, TypeHandlerRegistry typeHandlerRegistry, Pair<Class<?>, Class<?>> kvTypePair, CustomMap customMap) throws Throwable {
ResultSet resultSet = statement.getResultSet();
List<Object> res = new ArrayList<>();
Map<Object, Object> map = new HashMap<>();
while (resultSet.next()) {
Object key = this.getObject(resultSet, 1, typeHandlerRegistry, kvTypePair.getKey());
Object value = this.getObject(resultSet, 2, typeHandlerRegistry, kvTypePair.getValue());
if (map.containsKey(key)) {// 该key已存在
if (!customMap.isAllowKeyRepeat()) {
// 判断是否允许key重复
throw new DuplicateKeyException("CustomMap duplicated key!key=" + key);
}
Object preValue = map.get(key);
if (!customMap.isAllowValueDifferentWithSameKey() && !Objects.equals(value, preValue)) {
// 判断是否允许value不同
throw new DuplicateKeyException("CustomMap different value with same key!key=" + key + ",value1=" + preValue + ",value2=" + value);
}
}
// 第一列作为key,第二列作为value。
map.put(key, value);
}
res.add(map);
return res;
}
/**
* 结果类型转换。
* 这里借用注册在MyBatis的typeHander(包括自定义的),方便进行类型转换。
*
* @param columnIndex 字段下标,从1开始
* @param typeHandlerRegistry MyBatis里typeHandler的注册器,方便转换成用户指定的结果类型
* @param javaType 要转换的Java类型
*/
private Object getObject(ResultSet resultSet, int columnIndex, TypeHandlerRegistry typeHandlerRegistry, Class<?> javaType) throws SQLException {
final TypeHandler<?> typeHandler = typeHandlerRegistry.hasTypeHandler(javaType) ? typeHandlerRegistry.getTypeHandler(javaType) : typeHandlerRegistry.getUnknownTypeHandler();
return typeHandler.getResult(resultSet, columnIndex);
}
}
SqlSessionFactory配置拦截器
@Bean(name = "primarySqlSessionFactory")
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
CatMybatisPlugin catMybatisPlugin = new CatMybatisPlugin();
factoryBean.setPlugins(new Interceptor[]{catMybatisPlugin});
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml");
factoryBean.setMapperLocations(resources);
//设置自定义map拦截器
factoryBean.setPlugins(new CustomMapResultPlugin());
SqlSessionFactory sessionFactory = factoryBean.getObject();
return sessionFactory;
}
Mapper配置自定义注解
@CustomMap
Map<String, Long> queryTotalByType(Map<String, Object> map);