1. 程式人生 > >Mybatis原始碼深度解析

Mybatis原始碼深度解析

前言:

    mybatis是我們常用的一種操作資料庫的框架。

    我們在使用的mybatis有多種方式:原生mybatis、與Spring結合使用的mybatis、與SprinBoot結合使用的mybatis。

    使用的方式越來越簡單,需要我們配置的項也越來越少,但是原理都是通用的,底層都是mybatis框架,而mybatis框架的底層也就是我們熟悉的JDBC。

    萬變不離其宗,下面請隨著筆者來以對照JDBC的方式來剖析mybatis原始碼,看完其原始碼可以發現其實框架沒有我們想象中的那麼難,只是封裝的好一點,考慮的情況多一點

 

 

1.JDBC操作資料庫的方式

String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
String sql = "update blog set name = ? where id=?";

try {
// 1.獲取連線
Connection connection = DriverManager.getConnection(url, username, password);

// 2.建立 preparedStatement
PreparedStatement prepareStatement = connection.prepareStatement(sql);

// 3.初始化引數
prepareStatement.setString(1, "lucy");
prepareStatement.setInt(2, 1);

// 4.執行update
prepareStatement.executeUpdate();

} catch (SQLException e) {
e.printStackTrace();
}
    主要分四步:獲取連線、建立preparedStatement、封裝引數、執行

    下面我們也按照這四步來分析Mybatis

 

 

2.使用原生mybatis方式來操作資料庫

    關於mybatis的詳細使用讀者可參考 易佰教程 https://www.yiibai.com/mybatis/install_configure.html  

    筆者直接摘錄其中使用mybatis的經典方式,程式碼如下(注意:以下程式碼來自 易佰教程):

public class HelloWord {
private static SqlSessionFactory sqlSessionFactory;
private static Reader reader;

static {
try {
// 1.讀取mybatis配置檔案,並生成SQLSessionFactory
reader = Resources.getResourceAsReader("config/Configure.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (Exception e) {
e.printStackTrace();
}
}
public static SqlSessionFactory getSession() {
return sqlSessionFactory;
}
/**
* @param args
*/
public static void main(String[] args) {
// 2.獲取session,主要的CRUD操作均在SqlSession中提供
SqlSession session = sqlSessionFactory.openSession();
try {
// 3.執行查詢操作
// 該方法包括三個步驟:封裝入引數、執行查詢、封裝結果為物件型別
User user = (User) session.selectOne(
"com.yiibai.mybatis.models.UserMapper.GetUserByID", 1);
if(user!=null){
String userInfo = "名字:"+user.getName()+", 所屬部門:"+user.getDept()+", 主頁:"+user.getWebsite();
System.out.println(userInfo);
}
} finally {
session.close();
}
}

}
    通過上面的使用可知,最關鍵的程式碼就是session.selectOne(),裡面包括了入參的封裝、查詢的執行、結果封裝。這句話基本對應了JDBC中全部的四步關鍵操作。下面我們先來看下SqlSession的獲取,然後再對其方法進行分析。

 

 

3.SqlSession的獲取

// 1.讀取config目錄下Configure.xml檔案
Reader reader = Resources.getResourceAsReader("config/Configure.xml");
// 2.使用SqlSessionFactoryBuilder建立SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 3.在SqlSessionFactory中建立SqlSession
SqlSession session = sqlSessionFactory.openSession();
   1)Resources.getResourceAsReader("config/Configure.xml")讀取檔案

public static Reader getResourceAsReader(String resource) throws IOException {
Reader reader;
if (charset == null) {
// 預設為null,走該步驟
reader = new InputStreamReader(getResourceAsStream(resource));
} else {
reader = new InputStreamReader(getResourceAsStream(resource), charset);
}
return reader;
}

//getResourceAsStream()
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream(null, resource);
}

//getResourceAsStream()
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
// ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) throw new IOException("Could not find resource " + resource);
return in;
}

//classLoaderWrapper.getResourceAsStream(resource, loader)
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
if (null != cl) {

// 關鍵就是這句話
InputStream returnValue = cl.getResourceAsStream(resource);

// now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource
if (null == returnValue) returnValue = cl.getResourceAsStream("/" + resource);

if (null != returnValue) return returnValue;
}
}
return null;
}
    總結1):主要就是通過ClassLoader.getResourceAsStream()來獲取指定classpath路徑下的Resource

 

 

    2)new SqlSessionFactoryBuilder().build(reader)獲取SessionFactory

