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>

 

posted @ 2021-10-03 15:56  Halloworlds  阅读(3150)  评论(0)    收藏  举报