mybatis代理機制講解

問題描述

在使用Mybatis開發中,或者和Spring整合中,在Dao層中的Mapper接口與xml中的sql對應着,在service中直接調用Dao中的方法就能夠直接訪問sql。以下所示:html

/**
 * interface 層的代碼
 */
public interface ArticleMapper {
    Article selectByPrimaryKey(Integer id);
}

 

在xml中,咱們的sql語句這樣定義:java

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 3 <mapper namespace="cn.com.gkmeteor.article.provider.mapper.ArticleMapper"> <!-- interface 與 sql文件 關聯語句 -->
 4   ...
 5 
 6   <!-- xml中對應的sql -->
 7   <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
 8     select 
 9     <include refid="Base_Column_List" />
10     from tb_article
11     where id = #{id,jdbcType=INTEGER}
12   </select>
13  
14   ...
15 </mapper>

 

咱們在業務代碼中,以下調用的方式就能夠從數據庫中獲取到數據:sql

@Resource
private ArticleMapper articleMapper;

...

public Article getArticleById(Integer id) {
    return articleMapper.selectByPrimaryKey(id);
}

 

若是仔細研究就會發現,ArticleMapper 接口並無實現類,咱們看到xml代碼中的關聯語句,知道知道mapper和sql是有關聯的,可是也只知道這麼多了。問題是,執行mapper中的方法後爲何就能夠執行xml中定義的sql呢?本文對這個問題進行回答。數據庫

mybatis執行sql的完整流程

在MyBatis框架下中調用一個sql時候,會經歷下面這個完整的流程:segmentfault

// 解析配置文件,生成配置
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

// 根據配置,構建一個SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);  

// 獲得一個真正可用的SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();

// 從SqlSession獲取interface的代理
ArticleMapper articleMapperProxy = sqlSession.getMapper(ArticleMapper.class);

// 執行代理類中的方法
Article article = articleMapperProxy.selectByPrimaryKey(123);

// 如下省略對 article 的操做

 

咱們這裏不討論mybatis解析配置文件和加載configuration的過程,也就是上面的 build 方法。這裏面一樣有些學問,在這裏咱們簡單認爲經過 build,咱們獲得了 sqlSessionFactory 對象,裏面包含 configuration 對象。緩存

在這篇文章中,咱們只關注mybatis初始化完成以後,mybatis框架到底作了哪些事情來讓咱們自如地執行sql。咱們先來了解上面代碼中重要的三個概念。mybatis

 

SqlSessionFactoryBuilderapp

這是SqlSessionFactory的構造器,其中最重要的是build方法,經過build方法能夠構建出 SqlSessionFactory的實例。框架

SqlSessionFactoryide

SqlSessionFactory實例被 build 方法建立出來之後,在應用的運行期間一直存在。它包含了不少重要信息,這些信息在以後的調用過程當中會反覆使用。讓咱們來看一下:

# sqlSessionFactory 中的重要信息

sqlSessionFactory
    configuration
        environment        # 裏面有 dataSource 信息
        mapperRegistry
            config         # 裏面有配置信息
            knownMappers   # 裏面有全部的 mapper,讓咱們記住它,它將是後面篇章中的主角
        mappedStatements   # 裏面有全部 mapper 的全部方法
        resultMaps         # 裏面有全部 xml 中的全部 resultMap
        sqlFragments       # 裏面有全部的 sql 片斷

 

sqlSessionFactory最重要的是 openSession 方法,返回了一個 SqlSession 實例。

SqlSession

一個請求線程會有一個SqlSession實例。該實例貫穿了數據庫鏈接的生命週期,是訪問數據庫的惟一渠道。sqlSession 中有 selectList,selectOne 等全部的sql增刪改查數據庫的方法,最終對數據庫的操做都落在 sqlSession 上。

sqlSession的getMapper流程

有了上面三個重點類的基本知識之後,咱們知道對數據庫的操做都落在 sqlSession 上。能夠先把結論告訴你,sqlSession.getMapper以後,獲取到的實際上是 configuration 對象中的 mapper的代理類。那麼,具體的過程是怎樣的?

詳細流程

咱們跟蹤進去看sqlSession.getMapper之後到底發生了什麼。

public class DefaultSqlSession implements SqlSession {

    private final Configuration configuration;
    private final Executor executor;

