mybatis--動態代理實現


如果我們要使用MyBatis進行數據庫操作的話,大致要做兩件事情:

  1. 定義dao接口文件
    在dao接口中定義需要進行的數據庫操作方法。
  2. 創建映射文件
    當有了dao接口后,還需要為該接口創建映射文件。映射文件中定義了一系列SQL語句,這些SQL語句和dao接口一一對應。

MyBatis在初始化的時候會將映射文件與dao接口一一對應,並根據映射文件的內容為每個函數創建相應的數據庫操作能力。而我們作為MyBatis使用者,只需將dao接口注入給Service層使用即可。
那么MyBatis是如何根據映射文件為每個dao接口創建具體實現的?答案是——動態代理。

1、解析mapper文件

啟動時加載解析mapper的xml

如果不是集成spring的,會去讀取<mappers>節點,去加載mapper的xml配置

<?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>
    <settings>
        <setting name="cacheEnabled" value="true"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="multipleResultSetsEnabled" value="true"/>
        <setting name="useColumnLabel" value="true"/>
        <setting name="useGeneratedKeys" value="false"/>
        <setting name="defaultExecutorType" value="SIMPLE"/>
        <setting name="defaultStatementTimeout" value="2"/>
    </settings>
    <typeAliases>
        <typeAlias alias="CommentInfo" type="com.xixicat.domain.CommentInfo"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/demo"/>
                <property name="username" value="root"/>
                <property name="password" value=""/>
            </dataSource>
        </environment>
    </environments>
 <mappers> <mapper resource="com/xixicat/dao/CommentMapper.xml"/> </mappers>
</configuration>

如果是集成spring的,會去讀spring的sqlSessionFactory的xml配置中的mapperLocations,然后去解析mapper的xml

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 配置mybatis配置文件的位置 -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="typeAliasesPackage" value="com.xixicat.domain"/>
        <!-- 配置掃描Mapper XML的位置 -->
        <property name="mapperLocations" value="classpath:com/xixicat/dao/*.xml"/>
    </bean>

mapper節點解析過程

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

由上述代碼可知,解析mapper節點的解析是由XMLMapperBuilder類的parse()函數來完成的,下面我們就詳細看一下parse()函數。

public void parse() {
    // 若當前Mapper.xml尚未加載,則加載
    if (!configuration.isResourceLoaded(resource)) { 
      // 解析<mapper>節點
      configurationElement(parser.evalNode("/mapper"));
      // 將當前Mapper.xml標注為『已加載』(下回就不用再加載了)
      configuration.addLoadedResource(resource);
      // 【關鍵】將Mapper Class添加至Configuration中
      bindMapperForNamespace(); //綁定namespace
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

這個函數主要做了兩件事:

  1. 解析<mapper>節點,並將解析結果注冊進configuration中;
  2. 將當前映射文件所對應的DAO接口的Class對象注冊進configuration
    這一步極為關鍵!是為了給DAO接口創建代理對象,下文會詳細介紹。

下面再進入bindMapperForNamespace()函數,看一看它做了什么:

private void bindMapperForNamespace() {
    // 獲取當前映射文件對應的DAO接口的全限定名
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      // 將全限定名解析成Class對象
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // 將當前Mapper.xml標注為『已加載』(下回就不用再加載了)
          configuration.addLoadedResource("namespace:" + namespace);
          // 將DAO接口的Class對象注冊進configuration中
          configuration.addMapper(boundType);
        }
      }
    }
  }

這個函數主要做了兩件事:

  1. <mapper>節點上定義的namespace屬性(即:當前映射文件所對應的DAO接口的權限定名)解析成Class對象
  2. 將該Class對象存儲在configuration對象里的屬性MapperRegistry中,見下代碼
MapperRegistry mapperRegistry = new MapperRegistry(this)

public <T> void addMapper(Class<T> type) {
        this.mapperRegistry.addMapper(type);
    }

我們繼續進入MapperRegistry

public class MapperRegistry {
  private final Configuration config;
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
}

MapperRegistry有且僅有兩個屬性:ConfigurationknownMappers
其中,knownMappers的類型為Map<Class<?>, MapperProxyFactory<?>>,它是一個Map,key為dao接口的class對象,而value為該dao接口代理對象的工廠。那么這個代理對象工廠是什么,又怎么產生的呢?我們先來看一下MapperRegistryaddMapper()函數。

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 創建MapperProxyFactory對象,並put進knownMappers中
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

從這個函數可知,MapperProxyFactory是在這里創建,並put進knownMappers中的。
下面我們就來看一下MapperProxyFactory這個類究竟有些啥:

public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private final 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) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

