淺析MyBatis的動態代理原理

pjmike_pj發表於2019-05-09

原文部落格地址: pjmike的部落格

前言

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

MyBatis的核心元件及應用

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

  • 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:

  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動態原理的實現。

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配置檔案是如何讀取的,對映器檔案是如何讀取的?

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()方法中:

 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中

對映器Mapper檔案的解析

  //解析mapper對映器檔案
  mapperElement(root.evalNode("mappers"));
複製程式碼

該方法是對全域性配置檔案中mappers屬性的解析,走進去:

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的操作並沒有仔細闡述,有興趣的同學可以查閱相關資料。

參考資料 & 鳴謝

相關文章