淺析MyBatis的動態代理原理

前言

一直以來都在使用MyBatis作持久化框架,也知道當咱們定義XXXMapper接口類並利用它來作CRUD操做時,Mybatis是利用了動態代理的技術幫咱們生成代理類。那麼動態代理內部的實現細節究竟是怎麼的呀?XXXMapper.java類和XXXMapper.xml究竟是如何關聯起來的呀?本篇文章就來詳細剖析下MyBatis的動態代理的具體實現機制。java

MyBatis的核心組件及應用

在詳細探究MyBatis中動態代理機制以前,先來補充一下基礎知識,認識一下MyBatis的核心組件。mysql

  • SqlSessionFactoryBuilder(構造器): 它能夠從XML、註解或者手動配置Java代碼來建立SqlSessionFactory。
  • SqlSessionFactory: 用於建立SqlSession (會話) 的工廠
  • SqlSession: SqlSession是Mybatis最核心的類,能夠用於執行語句、提交或回滾事務以及獲取映射器Mapper的接口
  • SQL Mapper: 它是由一個Java接口和XML文件(或註解)構成的,須要給出對應的SQL和映射規則,它負責發送SQL去執行,並返回結果
注意: 如今咱們使用Mybatis,通常都是和Spring框架整合在一塊兒使用,這種狀況下,SqlSession將被Spring框架所建立,因此每每不須要咱們使用SqlSessionFactoryBuilder或者SqlSessionFactory去建立SqlSession

下面展現一下如何使用MyBatis的這些組件,或者如何快速使用MyBatis:git

  1. 數據庫表
CREATE TABLE  user(
  id int,
  name VARCHAR(255) not NULL ,
  age int ,
  PRIMARY KEY (id)
)ENGINE =INNODB DEFAULT CHARSET=utf8;
  1. 聲明一個User類
@Data
public class User {
    private int id;
    private int age;
    private String name;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
  1. 定義一個全局配置文件mybatis-config.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>
    <!--enviroments表示環境配置,能夠配置成開發環境(development)、測試環境(test)、生產環境(production)等-->
    <environments default="development">
        <environment id="development">
            <!--transactionManager: 事務管理器,屬性type只有兩個取值:JDBC和MANAGED-->
            <transactionManager type="MANAGED" />
            <!--dataSource: 數據源配置-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <property name="username" value="root" />
                <property name="password" value="root" />
            </dataSource>
        </environment>
    </environments>
    <!--mappers文件路徑配置-->
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>
  1. UserMapper接口
public interface UserMapper {
    User selectById(int id);
}
  1. UserMapper文件
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace屬性表示命令空間,不一樣xml映射文件namespace必須不一樣-->
<mapper namespace="com.pjmike.mybatis.UserMapper">
    <select id="selectById" parameterType="int"
            resultType="com.pjmike.mybatis.User">
             SELECT id,name,age FROM user where id= #{id}
       </select>
</mapper>
  1. 測試類
public class MybatisTest {
    private static SqlSessionFactory sqlSessionFactory;
    static {
        try {
            sqlSessionFactory = new SqlSessionFactoryBuilder()
                    .build(Resources.getResourceAsStream("mybatis-config.xml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user = userMapper.selectById(1);
            System.out.println("User : " + user);
        }
    }
}
// 結果:
User : User{id=1, age=21, name='pjmike'}

上面的例子簡單的展現瞭如何使用MyBatis,與此同時,我也將用這個例子來進一步探究MyBatis動態原理的實現。github

MyBatis動態代理的實現

public static void main(String[] args) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// <1>
        User user = userMapper.selectById(1);
        System.out.println("User : " + user);
    }
}

在前面的例子中,咱們使用sqlSession.getMapper()方法獲取UserMapper對象,實際上這裏咱們是獲取了UserMapper接口的代理類,而後再由代理類執行方法。那麼這個代理類是如何生成的呢?在探究動態代理類如何生成以前,咱們先來看下SqlSessionFactory工廠的建立過程作了哪些準備工做,好比說mybatis-config配置文件是如何讀取的,映射器文件是如何讀取的?sql

mybatis全局配置文件解析

private static SqlSessionFactory sqlSessionFactory;
static {
    try {
        sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream("mybatis-config.xml"));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

咱們使用new SqlSessionFactoryBuilder().build()的方式建立SqlSessionFactory工廠,走進build方法數據庫

public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

對於mybatis的全局配置文件的解析,相關解析代碼位於XMLConfigBuilder的parse()方法中:api

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //解析全局配置文件
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析mapper映射器文件
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

從parseConfiguration方法的源代碼中很容易就能夠看出它對mybatis全局配置文件中各個元素屬性的解析。固然最終解析後返回一個Configuration對象,Configuration是一個很重要的類,它包含了Mybatis的全部配置信息,它是經過XMLConfigBuilder取錢構建的,Mybatis經過XMLConfigBuilder讀取mybatis-config.xml中配置的信息,而後將這些信息保存到Configuration中mybatis

映射器Mapper文件的解析

//解析mapper映射器文件
  mapperElement(root.evalNode("mappers"));

該方法是對全局配置文件中mappers屬性的解析,走進去:app

mapper xml

mapperParser.parse()方法就是XMLMapperBuilder對Mapper映射器文件進行解析,可與XMLConfigBuilder進行類比框架

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper")); //解析映射文件的根節點mapper元素
      configuration.addLoadedResource(resource);  
      bindMapperForNamespace(); //重點方法,這個方法內部會根據namespace屬性值,生成動態代理類
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
  • configurationElement(XNode context)方法

該方法主要用於將mapper文件中的元素信息,好比insertselect這等信息解析到MappedStatement對象,並保存到Configuration類中的mappedStatements屬性中,以便於後續動態代理類執行CRUD操做時可以獲取真正的Sql語句信息

configurationElement

buildStatementFromContext方法就用於解析insert、select這類元素信息,並將其封裝成MappedStatement對象,具體的實現細節這裏就不細說了。

  • bindMapperForNamespace()方法

該方法是核心方法,它會根據mapper文件中的namespace屬性值,爲接口生成動態代理類,這就來到了咱們的主題內容——動態代理類是如何生成的。

動態代理類的生成

bindMapperForNamespace方法源碼以下所示:

private void bindMapperForNamespace() {
    //獲取mapper元素的namespace屬性值
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // 獲取namespace屬性值對應的Class對象
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //若是沒有這個類,則直接忽略,這是由於namespace屬性值只須要惟一便可,並不必定對應一個XXXMapper接口
        //沒有XXXMapper接口的時候,咱們能夠直接使用SqlSession來進行增刪改查
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          //若是namespace屬性值有對應的Java類,調用Configuration的addMapper方法,將其添加到MapperRegistry中
          configuration.addMapper(boundType);
        }
      }
    }
  }