這個類有三個重要成員:

  1. mapperInterface屬性
    這個屬性就是DAO接口的Class對象,當創建MapperProxyFactory對象的時候需要傳入
  2. methodCache屬性
    這個屬性用於存儲當前DAO接口中所有的方法。
  3. newInstance函數
    這個函數用於創建DAO接口的代理對象,它需要傳入一個MapperProxy對象作為參數。而MapperProxy類實現了InvocationHandler接口,由此可知它是動態代理中的處理類,所有對目標函數的調用請求都會先被這個處理類截獲,所以可以在這個處理類中添加目標函數調用前、調用后的邏輯。

2、DAO函數調用過程

當MyBatis初始化完畢后, configuration對象中存儲了所有DAO接口的Class對象和相應的 MapperProxyFactory對象(用於創建DAO接口的代理對象)。接下來,就到了使用DAO接口中函數的階段了。
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
    ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
    List<Product> productList = productMapper.selectProductList();
    for (Product product : productList) {
        System.out.printf(product.toString());
    }
} finally {
    sqlSession.close();
}

我們首先需要從sqlSessionFactory對象中創建一個SqlSession對象,然后調用sqlSession.getMapper(ProductMapper.class)來獲取代理對象。
我們先來看一下sqlSession.getMapper()是如何創建代理對象的?

  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }

sqlSession.getMapper()調用了configuration.getMapper(),那我們再看一下configuration.getMapper()

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

configuration.getMapper()又調用了mapperRegistry.getMapper(),那好,我們再深入看一下mapperRegistry.getMapper()

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

看到這里我們就恍然大悟了,原來它根據上游傳遞進來DAO接口的Class對象,從configuration中取出了該DAO接口對應的代理對象生成工廠:MapperProxyFactory
在有了這個工廠后,再通過newInstance函數創建該DAO接口的代理對象,並返回給上游。

OK,此時我們已經獲取了代理對象,接下來就可以使用這個代理對象調用相應的函數了。

SqlSession sqlSession = sqlSessionFactory.openSession();
try {
    ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
    List<Product> productList = productMapper.selectProductList();
} finally {
    sqlSession.close();
}

以上述代碼為例,當我們獲取到ProductMapper的代理對象后,我們調用了它的selectProductList()函數。

3、下面我們就來分析下代理函數調用過程。

當調用了代理對象的某一個代理函數后,這個調用請求首先會被發送給代理對象處理類MapperProxyinvoke()函數,這個類繼承了InvocationHandler:

//這里會攔截Mapper接口的所有方法 
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) { //如果是Object中定義的方法,直接執行。如toString(),hashCode()等
      try {
        return method.invoke(this, args);//
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);  //其他Mapper接口定義的方法交由mapperMethod來執行
    return mapperMethod.execute(sqlSession, args);
  }

這個方法中,首先會過濾object中的通用方法,遇到object方法會直接執行;

但是如果是非通用方法,現執行cachedMapperMethod(method):從當前代理對象處理類MapperProxymethodCache屬性中獲取method方法的詳細信息(即:MapperMethod對象)。如果methodCache中沒有就創建並加進去。

有了MapperMethod對象后執行它的execute()方法,該方法就會調用JDBC執行相應的SQL語句,並將結果返回給上游調用者。至此,代理對象函數的調用過程結束!

MapperMethod類是統管所有和數據庫打交道的方法。所以,不管你的dao層有多少方法,歸結起來的sql語句都有且僅有只有insert、delete、update、select,可以預料在MapperMethod的execute方法中首先判斷是何種sql語句。

 1   /**
 2    * 這個方法是對SqlSession的包裝,對應insert、delete、update、select四種操作
 3    */
 4 public Object execute(SqlSession sqlSession, Object[] args) {
 5     Object result;//返回結果
 6    //INSERT操作
 7     if (SqlCommandType.INSERT == command.getType()) {
 8       Object param = method.convertArgsToSqlCommandParam(args);
 9       //調用sqlSession的insert方法 
10       result = rowCountResult(sqlSession.insert(command.getName(), param));
11     } else if (SqlCommandType.UPDATE == command.getType()) {
12       //UPDATE操作 同上
13       Object param = method.convertArgsToSqlCommandParam(args);
14       result = rowCountResult(sqlSession.update(command.getName(), param));
15     } else if (SqlCommandType.DELETE == command.getType()) {
16       //DELETE操作 同上
17       Object param = method.convertArgsToSqlCommandParam(args);
18       result = rowCountResult(sqlSession.delete(command.getName(), param));
19     } else if (SqlCommandType.SELECT == command.getType()) {
20       //如果返回void 並且參數有resultHandler  ,則調用 void select(String statement, Object parameter, ResultHandler handler);方法  
21       if (method.returnsVoid() && method.hasResultHandler()) {
22         executeWithResultHandler(sqlSession, args);
23         result = null;
24       } else if (method.returnsMany()) {
25         //如果返回多行結果,executeForMany這個方法調用 <E> List<E> selectList(String statement, Object parameter);   
26         result = executeForMany(sqlSession, args);
27       } else if (method.returnsMap()) {
28         //如果返回類型是MAP 則調用executeForMap方法 
29         result = executeForMap(sqlSession, args);
30       } else {
31         //否則就是查詢單個對象
32         Object param = method.convertArgsToSqlCommandParam(args);
33         result = sqlSession.selectOne(command.getName(), param);
34       }
35     } else {
36         //接口方法沒有和sql命令綁定
37         throw new BindingException("Unknown execution method for: " + command.getName());
38     }
39     //如果返回值為空 並且方法返回值類型是基礎類型 並且不是void 則拋出異常  
40     if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
41       throw new BindingException("Mapper method '" + command.getName() 
42           + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
43     }
44     return result;
45   }

