Mybatis:解决调用带有集合类型形参的mapper方法时,集合参数为空或null的问题
此文章有问题,待修改!
使用Mybatis时,有时需要批量增删改查,这时就要向mapper方法中传入集合类型(List或Set)参数,下面是一个示例。
// 该文件不完整,只展现关键部分 @Mapper public class UserMapper { List<User> selectByBatchIds(List<Long> ids); }
<!-- 省略不重要代码,只保留与selectByBatchIds()方法对应的部分 --> <select id="selectByBatchIds" parameterType="long" resultMap="user"> select * from `user` where id in <foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>; </select>
但是如果传入的集合类型参数为null或空集合会怎样呢?如果集合类型参数为null,程序调用方法时抛出NullPointerException;如果集合类型参数为空集合,渲染出来的sql语句将会是"select * from `user` where id in ;",执行sql时也会报错。
这类问题经典的解决办法有两种。第一种方法,在调用mapper方法前,检查方法实参是否为null或空集合;第二种方法:在XXMapper.xml的CRUD元素中使用<if>标签或<choose>标签进行判断,下面是一个改进的XXMapper.xml的示例。
<!-- 省略不重要代码,只保留与selectByBatchIds()方法相关的片段 --> <select id="selectByBatchIds" parameterType="long" resultMap="user"> <choose> <when test="ids != null and ids.size() != 0"> select * from `user` where id in <foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach> </when> <otherwise>select * from `user` where false</otherwise> </choose>; </select>
上面的两种方法都需要在许多地方增加检查代码,显得不够优雅,有没有比较优雅的方法呢?有,使用Mybatis拦截器。拦截器可以拦截mapper方法的执行,根据条件决定mapper方法如何执行,如果传入的参数为空集合,则返回默认值(空集合、0或null)。下面是一个示例。
package org.anonym.rbac.persistence.mybatis.interceptor; import cn.hutool.core.util.ReflectUtil; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.anonym.rbac.persistence.mybatis.EmptyReturnZero; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; /** * 此Mybatis拦截器处理mapper方法中数组或集合类型参数为null或为空的情况。<br /> * 如果mapper方法的数组或集合参数为null或为空,则此拦截器令mapper方法的返回零值。<br /> * 不同类型的零值规定如下。<br /> * <ol> * <li>{@code boolean}及其包装器类型:{@code false};</li> * <li>{@code int}、{@code long}、{@code float}、{@code double}、{@code short}、{@code byte}等基本数据类型 * 及其包装类型:{@code 0};</li> * <li>{@code char}及其包装器类型:{@code '\u0000'};</li> * <li>String类型:{@code ""};</li> * <li>集合类型:空集合;</li> * <li>数组类型:空数组;</li> * <li>映射类型:空映射;</li> * <li>其它类型:{@code null}。</li> * </ol> */ @Data @NoArgsConstructor @AllArgsConstructor @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class})}) public class EmptyCollectionArgInterceptor implements Interceptor { private final static String CLASS_METHOD_DELIMITER = "(?<=[A-Z][a-zA-Z0-9]{1,65535})\\.(?=[a-z][a-zA-Z0-9]*$)"; // 需要检查的mapper方法名字及其参数 private final static Map<String, ArgAndReturnValue> NEED_CHECK = new ConcurrentHashMap<>(); // 不需要检查的mapper方法名字 private final static Set<String> NOT_CHECK = new ConcurrentSkipListSet<>(); // 各种mapper方法返回值类型对应的零值 private final static Map<Class<?>, Object> zeroValues = Map.ofEntries( Map.entry(byte.class, (byte) 0), Map.entry(Byte.class, (byte) 0), Map.entry(short.class, (short) 0), Map.entry(Short.class, (short) 0), Map.entry(int.class, 0), Map.entry(Integer.class, 0), Map.entry(long.class, 0L), Map.entry(Long.class, 0L), Map.entry(float.class, 0.0f), Map.entry(Float.class, 0.0f), Map.entry(double.class, 0.0d), Map.entry(Double.class, 0.0d), Map.entry(boolean.class, false), Map.entry(Boolean.class, false), Map.entry(char.class, '\u0000'), Map.entry(Character.class, '\u0000'), Map.entry(String.class, ""), Map.entry(Map.class, Collections.emptyMap()) ); // 此过滤器是否应用到所有mapper方法。如果为true,则对所有方法都检查,否则只对有@EmptyReturnZero注解的方法检查 private boolean ignoreAnnotation = false; @Override public Object intercept(@NotNull Invocation invocation) throws Throwable { // 获得Executor方法的实参数组,第一个参数是MappedStatement对象,第二个参数是mapper方法的参数 final Object[] executorMethodArgs = invocation.getArgs(); // mapper方法实参 Object mapperMethodArgs = executorMethodArgs[1]; MappedStatement mappedStatement = (MappedStatement) executorMethodArgs[0]; // mapper方法id:就是在XXMapper.xml的CRUD元素中写的id,而且在该id前加上了对应mapper接口的全限定类名 String mapperMethodId = mappedStatement.getId(); SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); // 确定mapper方法执行的sql语句类型 boolean isUpdate = sqlCommandType == SqlCommandType.UPDATE || sqlCommandType == SqlCommandType.INSERT || sqlCommandType == SqlCommandType.DELETE; // 通过mapperMethodId判断该mapper方法是否有集合参数 if (requireCheck(mapperMethodId)) { // 如果该mapper方法有需要检查的集合参数 // 而mapperMethodArgs为null,显然传入该mapper方法的实参为null,这时应该返回零值 ArgAndReturnValue argAndReturnValue = NEED_CHECK.get(mappedStatement.getId()); Class<?> returnType = argAndReturnValue.returnType(); if (mapperMethodArgs == null) { return zero(isUpdate, returnType); } // 如果mapperMethodArgs不为null,那么它一定是Map类型的参数 @SuppressWarnings("unchecked") Map<String, ?> argMap = (Map<String, ?>) mapperMethodArgs; for (String requiredNotEmptyArg : argAndReturnValue.args()) { // 从argMap取出所有集合类型的实参,检查它是否为null或是否为空。如果是,则返回零值 Object arg = argMap.get(requiredNotEmptyArg); if (arg == null || ((Collection<?>) arg).isEmpty()) { return zero(isUpdate, returnType); } } } // 如果上述检查没有问题,则让mapper方法正常执行 return invocation.proceed(); } /** * 检查mapper方法是否有需要检查集合或数组类型参数。<br /> */ private boolean requireCheck(String mapperMethodId) throws ClassNotFoundException { // 如果该方法名字存在于无需检查方法集合中,说明该方法无需检查,返回false if (NOT_CHECK.contains(mapperMethodId)) { return false; } // 如果该方法名字存在于需要检查方法Map中,说明该方法需要检查,返回true if (NEED_CHECK.containsKey(mapperMethodId)) { return true; } // 如果上述两个Map中不包含该方法,则需要通过反射判断该方法是否需要检查 String[] classAndMethodName = mapperMethodId.split(CLASS_METHOD_DELIMITER, 2); Class<?> mapperClass = Class.forName(classAndMethodName[0]); Method targetMethod = ReflectUtil.getPublicMethods( mapperClass, method -> !method.isDefault() && method.getName().equals(classAndMethodName[1]) ).get(0); // 是否忽略形参上的@EmptyReturnZero注解,直接将所有集合参数加入检查 boolean ignoreParamAnnotation = this.ignoreAnnotation || mapperClass.isAnnotationPresent(EmptyReturnZero.class) || targetMethod.isAnnotationPresent(EmptyReturnZero.class); // 检查目标方法是否有要检查的集合参数 Set<String> collectionArgNames = new HashSet<>(); for (Parameter parameter : targetMethod.getParameters()) { Class<?> parameterType = parameter.getType(); if ((parameterType.isArray() || Collection.class.isAssignableFrom(parameterType)) && (ignoreParamAnnotation || parameter.isAnnotationPresent(EmptyReturnZero.class))) { Param param = parameter.getDeclaredAnnotation(Param.class); String parameterName = param != null ? param.value() : parameter.getName(); collectionArgNames.add(parameterName); } } if (collectionArgNames.isEmpty()) { // 如果collectionArgNames为空,说明该方法没有集合参数,不需要检查,返回false // 同时将该方法名字存入无需检查方法集合中 NOT_CHECK.add(mapperMethodId); return false; } else { // 如果该collectionArgNames不为空,说明该方法有集合参数需要检查,返回true // 同时将该方法名字存入需要检查方法Map中 collectionArgNames = Collections.unmodifiableSet(collectionArgNames); NEED_CHECK.put(mapperMethodId, new ArgAndReturnValue(collectionArgNames, targetMethod.getReturnType())); return true; } } /** * 当mapper方法的集合参数为空时,用于确定Invocation.proceed()方法返回的零值。 */ private @NotNull Object zero(boolean isUpdate, @NotNull Class<?> clazz) { // 如果mapper方法执行的是update、insert、delete语句,则零值是0。 if (isUpdate) { return 0; } // 如果mapper方法执行的是select语句,则零值应当存储在列表中。 ArrayList<Object> list = new ArrayList<>(); if (clazz.isArray() || Collection.class.isAssignableFrom(clazz) || !zeroValues.containsKey(clazz)) { // 如果返回值类型是数组或集合,或者返回值类型不是规定的类型,则返回空列表 return list; } else if (Map.class.isAssignableFrom(clazz)) { // 如果返回值类型是Map,则返回一个包含一个空Map的列表 list.add(zeroValues.get(Map.class)); } else { // 如果返回值类型属于事先定义的零值,则返回一个包含一个零值对象的列表 list.add(zeroValues.get(clazz)); } return list; } } /** * 用于封装mapper方法的集合参数和返回值类型。 * @param args 集合参数名称集合 * @param returnType mapper方法返回值类型 */ record ArgAndReturnValue(Set<String> args, Class<?> returnType) { }
要使该拦截器生效,需要在mybatis-config.xml中配置该拦截器,在mybatis-config.xml中添加如下内容即可:
<plugins> <plugin interceptor="demo.persistence.mybatis.interceptor.EmptyCollectionArgsInterceptor" /> </plugins>

浙公网安备 33010602011771号