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進行資料庫操作

進行一次資料庫查詢操作的步驟如下:

1. 通過DefaultSqlSessionFactory創建一個DefaultSqlSession對象

SqlSession sqlSession = sqlSessionFactory.openSession(true);

2. 創建獲取一個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);

}

3. 調用代理對象的查詢方法

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;

}

4. 接下來的關注點在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等技術資料

由於平台規則限制,需要獲取資料的朋友們可以關注小編,後台私信「架構」獲取。


推薦閱讀:
相关文章