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,也非常简单
@Mapper
public 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 查询中这几个步骤的详细情况。
准备运行环境 Environment
Environment 有两个核心属性,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,从而创建一个数据库连接。
创建核心配置对象 Configuration
Configuration 类非常复杂,包含很多配置信息,我们优先关注以下核心属性
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,这是默认的 SqlSessionFactory
public 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;
}
接下来的关注点在 SqlSession
SqlSession 的 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 技术,架构技术感兴趣的同学,欢迎加群,一起学习,相互讨论。