    private final boolean autoCommit;
    private boolean dirty;
    private List<Cursor<?>> cursorList;

    public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
        this.configuration = configuration;
        this.executor = executor;
        this.dirty = false;
        this.autoCommit = autoCommit;
    }

    public DefaultSqlSession(Configuration configuration, Executor executor) {
        this(configuration, executor, false);
    }

    ...
   
    // 重點方法
    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.<T>getMapper(type, this);
    }

}

 

經過調用DefaultSqlSession的getMapper方法而且傳入一個類型對象獲取,底層調用的是配置對象configuration的getMapper方法,configuration對象是咱們在加載DefaultSqlSessionFactory時傳入的。

而後咱們再來看下這個配置對象的getMapper,傳入的是【類型對象】和【自身 (DefaultSqlSession) 】。(這裏說的類型對象,就是咱們平時寫的DAO層接口,裏面是一些數據庫操做的接口方法。)

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

 

咱們看到這個configuration的getMapper方法裏調用的是mapperRegistry的getMapper方法,參數依然是類型對象和sqlSession。咱們要先來看下這個 MapperRegistry 是什麼。

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

    public MapperRegistry(Configuration config) {
        this.config = config;
    }
    ....
}

 

從這裏咱們能夠知道其實這個MapperRegistry就是保持了一個Configuration對象和一個HashMap,而這個HashMap的key是類型對象,value是MapperProxyFactory。咱們這裏先無論MapperProxyFactory是什麼東西,咱們如今只須要知道MapperRegistry是這麼一個東西就能夠了。這裏有人會問MapperRegistry對象是怎麼來的,這裏是在初始化Configuration對象時初始化了這個MapperRegistry對象的,具體代碼能夠看configuration對象的初始化。

 

接下來咱們繼續看下這個MapperRegistry的getMapper方法。

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

 

這裏咱們能夠看到從knownMappers中獲取key爲類型對象的MapperProxyFactory對象。而後調用MapperProxyFactory對象的newInstance方法返回,newInstance方法傳入sqlSession對象。到這裏咱們可能看不出什麼端倪,那咱們就繼續往下看這個newInstance方法作的什麼事情吧。

public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();

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

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

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

    protected T newInstance(MapperProxy<T> mapperProxy) {
        // 新建一個mapperProxy的代理類
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
    }

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

 

這裏咱們能夠看到MapperProxyFactory直接new了一個MapperProxy對象,而後調用另一重載的newInstance方法傳入MapperProxy對象。這裏咱們能夠看出一些東西了,經過調用Proxy.newProxyInstance動態代理了咱們的mapperProxy對象!這裏的mapperInterface即咱們的dao層(持久層)接口的類型對象。

因此得出總結:咱們經過sqlSesssion.getMapper(clazz)獲得的Mapper對象是一個mapperProxy的代理類!這點印證告終論中說的getMapper其實獲得的是mapper的方法的代理。

因爲代理,我每次在本身的程序裏執行 mapper 中的方法,都會走 MapperProxy中的invoke方法。咱們來看看 MapperProxy 的 invoke方法中。

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;
   }
    ....
}

 

MapperProxy必然實現了InvocationHandler接口。因此當咱們調用咱們的持久層接口的方法時,必然就會調用到這個MapperProxy對象的invoke方法。這屬於JDK的代理的知識。因此接下來咱們進入這個方法看看具體mybatis爲咱們作了什麼。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
        // 若是是toString, equals之類的方法,直接執行
        try {
            return method.invoke(this, args);
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    } else {
        // 從緩存的 MapperMethod 中拿,若是沒有,就新建一個,而且放入緩存對象
        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        // mapperMethod 執行
        return mapperMethod.execute(this.sqlSession, args);
    }
}

 

看看 cachedMapperMethod 方法:

private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);
    if (mapperMethod == null) {
        mapperMethod = new MapperMethod(
            this.mapperInterface, method, this.sqlSession.getConfiguration());
        this.methodCache.put(method, mapperMethod);
    }

    return mapperMethod;
}

 

從代碼中咱們能夠看到前面作了一個判斷,這個判斷主要是遇到像toString方法或者equals方法時,能夠正常調用。而後咱們能夠看到它調用cachedMapperMethod返回MapperMethod對象,接着就執行這個MapperMethod對象的execute方法。這個cachedMapperMethod方法主要是能緩存咱們使用過的一些mapperMethod對象,方便下次使用。

MapperMethod

mybatis的動態代理機制的重點來了!像其餘全部的類同樣,MapperMethod也有實例化的過程和重要的execute方法。

MapperMethod實例化過程

MapperMethod對象主要是獲取mapper中的某個方法對應的sql命令和執行相應SQL操做等的處理,很是重要。經過 mapperMethod,mapper中的方法就與 xml 中定義的sql語句結合起來了。

public class MapperMethod {
    private final MapperMethod.SqlCommand command;
    private final MapperMethod.MethodSignature method;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        // 獲取 SqlCommand 類
        // 其中主要記錄了mapper中的method方法的名稱與sql執行類型(INSERT仍是SELECT)的映射
        // 方便以後在 execute 的時候根據sql執行類型來執行對應的 switch case 邏輯
        this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
        
        // 獲取 MethodSignature 類,其中記錄了method方法的返回類型,以及其餘一些必要的、和sql執行相關的信息
        // 這些信息有不少,大部分是從configuration中處理獲得的
        // 如何處理獲得這些信息,以及如何使用這些信息,屬於另外一個知識範疇了
        // 在這裏咱們能夠簡單地理解爲獲取了method方法的返回類型等信息,在以後 execute的時候要使用的
        this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
    }
    ....
}

 

MapperMethod execute方法

來看一下 mapperMethod 對象的 execute方法:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object param;
    Object result;
    switch(this.command.getType()) {
    case INSERT:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
        break;
    case UPDATE:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
        break;
    case DELETE:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
        break;
    case SELECT:
        if (this.method.returnsVoid() && this.method.hasResultHandler()) {
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (this.method.returnsMany()) {
            result = this.executeForMany(sqlSession, args);
        } else if (this.method.returnsMap()) {
            result = this.executeForMap(sqlSession, args);
        } else if (this.method.returnsCursor()) {
            result = this.executeForCursor(sqlSession, args);
        } else {
            param = this.method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(this.command.getName(), param);
        }
        break;
    case FLUSH:
        result = sqlSession.flushStatements();
        break;
    default:
        throw new BindingException("Unknown execution method for: " + this.command.getName());
    }

    if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
        throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
    } else {
        return result;
    }
}

 

咱們能夠清晰的看到,這裏針對數據庫的增刪改查作了對應的操做,這裏咱們能夠看下查詢操做。咱們能夠看到這裏針對方法的不一樣返回值做了不一樣的處理,咱們以其中一種狀況來舉例:

// SELECT語句,當全部狀況都不知足時,執行下面的邏輯
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);

這裏咱們能夠看到它將方法參數類型轉換成數據庫層面上的參數類型,最後調用sqlSession對象的selectOne方法執行。因此咱們看到最後仍是回到sqlSession對象上來,符合前面所說的sqlSession是mybatis提供的與數據庫交互的惟一對象。

接下來咱們看下這個selectOne方法作了什麼事,這裏咱們看的是defaultSqlSession的selectOne方法。

public <T> T selectOne(String statement, Object parameter) {
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
        return list.get(0);
    } else if (list.size() > 1) {
        throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
        return null;
    }
}

 

咱們看到它調用selectList方法,經過取返回值的第一個值做爲結果返回。那麼咱們來看下這個selectList方法。

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) {
    List var5;
    try {
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception var9) {
        throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
    } finally {
        ErrorContext.instance().reset();
    }

    return var5;
}

 

咱們能夠看到這裏調用了executor的query方法,咱們再進入到query裏看看。這裏咱們看的是BaseExecutor的query方法,先不看緩存的Executor中的query方法。注意看getBoundSql這個方法:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // getBoundSql方法很是重要
    // 做用是根據傳入的sql參數,加上configuration和mappedStatement中的信息,組裝一個 BoundSql 給你
    // 具體怎麼組裝的,又有一段複雜的邏輯,其中也牽涉到了緩存技術,在這裏不展開研究
    // 在這裏只要簡單的認爲咱們獲得了 mappedStatement 對應的 BoundSql
    // 裏面有sql語句和sql參數,是一個完整的sql了,終於立刻能夠去數據庫執行一把了
    BoundSql boundSql = ms.getBoundSql(parameter);
    
    CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
    return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

...

public <E> List<E> query( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (this.closed) {
        throw new ExecutorException("Executor was closed.");
    } else {
        if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
            this.clearLocalCache();
        }

        List list;
        try {
            ++this.queryStack;
            list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
            if (list != null) {
                this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);  // 重點方法
            }
        } finally {
            --this.queryStack;
        }

        if (this.queryStack == 0) {
            Iterator i$ = this.deferredLoads.iterator();

            while(i$.hasNext()) {
                BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();
                deferredLoad.load();
            }

            this.deferredLoads.clear();
            if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                this.clearLocalCache();
            }
        }

        return list;
    }
}

 

boundSql中到底有些什麼,咱們來看看:

-- boundSql 中的sql語句以下
SELECT id, job_name, job_group_name, trigger_name, trigger_group_name, cron, status, create_time, update_time, operate_id, operate_name FROM cron_quartz_record WHERE job_group_name = ?

 

回到代碼跟蹤,進入queryFromDatabase這個重點方法:

private <E> List<E> queryFromDatabase(
        MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, 
        CacheKey key, BoundSql boundSql) throws SQLException {
    this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

    List list;
    try {
        list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);  // 重點方法
    } finally {
        this.localCache.removeObject(key);
    }

    this.localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        this.localOutputParameterCache.putObject(key, parameter);
    }

    return list;
}

 

咱們看到有個一個方法doQuery,進入方法看看作了什麼。點進去後咱們發現是抽象方法,咱們選擇simpleExecutor子類查看實現。

public <E> List<E> doQuery( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;

    List var9;
    try {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        stmt = this.prepareStatement(handler, ms.getStatementLog());
        var9 = handler.query(stmt, resultHandler);
    } finally {
        this.closeStatement(stmt);
    }

    return var9;
}

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Connection connection = this.getConnection(statementLog);
    Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}

 

咱們看到了熟悉的 statement 和 prepareStatement 方法,這很像 JDBC 中的 preparedStatement。咱們也看到了 query 方法,這很像 JDBC 中的 executeQuery。咱們知道,代碼跟蹤的旅程快要結束了。

咱們能夠看到經過configuration對象的newStatementHandler方法構建了一個StatementHandler,而後在調用prepareStatement方法中獲取鏈接對象,經過StatementHandler獲得Statement對象。另外咱們注意到在獲取了Statement對象後調用了parameterize方法。若是繼續跟蹤下去,咱們能夠發現會調用ParameterHandler對象的setParameters方法去處理咱們的參數。因此這裏的prepareStatement方法主要使用了StatementHandler和ParameterHandler對象幫助咱們處理語句集和參數的處理。最後還調用了StatementHandler的query方法,咱們繼續跟蹤下去。

這裏咱們進入到PreparedStatementHandler這個handler查看代碼。

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    c ps = (PreparedStatement)statement;
    ps.execute();  // 操做數據庫
    return this.resultSetHandler.handleResultSets(ps);
}

看到這裏,咱們終於找到了操做數據庫的地方了,就是ps.execute()這句代碼。底層咱們能夠發現就是原始的JDBC!而後將這個執行後的PreparedStatement交給resultSetHandler處理結果集,最後返回咱們須要的結果集。

 

總結

咱們總結一下mybatis的動態代理機制:

sqlSession是惟一訪問數據庫的渠道,經過sqlSession.getMapper方法,獲得到是mapper的代理類,已經不是咱們在預先寫好的mapper接口了。當咱們執行mapper中的方法,在代理類中的執行邏輯以下:

  1. if 判斷
  2. else if 判斷
  3. cachedMapperMethod 重點方法
  4. mapperMethod.execute 重點方法,實際執行的方法

在mapperMethod對象中,經過getBoundSql方法,早早的爲你準備好了mapper中的方法實際對應的sql語句,並且是能夠被執行的preparedStatement。這樣一來,咱們在業務代碼中執行mapper中的方法,實際上就是去數據庫中執行對應的sql語句了。

參考資料

  • https://segmentfault.com/a/1190000015117926
  • http://www.mybatis.org/mybatis-3/zh/getting-started.html

 

創做時間:05/22/2019 21:23

相關文章
相關標籤/搜索