Mybatis技術原理理——總體流程理解

前言:2018年,是最雜亂的一年!因此你看個人博客,是否是很空!html

 

網上有不少關於Mybatis原理介紹的博文,這裏介紹兩篇我我的很推薦的博文  Mybatis3.4.x技術內幕 MyBaits源碼分析!讓我找到了學習的入口,固然另外你必需要看的官方文檔 MyBatis學習。那麼有了這些知識,就讓咱們愉快的吃雞之路吧!mysql

一:你首先得知道的知識點。

1.1 JDBCsql

  在我的看來, Mybatis的核心就是對SQL語句的管理!那麼在JAVA環境下,對SQL的管理或者其餘任何的實現,確定是離不開JAVA的數據庫操做接口,包括Connrction接口、Statement接口、PreparadStatement接口以及ResultSet接口等等的屬性,你能夠先經過JDBC來操做一次或者更屢次的數據庫。這個就很少作贅述了!數據庫

1.2 動態代理apache

  不知道你最初有沒有和我同樣,有這樣的疑問。Mybatis的mapper層明明就是一個接口,都沒有實現類!即便是在TMapper.xml中作了映射,也沒有看到任何關於接口的實現類,那他是怎麼被實例化的呢,又是怎麼實現方法的功能的呢?動態代理會告訴你答案!緩存

先來看看jdk動態代理的一個小例子:
創建一個實體session

public class TestDemo {
    private Integer id;
    private String name;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

     創建一個mappermybatis

public interface TestDemoMapper {
    TestDemo getById(Integer id);
}

  創建一個Mapper的代理類,須要繼承一個 InvocationHandler 便可app

public class DemoMapperProxy<T> implements InvocationHandler {

    @SuppressWarnings("unchecked")
    public T newInstance(Class<T> clz) {
        return (T)Proxy.newProxyInstance(clz.getClassLoader(), new Class[] { clz }, this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TestDemo testDemo = new TestDemo();
        testDemo.setId(1);
        testDemo.setName("proxyName");
        return testDemo;
    }
}

測試類ide

public static void main(String[] args) {
        DemoMapperProxy<TestDemoMapper> demoMapperProxy = new DemoMapperProxy<TestDemoMapper>();
        TestDemoMapper testDemoMapper = demoMapperProxy.newInstance(TestDemoMapper.class);
        TestDemo byId = testDemoMapper.getById(1);
        System.out.println(byId);
    }

  結果:TestDemo{id=1, name='proxyName'},

  因此這就應該大體可以說明Mybatis中的mapper是怎麼工做的了吧!

二:關於mapper的總體流程理解

2.1 先來看看官方網站的一個例子吧

在官方文檔中的 Mybatis 3 的入門介紹中,介紹了怎麼配置一個mybatis的測試類

mybatis-config.xml 中的一個簡單配置:

<configuration>
    <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://172.20.139.237:3306/rubber-fruit?useUnicode=true&amp;characterEncoding=utf-8&amp;autoReconnect=true" />
                <property name="username" value="user123" />
                <property name="password" value="u123" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="UserMapper.xml" />
    </mappers>
</configuration>

  而後在寫一個測試類:

  public static void main(String[] args) throws Exception {

        String resouse = "mybatis-config.xml";
        InputStream stream = Resources.getResourceAsStream(resouse);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User byId = mapper.getById(1);
        sqlSession.close();
        System.out.println(byId);
    }

  其實這個邏輯很簡單,首先是拿到mabatis-config.xml 中的配置對象,生成SqlSessionFactory,而後經過sqlSession拿到mapper,經過動態代理完成方法的執行,並返回結果!那麼咱們就能夠簡單的拆分紅兩個部分,第一:配置文件的初始化,第二:sql的執行,後面的文章也會更具這個大模塊來更具細化的講解

  其實上面的第四行代碼 能夠更換成config對象的,可能看的更清楚一些:

     String resouse = "mybatis-config.xml";
        InputStream stream = Resources.getResourceAsStream(resouse);
        XMLConfigBuilder configBuilder = new XMLConfigBuilder(stream,null,null);
        Configuration parse = configBuilder.parse();
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(parse);

