mybatis代理机制讲解

Posted by Julius on May 22, 2019

问题描述

在使用Mybatis开发中,或者和Spring整合中,在Dao层中的Mapper接口与xml中的sql对应着,在service中直接调用Dao中的方法就可以直接访问sql。如下所示:

interface层的代码:

public interface ArticleMapper {
    Article selectByPrimaryKey(Integer id);
}

在xml中,我们的sql语句这样定义:

<!-- xml中对应的sql -->
...
 <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from tb_article
    where id = #{id,jdbcType=INTEGER}
  </select>
...

我们在业务代码中,如下调用的方式就可以从数据库中获取到数据:

@Resource
private ArticleMapper articleMapper;

...

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

如果仔细研究,就会发现,ArticleMapper 接口并没有实现类,我们只是模糊地知道mapper和sql是有关联的。但是具体来说,执行mapper中的方法后为什么就可以执行xml中定义的sql呢?本文对这个问题进行回答。

mybatis执行sql的完整流程

在MyBatis框架下中调用一个sql时候,会经历下面这个完整的流程:

// 解析配置文件,生成配置
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的过程,只关注mybatis初始化开始以后,mybatis框架到底做了哪些事情。我们先来了解上面代码中重要的三个概念。

SqlSessionFactoryBuilder

这是SqlSessionFactory的构造器,其中最重要的是build方法,通过build方法可以构建出 SqlSessionFactory的实例。

SqlSessionFactory

SqlSessionFactory实例被创建出来以后,在应用的运行期间一直存在。它包含了很多重要信息,这些信息在之后的调用过程中会反复使用。让我们来看一下:

# 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之后,获取到的其实是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对象的,代码大家可以去看,为了避免混乱,保持贴出来的代码是一条线走下来的,这里就不贴出来了。

接下来我们继续看下这个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

This work is licensed under a CC A-S 4.0 International License.