天天看點

Mybatis核心源碼-通過sqlSession擷取映射器代理工廠

作者:王朋code

先看一段代碼,熟悉Mybatis的小夥伴都知道我們可以通過getMapper方法擷取對應的接口,然後調用接口的方法便可以執行對應的MappedStatement中的sql,那麼對應的原理是什麼呢?我們今天來看一下!

Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
BusinessRequestMonitorLogMapper monitorLogMapper = sqlSession.getMapper(BusinessRequestMonitorLogMapper.class);
List<String> strings = monitorLogMapper.selectByIdAndNameAndMethodMap(map);
System.out.println(strings);           

mybatis-config.xml檔案,我們隻研究最下面的mappers

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- plugins在配置檔案中的位置必須符合要求,否則會報錯,順序如下: properties?, settings?, typeAliases?,
        typeHandlers?, objectFactory?,objectWrapperFactory?, plugins?, environments?,
        databaseIdProvider?, mappers? -->
    <properties resource="dataSource.properties"/>
    <!-- 全局參數 -->
    <settings>
        <!-- 使全局的映射器啟用或禁用緩存。 -->
        <setting name="cacheEnabled" value="true"/>
        <!-- 全局啟用或禁用延遲加載。當禁用時,所有關聯對象都會即時加載。 -->
        <!--
                <setting name="lazyLoadingEnabled" value="false"/>
        -->
        <!-- 當啟用時,有延遲加載屬性的對象在被調用時将會完全加載任意屬性。否則,每種屬性将會按需要加載。 -->
        <setting name="aggressiveLazyLoading" value="true"/>
        <!-- 是否允許單條sql 傳回多個資料集 (取決于驅動的相容性) default:true -->
        <setting name="multipleResultSetsEnabled" value="true"/>
        <!-- 是否可以使用列的别名 (取決于驅動的相容性) default:true -->
        <setting name="useColumnLabel" value="true"/>
        <!-- 允許JDBC 生成主鍵。需要驅動器支援。如果設為了true,這個設定将強制使用被生成的主鍵,有一些驅動器不相容不過仍然可以執行。 default:false -->
        <setting name="useGeneratedKeys" value="true"/>
        <!-- 指定 MyBatis 如何自動映射 資料基表的列 NONE:不隐射 PARTIAL:部分 FULL:全部 -->
        <setting name="autoMappingBehavior" value="PARTIAL"/>
        <!-- 這是預設的執行類型 (SIMPLE: 簡單; REUSE: 執行器可能重複使用prepared statements語句;BATCH:
            執行器可以重複執行語句和批量更新) -->
        <setting name="defaultExecutorType" value="SIMPLE"/>
        <!-- 使用駝峰命名法轉換字段。 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 設定本地緩存範圍 session:就會有資料的共享 statement:語句範圍 (這樣就不會有資料的共享 ) defalut:session -->
        <setting name="localCacheScope" value="SESSION"/>
        <!-- 設定但JDBC類型為空時,某些驅動程式 要指定值,default:OTHER,插入空值時不需要指定類型 -->
        <setting name="jdbcTypeForNull" value="NULL"/>
        <!-- 開啟sql列印 -->
        <!--		value取值問題檢視筆記:mybatis整合日志架構-->
<!--
        <setting name="logImpl" value="STDOUT_LOGGING" />
-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <environments default="business">
    </environments>
    <mappers>
        <!--        用于掃mapper.xml的,xml和接口名字一緻-->
<!--        <package name="org.apache.test"/>-->
        <!--每一個Mapper.xml都需要在Mybatis核心配置檔案中注冊!-->
<!--
        <mapper resource="org/apache/test/BusinessRequestMonitorLogMapper.xml"/>
-->
        <!--每一個Mapper.xml都需要在Mybatis核心配置檔案中注冊!-->
        <mapper class="org.apache.test.BusinessRequestMonitorLogMapper"/>
    </mappers>
</configuration>           

先看SqlSessionFactoryBuilder這個類,這個類的作用是我們今天可以簡單就是初始化了所有的我們所設定的資訊并放到了Configuration這個全局對象中

這裡的加載非常重要,我們後續的getMapper要用到裡面的資訊

//它使用了一個參照了XML文檔或更特定的SqlMapConfig.xml檔案的Reader執行個體。
  //可選的參數是environment和properties。Environment決定加載哪種環境(開發環境/生産環境),包括資料源和事務管理器。
  //如果使用properties,那麼就會加載那些properties(屬性配置檔案),那些屬性可以用${propName}文法形式多次用在配置檔案中。和Spring很像,一個思想?
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        //委托XMLConfigBuilder來解析mybatis-config.xml檔案,并建構
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      //解析mybatis-config.xml并把對應的資訊轉換成Configuration這個全局對象
      //重點看parser.parse()
      return build(parser.parse());
    } catch (Exception e) {
      ...
    }
  }           