這裏提到了Configuration的addMapper方法,實際上Configuration類裏面經過MapperRegistry對象維護了全部要生成動態代理類的XxxMapper接口信息,可見Configuration類確實是至關重要一類

public class Configuration {
    ...
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);
    ...
    public <T> void addMapper(Class<T> type) {
      mapperRegistry.addMapper(type);
    }
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      return mapperRegistry.getMapper(type, sqlSession);
    }
    ...
}

其中兩個重要的方法:getMapper()和addMapper()

  • getMapper(): 用於建立接口的動態類
  • addMapper(): mybatis在解析配置文件時,會將須要生成動態代理類的接口註冊到其中

1. Configuration#addMappper()

Configuration將addMapper方法委託給MapperRegistry的addMapper進行的,源碼以下:

public <T> void addMapper(Class<T> type) {
    // 這個class必須是一個接口,由於是使用JDK動態代理,因此須要是接口,不然不會針對其生成動態代理
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 生成一個MapperProxyFactory,用於以後生成動態代理類
        knownMappers.put(type, new MapperProxyFactory<>(type));
        //如下代碼片斷用於解析咱們定義的XxxMapper接口裏面使用的註解,這主要是處理不使用xml映射文件的狀況
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

MapperRegistry內部維護一個映射關係,每一個接口對應一個MapperProxyFactory(生成動態代理工廠類)

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

這樣便於在後面調用MapperRegistry的getMapper()時,直接從Map中獲取某個接口對應的動態代理工廠類,而後再利用工廠類針對其接口生成真正的動態代理類。

2. Configuration#getMapper()

Configuration的getMapper()方法內部就是調用MapperRegistry的getMapper()方法,源代碼以下:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    //根據Class對象獲取建立動態代理的工廠對象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 {
      //這裏能夠看到每次調用都會建立一個新的代理對象返回
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

從上面能夠看出,建立動態代理類的核心代碼就是在MapperProxyFactory.newInstance方法中,源碼以下:

protected T newInstance(MapperProxy<T> mapperProxy) {
    //這裏使用JDK動態代理,經過Proxy.newProxyInstance生成動態代理類
    // newProxyInstance的參數:類加載器、接口類、InvocationHandler接口實現類
    // 動態代理能夠將全部接口的調用重定向到調用處理器InvocationHandler,調用它的invoke方法
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
PS: 關於JDK動態代理的詳細介紹這裏就再也不細說了,有興趣的能夠參閱我以前寫的文章: 動態代理的原理及其應用

這裏的InvocationHandler接口的實現類是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 {
    
    try {
      //若是調用的是Object類中定義的方法,直接經過反射調用便可
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //調用XxxMapper接口自定義的方法,進行代理
    //首先將當前被調用的方法Method構形成一個MapperMethod對象,而後掉用其execute方法真正的開始執行。
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
  private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }
  ...
}

最終的執行邏輯在於MapperMethod類的execute方法,源碼以下:

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      //insert語句的處理邏輯
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      //update語句的處理邏輯
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      //delete語句的處理邏輯
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      //select語句的處理邏輯
      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);
          //調用sqlSession的selectOne方法
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        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;
  }
  ...
}

在MapperMethod中還有兩個內部類,SqlCommand和MethodSignature類,在execute方法中首先用switch case語句根據SqlCommand的getType()方法,判斷要執行的sql類型,好比INSET、UPDATE、DELETE、SELECT和FLUSH,而後分別調用SqlSession的增刪改查等方法。

慢着,說了這麼多,那麼這個getMapper()方法何時被調用呀?實際是一開始咱們調用SqlSession的getMapper()方法:

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  private final Executor executor;
  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
  }
  ...
}

因此getMapper方法的大體調用邏輯鏈是:
SqlSession#getMapper() ——> Configuration#getMapper() ——> MapperRegistry#getMapper() ——> MapperProxyFactory#newInstance() ——> Proxy#newProxyInstance()

還有一點咱們須要注意:咱們經過SqlSession的getMapper方法得到接口代理來進行CRUD操做,其底層仍是依靠的是SqlSession的使用方法

小結

根據上面的探究過程,簡單畫了一個邏輯圖(不必定準確):

Mybatis動態代理

本篇文章主要介紹了MyBatis的動態原理,回過頭來,咱們須要知道咱們使用UserMapper的動態代理類進行CRUD操做,本質上仍是經過SqlSession這個關鍵類執行增刪改查操做,可是對於SqlSession如何具體執行CRUD的操做並無仔細闡述,有興趣的同窗能夠查閱相關資料。

參考資料 & 鳴謝

相關文章
相關標籤/搜索