我們選取第26行中的executeForMany中的方法來解讀試試看。

 1 private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
 2     List<E> result;
 3     Object param = method.convertArgsToSqlCommandParam(args);
 4     if (method.hasRowBounds()) { //是否分頁查詢,RowBounds中有兩個數字,offset和limit
 5       RowBounds rowBounds = method.extractRowBounds(args);
 6       result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
 7     } else {
 8       result = sqlSession.<E>selectList(command.getName(), param);
 9     }
10     // issue #510 Collections & arrays support
11     if (!method.getReturnType().isAssignableFrom(result.getClass())) {
12       if (method.getReturnType().isArray()) {
13         return convertToArray(result);
14       } else {
15         return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
16       }
17     }
18     return result;
19   }

第6行和第8行代碼就是我們真正執行sql語句的地方,原來兜兜轉轉它又回到了sqlSession的方法中。DefaultSqlSession是SqlSession的實現類,所以我們重點關注DefaultSqlSession類。

  public <E> List<E> selectList(String statement) {
    return this.selectList(statement, null);
  }

  public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      return result;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

我們看到關於selectList有三個重載方法,最后調用的都是public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds)。在此方法中第一個參數為String類型且命名為statement,第二個參數為Object命名為parameter,回到MapperMethod的executeForMany方法,可以看到傳遞了什么參數進來,跟蹤command.getName()是怎么來的。

private final SqlCommand command;

看來是一個叫做SqlCommand的變量,進而我們可以發現這個SqlCommand是MapperMethod的靜態內部類。

 1 //org.apache.ibatis.binding.MapperMethod
 2   public static class SqlCommand {
 3 
 4     private final String name;
 5     private final SqlCommandType type;
 6 
 7     public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
 8       String statementName = mapperInterface.getName() + "." + method.getName();  //獲取接口+方法名
 9       MappedStatement ms = null;  //定義一個MappedStatement,這個MapperedStatement稍后介紹
10       if (configuration.hasStatement(statementName)) {  //從Configuration對象查找是否有這個方法的全限定名稱
11         ms = configuration.getMappedStatement(statementName);  //有,則根據方法的全限定名稱獲取MappedStatement實例
12       } else if (!mapperInterface.equals(method.getDeclaringClass())) { //如果沒有在Configuration對象中找到這個方法,則向上父類中獲取全限定方法名
13         String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
14         if (configuration.hasStatement(parentStatementName)) {  //在向上的父類查找是否有這個全限定方法名
15           ms = configuration.getMappedStatement(parentStatementName);   //有,則根據方法的全限定名稱獲取MappedStatement實例
16         }
17       }
18       if (ms == null) {
19         if(method.getAnnotation(Flush.class) != null){
20           name = null;
21           type = SqlCommandType.FLUSH;
22         } else {
23           throw new BindingException("Invalid bound statement (not found): " + statementName);
24         }
25       } else {
26         name = ms.getId();  //這個ms.getId,其實就是我們在mapper.xml配置文件中配置一條sql語句時開頭的<select id="……"……,即是接口的該方法名全限定名稱
27         type = ms.getSqlCommandType();  //顯然這是將sql是何種類型(insert、update、delete、select)賦給type
28         if (type == SqlCommandType.UNKNOWN) {
29           throw new BindingException("Unknown execution method for: " + name);
30         }
31       }
32     }
33 
34     public String getName() {
35       return name;
36     }
37 
38     public SqlCommandType getType() {
39       return type;
40     }
41   }

  大致對MapperMethod的解讀到此,再次回到DefaultSqlSession中,走到核心的這句

MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

在獲取到MappedStatement后,碰到了第一個SqlSession下的四大對象之一:Executor執行器。關於executor我們在下一篇文章中解析。

 
轉載鏈接:https://www.jianshu.com/p/46c6e56d9774
https://www.cnblogs.com/yulinfeng/p/6076052.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM