Mybatis:解決呼叫帶有集合型別形參的mapper方法時,集合引數為空或null的問題
阿新 • • 發佈:2021-10-03
使用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>