//解析配置
  public Configuration parse() {
    //如果已經解析過了,報錯;對應xml配置檔案,每一個XMLConfigBuilder都隻能解析一次
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //根節點是configuration,然後逐層開始解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
//我們隻看今天研究的
private void parseConfiguration(XNode root) {
    try {
      //分步驟解析
      ...
      //10.映射器,解析所有的mapper接口,并擷取MappedStatement
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }           
//直接看29行即可
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //10.4自動掃描包下所有映射器
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            //10.1使用類路徑
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //映射器比較複雜,調用XMLMapperBuilder
            //注意在for循環裡每個mapper都重新new一個XMLMapperBuilder,來解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //10.2使用絕對url路徑
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //映射器比較複雜,調用XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            //因為我們在配置檔案中隻用到了class标簽,是以目前方法會進入到此判斷中
            //10.3使用java類名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            //直接把這個映射加入配置,這裡直接調用了MapperRegistry#addMapper方法
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }           
//MapperRegistry中的方法
public <T> void addMapper(Class<T> type) {
    //mapper必須是接口!才會添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重複添加了,報錯
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        //這裡非常重要!!!将對應的全限定接口名作為key,同時建立的一個代理工廠對象
        //我們在這裡進行了put,後面會用過get方法拿到對應的mapper代理對象!!
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        //這個方法篇幅太長了,我在這裡說一下做的事情
        //解析目前的接口全限定類名,通過反射擷取接口中所有的方法名,然後拼接為全限定接口名+"."+方法名
        //然後存到mappedStatements這個map中,key=全限定接口名+"."+方法名,value=statement對象(裡面有目前這個xml的所有資訊)
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果加載過程中出現異常需要再将這個mapper從mybatis中删除,這種方式比較醜陋吧,難道是不得已而為之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }           

代理工廠對象

public class MapperProxyFactory<T> {

  //這裡就是全限定接口名,我們上面傳進來的
  private final Class<T> mapperInterface;
  private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    //用JDK自帶的動态代理生成映射器
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  //後面我們使用getMapper方法的時候就是調用的這個方法!!!很重要
  public T newInstance(SqlSession sqlSession) {
    //MapperProxy是真正幹活的地方!!!
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}           
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //代理以後,所有Mapper的方法調用時,都會調用這個invoke方法
    //并不是任何一個方法都需要執行調用代理對象進行執行,如果這個方法是Object中通用的方法(toString、hashCode等)無需執行
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //這裡優化了,去緩存中找MapperMethod
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //執行
    return mapperMethod.execute(sqlSession, args);
  }

  //去緩存中找MapperMethod
  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      //找不到才去new,這裡建立的時候會根據全限定接口名+方法名去查詢mappedStatements這個map
      //這個map上面我們也說了,存的value是xml中的标簽對象
      //是以這裡就可以拿到對應的xml中的全部資訊,當然也包括sql了
      //然後上面執行mapperMethod.execute方法,就會去查詢對應的sql了
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

}           

ok,上面的準備工作完成了,我們接下來來看getMapper做了什麼事!

BusinessRequestMonitorLogMapper monitorLogMapper = sqlSession.getMapper(BusinessRequestMonitorLogMapper.class);
//實際執行的是下面的代碼
@Override
  public <T> T getMapper(Class<T> type) {
    //最後會去調用MapperRegistry.getMapper
    return configuration.<T>getMapper(type, this);
  }
//繼續
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }
//繼續
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  //這裡根據全限定接口名拿到了對應工廠的mapperProxyFactory,這個我們在上面說到了
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      //這裡調用了newInstance,實際是建立了MapperProxy代理對象并傳回
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }           

List<String> strings = monitorLogMapper.selectByIdAndNameAndMethodMap(map);

當我們執行這行代碼時,實際就是調用了MapperProxy中的invoke方法(PS:不熟悉代理的小夥伴先溫習一下java的代理吧~),invoke方法中執行的代碼在上面已經解釋了,通過代理,我們就可以執行了對應的sql并擷取資料了~

簡單總結一下:

SqlSessionFactoryBuilder類build的時候擷取到所有的mapper接口并放到了knownMappers中,key=接口名,value=MapperProxyFactory;同時解析對應的xml标簽,然後放到了mappedStatements這個map中,key=全限定接口名+"."+方法名,value=statement對象(裡面有目前這個xml的所有資訊);資料準備完畢。

開始執行:BusinessRequestMonitorLogMapper monitorLogMapper = sqlSession.getMapper(BusinessRequestMonitorLogMapper.class);

執行getMapper接口,通過knownMappers.get()擷取對應的MapperProxyFactory,然後MapperProxyFactory通過newInstance方法中的MapperProxy方法建立了代理對象并傳回。

開始執行:List<BusinessRequestMonitorLog> strings = monitorLogMapper.selectById("1");

實際執行的是代理對象的MapperProxy#invoke方法,而invoke方法則會根據全限定接口名+方法名從mappedStatements這個map中get到statement對象,然後執行statement對象中的sql查詢出資料并傳回。