Mybatis:解决调用带有集合类型形参的mapper方法时,集合参数为空或null的问题

2023-02-12,,,,

此文章有问题,待修改!

使用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)。下面是一个示例。

  1 package demo.persistence.mybatis.interceptor;
2
3 import org.apache.ibatis.cache.CacheKey;
4 import org.apache.ibatis.executor.Executor;
5 import org.apache.ibatis.mapping.BoundSql;
6 import org.apache.ibatis.mapping.MappedStatement;
7 import org.apache.ibatis.plugin.Interceptor;
8 import org.apache.ibatis.plugin.Intercepts;
9 import org.apache.ibatis.plugin.Invocation;
10 import org.apache.ibatis.plugin.Signature;
11 import org.apache.ibatis.session.ResultHandler;
12 import org.apache.ibatis.session.RowBounds;
13 import org.jetbrains.annotations.NotNull;
14 import org.jetbrains.annotations.Nullable;
15
16 import java.lang.reflect.Method;
17 import java.lang.reflect.Parameter;
18 import java.util.*;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.ConcurrentSkipListSet;
21
22 import static org.springframework.util.StringUtils.quote;
23 import static demo.consts.IntegerType.isIntegerType;
24 import static demo.consts.RegularExpression.CLASS_METHOD_DELIMITER;
25
26 /**
27 * 此Mybatis拦截器处理mapper方法中集合类型参数为null或为空的情况。如果集合参数为null或为空,则mapper方法的返回值
28 * 为空集合、0或null,具体返回值视方法本身的返回值而定。<br />
29 * 注意:① 有的mapper方法将其所需参数放入Map中,此拦截器不处理此类情况;
30 * ② 有时,向mapper方法传递null参数被视为错误,但此拦截器将其当做正常情况处理
31 */
32 // Interceptors注解中写要拦截的的方法签名,但是此处要拦截的方法不是mapper类中的方法,而是Executor类中的方法。
33 // 可能Mybatis在执行mapper方法时是通过Executor类中的方法来执行的吧。
34 @Intercepts({
35 @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
36 RowBounds.class, ResultHandler.class}),
37 @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
38 RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
39 @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
40 public class EmptyCollectionArgsInterceptor implements Interceptor {
41
42 // 缓存具有集合参数的mapper方法名字以及集合参数的名字,执行这些方法时需要检查它的方法参数是否为null或为空
43 private final static Map<String, Set<String>> REQUIRE_CHECK = new ConcurrentHashMap<>();
44 // 缓存没有集合参数的mapper方法名字,执行这些方法时不需要检查它的方法参数
45 private final static Set<String> NOT_CHECK = new ConcurrentSkipListSet<>();
46
47 @Override
48 public Object intercept(@NotNull Invocation invocation) throws Throwable {
49 // 获得Executor方法的实参数组,第一个参数是MappedStatement对象,第二个参数是mapper方法的参数
50 final Object[] executorMethodArgs = invocation.getArgs();
51 MappedStatement mappedStatement = (MappedStatement) executorMethodArgs[0];
52 // 关于mapperMethodArgs变量的说明:
53 // (1) 如果mapper方法只有一个参数
54 // ① 如果该参数实际为null,则mapperMethodArgs值为null;
55 // ② 如果该参数为Map类型且不为null,则mapperMethodArgs的值就是该Map参数的值
56 // ③ 如果该参数为List类型且不为null,则mapperMethodArgs的类型为MapperMethod.ParamMap(继承于HashMap),
57 // Map中有三对键值,它们的值都是该List类型实参,键则分别为"collection"、"list"和List形参的名字
58 // ④ 如果该参数为Set类型且不为null,则mapperMethodArgs的类型为MapperMethod.ParamMap(继承于HashMap),
59 // Map中有两对键值对,它们的值都是该List类型实参,键则分别为"collection"和Set形参的名字
60 // (2) 如果mapper方法有多个参数,无论实参是否为null,mapperMethodArgs的类型始终为MapperMethod.ParamMap,
61 // Map中的键值对就是mapper方法的形参名字与实参值的对,此时集合类型参数没有别名
62 Object mapperMethodArgs = executorMethodArgs[1];
63 // mapper方法id,就是在XXMapper.xml的CRUD元素中写的id,而且在该id前加上了对应mapper接口的全限定类名
64 final String mapperMethodId = mappedStatement.getId();
65
66 // 通过mapperMethodId判断该mapper方法是否有集合参数。如果mapperMethodId尚未缓存,requireCheck()方法会将其缓存。
67 if (requireCheck(mapperMethodId)) {
68 // 如果该mapper方法有集合参数
69 // 而mapperMethodArgs为null,显然传入该mapper方法的实参为null,这时应该返回默认值
70 if (mapperMethodArgs == null) {
71 return getDefaultReturnValue(invocation);
72 }
73 // 如果mapperMethodArgs不为null,那么它一定是Map类型的参数
74 Map<String, ?> argMap = (Map<String, ?>) mapperMethodArgs;
75 final Set<String> requiredNotEmptyArgs = REQUIRE_CHECK.get(mapperMethodId);
76 for (String requiredNotEmptyArg : requiredNotEmptyArgs) {
77 // 从argMap取出所有集合类型的实参,检查它是否为null或是否为空。如果是,则返回默认值
78 final Object arg = argMap.get(requiredNotEmptyArg);
79 if (arg == null || ((Collection<?>) arg).isEmpty()) {
80 return getDefaultReturnValue(invocation);
81 }
82 }
83 }
84
85 // 如果上述检查没有问题,则让mapper方法正常执行
86 return invocation.proceed();
87 }
88
89 /**
90 * 当mapper方法出错时返回的默认值。
91 * @return 如果Executor方法返回List类型对象,则此方法返回空List;如果Executor方法返回数字,则此方法返回0;其余情况返回null。
92 */
93 private @Nullable Object getDefaultReturnValue(@NotNull Invocation invocation) {
94 Class<?> returnType = invocation.getMethod().getReturnType();
95 if (returnType.equals(List.class)) {
96 return Collections.emptyList();
97 // isIntegerType()方法判断Class对象是不是整数Class,自己写
98 } else if (isIntegerType(returnType)) {
99 return 0;
100 }
101 return null;
102 }
103
104 /**
105 * 检查mapper方法是否有集合类型参数。<br />
106 * 注意:此方法有副作用。
107 * @param mapperMethodId mapper方法。由mapper类的全限定名和方法名字组成。可由MappedStatement.getId()方法获取。
108 * @throws ClassNotFoundException 如果未能找到指定的mapper方法的类
109 * @throws NoSuchMethodException 如果未能找到指定的mapper方法
110 */
111 private static boolean requireCheck(String mapperMethodId) throws ClassNotFoundException, NoSuchMethodException {
112 // 如果该方法名字存在于无需检查方法集合中,说明该方法无需检查,返回false
113 if (NOT_CHECK.contains(mapperMethodId)) {
114 return false;
115 }
116 // 如果该方法名字存在于需要检查方法Map中,说明该方法需要检查,返回true
117 if (REQUIRE_CHECK.containsKey(mapperMethodId)) {
118 return true;
119 }
120
121 // 如果方法名字不在缓存中,则进行以下操作:
122 // 从完整方法名中分割出全限定类名和方法名
123 // CLASS_METHOD_DELIMITER是类和方法分隔符,自己写吧
124 final String[] fullClassAndMethod = mapperMethodId.split(CLASS_METHOD_DELIMITER, 2);
125 final String fullQualifiedName = fullClassAndMethod[0];
126 final String methodName = fullClassAndMethod[1];
127 Method targetMethod = null;
128 int paramCount = -1;
129 // 遍历指定对应类的全部方法,以找到目标方法
130 for (Method method : Class.forName(fullQualifiedName).getMethods()) {
131 // 个人习惯是在mapper接口中定义几个重载的默认方法,这些默认方法的参数数量比同名的非默认方法的参数数量少,
132 // 所以参数数量最多的方法就是要拦截并检查的方法
133 if (method.getName().equals(methodName) && method.getParameterCount() > paramCount) {
134 targetMethod = method;
135 paramCount = method.getParameterCount();
136 }
137 }
138
139 if (targetMethod == null) {
140 throw new NoSuchMethodException("Can't find method " + quote(mapperMethodId));
141 }
142 // 检查目标方法是否有集合参数。如果有,则将该集合参数的名字放入collectionArgNames中。
143 Set<String> collectionArgNames = new HashSet<>();
144 for (Parameter parameter : targetMethod.getParameters()) {
145 if (Collection.class.isAssignableFrom(parameter.getType())) {
146 collectionArgNames.add(parameter.getName());
147 }
148 }
149 if (collectionArgNames.isEmpty()) {
150 // 如果collectionArgNames为空,说明该方法没有集合参数,不需要检查,返回false
151 // 同时将该方法名字存入无需检查方法集合中
152 NOT_CHECK.add(mapperMethodId);
153 return false;
154 } else {
155 // 如果该collectionArgNames不为空,说明该方法有集合参数,需要检查,返回true
156 // 同时将该方法名字存入需要检查方法Map中
157 REQUIRE_CHECK.put(mapperMethodId, collectionArgNames);
158 return true;
159 }
160 }
161
162 }

要使该拦截器生效,需要在mybatis-config.xml中配置该拦截器,在mybatis-config.xml中添加如下内容即可:

    <plugins>
<plugin interceptor="demo.persistence.mybatis.interceptor.EmptyCollectionArgsInterceptor" />
</plugins>

Mybatis:解决调用带有集合类型形参的mapper方法时,集合参数为空或null的问题的相关教程结束。

《Mybatis:解决调用带有集合类型形参的mapper方法时,集合参数为空或null的问题.doc》

下载本文的Word格式文档,以方便收藏与打印。