public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}

//build()
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 主要就是這句話
// 實現了兩個功能parse.parse()解析了xml;build(configuration)建立了SqlSessionFactory
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
    * 下面看下parse.parse()方法如何進行xml解析

//XMLConfigBuilder.parse() 
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

// parseConfiguration()
// 可以看到主要是對xml各節點的分析,我們重點關注下mapperElement()方法
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));// 重點關注下這個方法
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

// mapperElement(root.evalNode("mappers"))
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 1.獲取resource資訊,也就是對應的mapper.xml對應路徑
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// 2.解析對應的mapper.xml檔案,
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 3.將解析出來的Mapper物件新增到Configuration中
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
    可以看到,通過解析configuration.xml檔案,獲取其中的Environment、Setting,重要的是將<mappers>下的所有<mapper>解析出來之後新增到Configuration,Configuration類似於配置中心,所有的配置資訊都在這裡

 

 

    * 解析完成之後,我們來看下parse(configuration)如何生成一個SQLSessionFactory

public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
    非常簡單,直接把Configuration當做引數,直接new一個DefaultSqlSessionFactory

 

 

    3)sqlSessionFactory.openSession()開啟一個SqlSession

public SqlSession openSession() {
//configuration.getDefaultExecutorType() = ExecutorType.SIMPLE;
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

// openSessionFromDataSource()
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
// 1.transactionFactory預設為 ManagedTransactionFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 2.建立一個Transaction,注意該Transaction是org.apache.ibatis.transaction.Transaction
// Connection即從此處獲取的
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

// 3.建立一個Executor,預設為SimpleExecutor,參考Executor程式碼可知,主要的CRUD操作就是再此處完成的
final Executor executor = configuration.newExecutor(tx, execType);
// 4.將Executor和Configuration做為引數封裝到DefaultSqlSession,預設返回該SqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
    總結3):該方法中有許多新的類出現,下面我們集中看下這些類,簡單瞭解下這些類的作用,以便我們更好的理解程式碼

    * TransactionFactory

    類似於我們的SessionFactory,主要是用來生產Transaction的,注意這個Transaction是org.apache.ibatis.transaction.Transaction

public interface TransactionFactory {

void setProperties(Properties props);
Transaction newTransaction(Connection conn);
Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}
    * org.apache.ibatis.transaction.Transaction

    可以看到getConnection()方法返回的就是我們夢寐以求的java.sql.Connection,後續的操作都是基於此的

    並且還有一些關於事務的操作commit、rollback等

public interface Transaction {
// java.sql.Connection
Connection getConnection() throws SQLException;
void commit() throws SQLException;
void rollback() throws SQLException;
void close() throws SQLException;
}
    * Executor

    根據其介面方法可以看到CRUD操作在這裡被實現,看來SqlSession將具體的操作都委託為Executor了

public interface Executor {
...
int update(MappedStatement ms, Object parameter) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

Transaction getTransaction();
...

}
    總結3:

    * 解析configuration.xml檔案,生成對應的Environment、Setting、Mapper,並註冊到Configuration。Configuration相當於配置管理中心,所有的配置都在這裡體現

    * 生成org.apache.ibatis.transaction.Transaction實現類,裡面我們需要的java.sql.Connection

    * 根據Transaction封裝Executor,Executor負責主要的CRUD操作

    * 將Configuration和Executor封裝到SqlSession中,SqlSession對外提供統一操作入口,內部委託為Executor來執行

 

 

4.SqlSession.selectOne()方法的執行過程

    SqlSession預設實現為DefaultSqlSession

public <T> T selectOne(String statement, Object parameter) {
// 直接呼叫了selectList()方法
List<T> list = this.<T>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()
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 1.獲取對應的MappedStatement
// MappedStatement包裝了每一個CRUD操作對應的詳細資訊
MappedStatement ms = configuration.getMappedStatement(statement);

// 2.executor預設實現為CachingExecutor
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
    注意:有關於Cache的筆者就不分析了,網路上有很多關於Cache的文章,讀者可自行檢視

    可以看到主要的實現都委託給executor了,下面我們重點來看其query()方法的實現

 

 

5.CachingExecutor.query()

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

//query()
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
...
// 重點就是這句,預設實現在BaseExecutor
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

//BaseExecutor.query()
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
List<E> list;
try {
queryStack++;
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);
}
} finally {
queryStack--;
}
...
return list;
}

