mybatis是一种非常流行的ORM框架,可以通过一些灵活简单的配置,大大提升我们操作数据库的效率,当然,我觉得它如此受欢迎的原因更主要的是,它的源码设计的非常简单。接下来我们就来聊聊使用mybatis做一次数据库查询操作背后都经历了什么。首先我们先上一段非常简单的代码,这是原始的JDBC方式的数据库操作。// 1. 创建数据源DataSource dataSource = getDataSource();// 2. 创建数据库连接try (Connection conn = dataSource.getConnection()) {try { conn.setAutoCommit(false); // 3. 创建Statement PreparedStatement stat = conn.prepareStatement(“select * from std_addr where id=?”); stat.setLong(1, 123456L); // 4. 执行Statement,获取结果集 ResultSet resultSet = stat.executeQuery(); // 5. 处理结果集,这一步往往是非常复杂的 processResultSet(resultSet); // 6.1 成功提交,对于查询操作,步骤6是不需要的 conn.commit();} catch (Throwable throwable) {// 6.2 失败回滚 conn.rollback();}}下面这段是mybatis连接数据库以及做同样的查询操作的代码。DataSource dataSource = getDataSource();TransactionFactory txFactory = new JdbcTransactionFactory();Environment env = new Environment(“test”, txFactory, dataSource);Configuration conf = new Configuration(env);conf.setMapUnderscoreToCamelCase(true);conf.addMapper(AddressMapper.class);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(conf);try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {AddressMapper mapper = sqlSession.getMapper(AddressMapper.class);Address addr = mapper.getById(123456L);}这是mybatis的Mapper,也非常简单@Mapperpublic interface AddressMapper {String TABLE = “std_addr”;@Select(“select * from " + TABLE + " where id=#{id}")Address getById(long id);}从上面的代码可以看出,通过mybatis查询数据库需要以下几个步骤:准备运行环境Environment,即创建数据源和事务工厂创建核心配置对象Configuration,此对象包含mybatis的配置信息(xml或者注解方式配置)创建SqlSessionFactory,用于创建数据库会话SqlSession创建SqlSession进行数据库操作下面我们从源码逐步分析mybatis在一次select查询中这几个步骤的详细情况。准备运行环境EnvironmentEnvironment有两个核心属性,dataSource和transactionFactory,下面是源码public final class Environment {private final String id;private final TransactionFactory transactionFactory;private final DataSource dataSource;}其中,dataSource用来获取数据库连接,transactionFactory用来创建事务。我们详细看一下mybatis的JdbcTransactionFactory的源码,这里可以通过数据源或者数据库连接来创建JdbcTransaction。public class JdbcTransactionFactory implements TransactionFactory {public Transaction newTransaction(Connection conn) {return new JdbcTransaction(conn);}public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {return new JdbcTransaction(ds, level, autoCommit);}}我把JdbcTransaction的源码精简了一下,大概是这个样子的。这里实际上就是把JDBC的DataSource或者一个Connection托管给了mybatis的Transaction对象,由Transaction来管理事务的提交与回滚。public class JdbcTransaction implements Transaction {protected Connection connection;protected DataSource dataSource;protected TransactionIsolationLevel level;protected boolean autoCommmit;public Connection getConnection() throws SQLException {if (connection == null) { connection = dataSource.getConnection(); if (level != null) { connection.setTransactionIsolation(level.getLevel()); } if (connection.getAutoCommit() != autoCommmit) { connection.setAutoCommit(autoCommmit); }}return connection;}public void commit() throws SQLException {if (connection != null && !connection.getAutoCommit()) { connection.commit();}}public void rollback() throws SQLException {if (connection != null && !connection.getAutoCommit()) { connection.rollback();}}}到这里,运行环境Environment已经准备完毕,我们可以从Environment中获取DataSource或者创建一个新的Transaction,从而创建一个数据库连接。创建核心配置对象ConfigurationConfiguration类非常复杂,包含很多配置信息,我们优先关注以下核心属性public class Configuration {protected Environment environment;protected boolean cacheEnabled = true;protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;// 保存着所有Mapper的动态代理对象protected final MapperRegistry mapperRegistry;// 保存着所有类型处理器,处理Java类型和JDBC类型的转换protected final TypeHandlerRegistry typeHandlerRegistry;// 保存配置的Statement信息,可以是XML或注解protected final Map<String, MappedStatement> mappedStatements;// 保存二级缓存信息protected final Map<String, Cache> caches;// 保存配置的ResultMap信息protected final Map<String, ResultMap> resultMaps;}从SqlSessionFactory的build方法可以看出,mybatis提供了两种解析配置信息的方式,分别是XMLConfigBuilder和MapperAnnotationBuilder。解析配置的过程,其实就是填充上述Configuration核心属性的过程。// 根据XML构建InputStream xmlInputStream = Resources.getResourceAsStream(“xxx.xml”);SqlSessionFactory xmlSqlSessionFactory = new SqlSessionFactoryBuilder().build(xmlInputStream);// 根据注解构建Configuration configuration = new Configuration(environment);configuration.addMapper(AddressMapper.class);SqlSessionFactory annoSqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);TypeHandlerRegistry处理Java类型和JDBC类型的映射关系,从TypeHandler的接口定义可以看出,主要是用来为PreparedStatement设置参数和从结果集中获取结果的public interface TypeHandlerRegistry<T> {void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;T getResult(ResultSet rs, String columnName) throws SQLException;T getResult(ResultSet rs, int columnIndex) throws SQLException;T getResult(CallableStatement cs, int columnIndex) throws SQLException;}总而言之,Configuration对象包含了mybatis的Statement、ResultMap、Cache等核心配置,这些配置信息是后续执行SQL操作的关键。创建SqlSessionFactory我们提供new SqlSessionFactoryBuilder().build(conf)构建了一个DefaultSqlSessionFactory,这是默认的SqlSessionFactorypublic SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);}DefaultSqlSessionFactory的核心方法有两个,代码精简过后是下面这个样子的。其实都是一个套路,通过数据源或者连接创建一个事务(上面提到的TransactionFactory创建事务的两种方式),然后创建执行器Executor,最终组合成一个DefaultSqlSession,代表着一次数据库会话,相当于一个JDBC的连接周期。private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);final Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);}private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {boolean autoCommit;try {autoCommit = connection.getAutoCommit();} catch (SQLException e) {autoCommit = true;}final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);final Transaction tx = transactionFactory.newTransaction(connection);final Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);}下面这段代码是Configuration对象创建执行器Executor的过程,默认的情况下会创建SimpleExecutor,然后在包装一层用于二级缓存的CachingExecutor,很明显Executor的设计是一个典型的装饰者模式。public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;}创建SqlSession进行数据库操作进行一次数据库查询操作的步骤如下:通过DefaultSqlSessionFactory创建一个DefaultSqlSession对象SqlSession sqlSession = sqlSessionFactory.openSession(true);创建获取一个Mapper的代理对象AddressMapper mapper = sqlSession.getMapper(AddressMapper.class);DefaultSqlSession的getMapper方法参数是我们定义的Mapper接口的Class对象,最终是从Configuration对象的mapperRegistry注册表中获取这个Mapper的代理对象。下面是MapperRegistry的getMapper方法的核心代码,可见这里是通过MapperProxyFactory创建代理public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);return mapperProxyFactory.newInstance(sqlSession);}然后是MapperProxyFactory的newInstance方法,看上去是不是相当熟悉。很明显,这是一段JDK动态代理的代码,这里会返回Mapper接口的一个代理类实例。public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}调用代理对象的查询方法Address byId = mapper.getById(110114);这里实际上是调用到Mapper对应的MapperProxy,下面是MapperProxy的invoke方法的一部分。可见,这里针对我们调用的Mapper的抽象方法,创建了一个对应的代理方法MapperMethod。public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {final MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}我精简了MapperMethod的execute方法的代码,如下所示。其实最终动态代理为我们调用了SqlSession的select方法。public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case SELECT:Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);break;}return result;}接下来的关注点在SqlSessionSqlSession的selectOne方法最终是调用的selectList,这个方法也非常简单,入参statement其实就是我们定义的Mapper中被调用的方法的全名,本例中就是x.x.AddressMapper.getById,通过statement获取对应的MappedStatement,然后交由executor执行query操作。public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {MappedStatement ms = configuration.getMappedStatement(statement);return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);}前面我们提到过默认的执行器是SimpleExecutor再装饰一层CachingExecutor,下面看看CachingExecutor的query代码,在这个方法之前会先根据SQL和参数等信息创建一个缓存的CacheKey。下面这段代码也非常明了,如果配置了Mapper级别的二级缓存(默认是没有配置的),则优先从缓存中获取,否则将调用被装饰者也就是SimpleExecutor(其实是BaseExecutor)的query方法。public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {Cache cache = ms.getCache();// cache不为空,表示当前Mapper配置了二级缓存if (cache != null) {flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) { List<E> list = (List<E>) tcm.getObject(cache, key); // 缓存未命中,查库 if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); } return list;}}return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}BaseExecutor的query方法的核心代码如下所示,这里有个一级缓存,是开启的,默认的作用域是SqlSession级别的。如果一级缓存未命中,则调用queryFromDatabase方法从数据库中查询。public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}return list;}然后将调用子类SimpleExecutor的doQuery方法,核心代码如下。public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog());return handler.<E>query(stmt, resultHandler);}通过源码发现Configuration创建的是一个RoutingStatementHandler,然后根据MappedStatement的statementType属性创建一个具体的StatementHandler(三种STATEMENT、PREPARED或者CALLABLE)。终于出现了一些熟悉的东西了,这不就是JDBC的三种Statement吗。我们选择其中的PreparedStatementHandler来看一看源码,这里就很清晰了,就是调用了JDBC的PreparedStatement的execute方法,然后将结果交由ResultHandler处理。public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute();return resultSetHandler.<E> handleResultSets(ps);}从上面doQuery的代码可以看出,执行的Statement是由prepareStatement方法创建的,可以看出这里是调用了StatementHandler的prepare方法创建Statement,实际上是通过MappedStatement的SQL、参数等信息,创建了一个预编译的PrepareStatement。private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog);stmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt);return stmt;}最终,这个PrepareStatement的执行结果ResultSet,会交由DefaultResultSetHandler来处理,然后根据配置中的类型、Results、返回值等信息,生成对应的实体对象。到这里我们就分析完了mybatis做一次查询操作所经历的全部流程。当然,这里面还有一些细节没有提到,比如说二级缓存、参数和结果集的解析等,这些具体的内容可能会在后续的mybatis源码解析文章中详细描述。说到最后给大家免费分享一波福利吧!我自己收集了一些Java资料,里面就包涵了一些BAT面试资料,以及一些 Java 高并发、分布式、微服务、高性能、源码分析、JVM等技术资料感兴趣的可以自己来我的Java架构进阶群,可以免费来群里下载Java资料,群号:171662117对Java技术,架构技术感兴趣的同学,欢迎加群,一起学习,相互讨论。