  其實Mybatis的前期初始化的一個重要的對象就是 Configuration對象,後面會慢慢揭開它的面紗!

2.2 總體流程圖

經過上面的這個測試方法,咱們經過源碼流程,大體能夠拆分出這樣的一個圖:

 其實從這個圖中咱們已經能夠看出Mybatis劃分先兩個部分:

 1:初始化過程

  首先是經過XMLConfigBuilder解析 mybatis-config.xml 對象來進行初始化,拿到Configuration對象,而後SqlSessionFactoryBuilder經過初始化的Configuration對象,生成SqlSession,SqlSession中調用getMapper對象方法,其實也就是Configuration中的getMapper方法 經過MapperProxy代理對象生成一個mapper。到這裏位置就是一個簡單的初初始化思路了!固然Mybatis的初始化遠遠不止這些。

2:mybatia中執行一個sql的完整流程

  從圖中也是可以很較清晰的看出這個執行流程的。由於Mapper是一個接口,因此不能直接實例化的!那麼MapperProxy的做用,就是經過JDK的動態代理,來間接的對mapper進行實例化,不瞭解的能夠看上面的1.2中的例子。那麼咱們能夠看看org.apache.ibatis.binding.MapperProxy源碼中的對象方法:

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      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);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  } 

經過代理的對象方法方法,拿到了一個MapperMethod對象,並對mapperMethod對象進行來緩存。爲啥要緩存呢?這個時候能夠看看MapperMethod對象是什麼?org.apache.ibatis.binding.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);
  }

     MapperMethod中的SqlCommand對象中的兩個私有方法,    

public static class SqlCommand {
    private final String name;
    private final SqlCommandType type;
}
public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

    而MapperMethod中的MethodSignature對象呢?

   private final boolean returnsMany;
    private final boolean returnsMap;
    private final boolean returnsVoid;
    private final boolean returnsCursor;
    private final Class<?> returnType;
    private final String mapKey;
    private final Integer resultHandlerIndex;
    private final Integer rowBoundsIndex;
    private final ParamNameResolver paramNameResolver;

   因此這些很清晰了,MapperMethod對象是把mapper中的sql進行了封裝,獲取了sql的執行類型和返回值。

  MapperMethod中的另一個重要的方法:execute() 則擔任這路由的重要任務:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      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;
  }

  經過執行的sql類型 選取不通的sqlSession進行執行。其實能夠看出,轉了一圈,最後的最後仍是落在了SqlSession當中。而SqlSession又把這個重要的操做交個了執行器Executor。最後又到了StatementHandler來負責執行最後的sql,ResultSetHandler放回執行的結果。

  2.3 記一個知識點

  看到這裏,忽然是想問一下一個問題的,Mapper的方法是否支持重載呢?

  答案是不能的!mybatis是使用package+Mapper+method 全名稱做爲Key值 去xml中尋找一個惟一sql來執行的那麼,重載方法時將致使矛盾。對於Mapper接口,Mybatis禁止方法重載。經過代碼斷點能夠看到。

  org.apache.ibatis.session.Configuration對象中有一個方法,addMappedStatement()

protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

 /**中間部分省略掉**

 public void addMappedStatement(MappedStatement ms) {

    mappedStatements.put(ms.getId(), ms);
  }

    

  而這個地方put的ms.getId 就是經過mapper的配置文件組合成的一個惟一的key值。那咱們在看看StrictMap 中的put方法源碼,那麼就一目瞭然了

 @SuppressWarnings("unchecked")
    public V put(String key, V value) {
      if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key);
      }
      if (key.contains(".")) {
        final String shortKey = getShortName(key);
        if (super.get(shortKey) == null) {
          super.put(shortKey, value);
        } else {
          super.put(shortKey, (V) new Ambiguity(shortKey));
        }
      }
      return super.put(key, value);
    }

  若是有相同的key的,那麼會拋出異常,這也就是爲啥mapper不能重載的緣由!

相關文章
相關標籤/搜索