1. 程式人生 > >Mybatis中@Param註解詳細使用和原理分析

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;
    }
}

結合一個實際的例子來說明下:
mapper介面
假如呼叫方法時傳入的實際引數是username=逍遙,password=123,那麼在呼叫時我們可以發現介面中的方法超過1個的所以方法會執行最後的else程式碼
執行和資料分析

作者:樑開權,叩丁狼教育講師。