//queryFromDatabase()
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 主要在這裡,該方法是一個抽象方法,由子類實現,當前專案子類為SimpleExecutor
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
...
}

//SimpleExecutor.doQuery()
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 1.StatementHandler封裝了JDBC Statement操作,如設定引數、將Statement結果集轉換成List集合
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 2.封裝preparedStatement的引數(主要功能就在這裡實現)
stmt = prepareStatement(handler, ms.getStatementLog());
// 3.執行execute操作
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
    總結:分析可知,主要的功能實現在StatementHandler中。

    * StatementHandler建立了PreparedStatement;

    * 封裝了其所需要的引數;

    * 並且對其結果進行處理,封裝為物件

 

    下面我們逐個方法來看

 

    1)configuration.newStatementHandler()獲取StatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

//RoutingStatementHandler構造方法
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
// 我們的預設實現為這個
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}

}
    返回的StatementHandler為PreparedStatementHandler

 

    2)prepareStatement(handler, ms.getStatementLog())

    功能:獲取Statement;封裝PreparedStatement引數;

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 1.獲取java.sql.Connection
// Connection connection = transaction.getConnection();
Connection connection = getConnection(statementLog);
// 2.獲取對應的Statement
stmt = handler.prepare(connection);
// 3.封裝引數
handler.parameterize(stmt);
return stmt;
}
    * handler.prepare()獲取Statement

    在這裡,最終還是通過java.sql.Connection.prepareStatement(sql)的方式來建立Statement

public Statement prepare(Connection connection) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
// 重點在這裡
statement = instantiateStatement(connection);
setStatementTimeout(statement);
setFetchSize(statement);
return statement;
...
}

//PreparedStatementHandler.instantiateStatement()
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() != null) {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
} else {
// 採用了預設實現,
// 直接對應了我們傳統JDBC方式的從connection中獲取PreparedStatement
return connection.prepareStatement(sql);
}
}
    * handler.parameterize(stmt)封裝引數

public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}

//
public void setParameters(PreparedStatement ps) throws SQLException {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 1.parameterMappings包含了需要拼裝的引數
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
// 2.獲取引數名稱
String propertyName = parameterMapping.getProperty();
// 3.獲取引數值
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 4.獲取引數對應的型別處理器
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
// 5.對應的封裝引數操作還是要委託給TypeHandler來處理
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}

//BaseTypeHandler.setParameter()
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
"Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " +
"Cause: " + e, e);
}
} else {
// 由於引數值非空,會走該方法
setNonNullParameter(ps, i, parameter, jdbcType);
}
}
    本例中引數型別為Integer,則對應的typeHandler為IntegerTypeHandler,我們可以看到

// IntegerTypeHandler.setNonNullParameter()
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
    對應於JDBC的處理為preparedStatement.setInt(index,value)

 

    總結2):進過詳細分析,我們可以看到,最終的處理還是JDBC那一套,通過connection建立preparedStatement;對preparedStatement進行引數封裝;

    下面就是最終執行

 

 

    3)handler.<E>query(stmt, resultHandler)執行execute操作

//PreparedStatementHandler.query() 
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 1.進行execute操作
ps.execute();
// 2.對結果進行封裝
return resultSetHandler.<E> handleResultSets(ps);
}
    preparedStatement.execute()這個是常規操作,直接進行執行操作

    最後是對結果進行封裝,交由resultSetHandler來操作

    寫這篇部落格太累了,哈哈,筆者就不打算繼續分析resultSetHandler的操作了,但其核心操作也會與JDBC封裝結果集一致的

 

 

總結:分析下mybatis核心操作如下

    * 解析configuration.xml,生成Environment、Setting、Mapper等物件,並註冊到Configuration

    * 從SqlSessionFactory中獲取SqlSession,SqlSession作為操作入口,接收使用者的資料庫操作,並委託給內部的Executor來實現

    * Executor內部StatementHandler負責Statement的建立;PreparedStatement引數的注入;execute方法的執行,所以,可以說重要的執行操作都在StatementHandler中

    * execute方法執行過會,ResultSetHandler進行結果的封裝
--------------------- 
作者:恐龍弟旺仔 
來源:CSDN 
原文:https://blog.csdn.net/qq_26323323/article/details/81335058 
版權宣告:本文為博主原創文章,轉載請附上博文連結!