Mybatis中@Param註解詳細使用和原理分析
對於目前市場上火爆的持久層框架MyBatis相信大家在工作中肯定是用得很多,但是你對其mapper介面代理物件和其方法上的@Param註解又瞭解多少呢?
廢話不多說,接來下就給大家來分析下
MapperRegistry
MapperRegistry是用於註冊和快取當前框架中所有的mapper介面
public class MapperRegistry { //框架的配置物件 private final Configuration config; //存放已經註冊的mapper介面和其程式碼物件的工廠 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();; public <T> T getMapper(Class<T> type, SqlSession sqlSession) { //從map中取出已經註冊的mapper介面代理物件的工廠 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); //建立並返回mapper介面的程式碼物件 return mapperProxyFactory.newInstance(sqlSession); } ...其他方法省略... }
上述程式碼是SqlSession中呼叫getMapper(mapper介面)方法的底層,也就是說我們呼叫getMapper方法其底層是呼叫了MapperRegistry物件的getMapper方法,那麼我們繼續往下研究那就要去看MapperProxyFactory中的newInstance方法啦
MapperProxyFactory
MapperProxyFactory是mapper的代理工廠專門用於建立mapper介面的代理物件
public class MapperProxyFactory<T> { //代理的介面 private final Class<T> mapperInterface; //快取代理的方法 private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); public T newInstance(SqlSession sqlSession) { //建立一個MapperProxy物件,其內部封裝了方法拿到介面的程式碼物件 final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); //呼叫方法返回介面的代理物件 } protected T newInstance(MapperProxy<T> mapperProxy) { //底層使用JDK動態建立介面的代理物件 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } ...其他方法省略... }
通過上述程式碼我們最終看到了mapper介面的代理物件其底層使用的是JDK的動態代理技術建立並返回代理物件,最終介面中所有的方法都會由mapperProxy物件中的invoke方法來實現,當前類的欄位MapperMethod物件至關總要,後期代理物件的invoke所執行的方法,最終是會呼叫到其物件的execute方法
MapperProxy
MapperProxy類實現了JDK動態代理的InvocationHandler介面,最後將由該物件的invoke方法來完成真正的方法執行
public class MapperProxy<T> implements InvocationHandler, Serializable { private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //判斷是否是Object類中的方法,如equals/hashCode/toString等方法 if (Object.class.equals(method.getDeclaringClass())) { try { //原封不動呼叫Object類中方法作為代理物件方法的預設實現 return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } //介面中其他的方法則呼叫之前方法緩衝器的處理策略 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } ...其他方法省略... }
MapperMethod
MapperMethod類是處理mapper介面中方法的真正處理器,該類內部的execute明確了代理的方法引數要怎麼處理,查詢得到的結果怎麼封裝然後返回
public class MapperMethod {
//對執行的SQL標籤的封裝,包含SQL型別和任務的完整ID等資訊
private final SqlCommand command;
//代理方法的簽名,其內部封裝了一系列操作,如方法多引數時對@Param註解的處理
private final MethodSignature method;
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) { //針對DML/DQL操作執行
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
}
MethodSignature類中的convertArgsToSqlCommandParam方法處理介面中的引數怎麼轉換成能用於執行SQL任務的引數,以下是底層執行的getNamedParams方法
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
//介面方法沒有引數,返回null
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
//介面方法上只有1個引數則返回唯一的那個物件
return args[names.firstKey()];
} else {
//介面方法上不止一個引數,就會把所有的引數封裝到Map中然後返回
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//把@Param註解中的value作為key,對應變數的值作為value
param.put(entry.getValue(), args[entry.getKey()]);
//自動生成key (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
//為了不覆蓋@Param註解設定的key
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
結合一個實際的例子來說明下:
假如呼叫方法時傳入的實際引數是username=逍遙,password=123,那麼在呼叫時我們可以發現介面中的方法超過1個的所以方法會執行最後的else程式碼
作者:樑開權,叩丁狼教育講師。