mybatis 原始碼分析(一)框架結構概覽
本篇部落格將主要對 mybatis 整體介紹,包括 mybatis 的專案結構,執行的主要流程,初始化流程,API 等各模組進行簡單的串聯,讓你能夠對 mybatis 有一個整體的把握。另外在 mybatis 原始碼的閱讀過程中,如果不想寫 demo 可以直接使用專案中的單元測試;
一、mybatis 結構介紹
mybatis的主要功能和使用 demo,在網上已經有很多了我就不再囉嗦了,同時 官方文件 也非常的詳細;另外 mybatis 中使用了多種設計模式,包括建造者、動態代理、策略、裝飾器模式等,在檢視原始碼的時候,最好先對這些設計模式有一定的瞭解;
其中 mybatis 的模組結構如下:
mybatis 的執行流程如下:
- 首先通過 Java API 或者 XML 配置完成初始化,最終所有的配置都在 Configuration 類中維護;
- 然後通過 SqlSessionFactory 得到 SqlSession,這裡 SqlSession 就是 mybatis 的頂層 API 了,主要通過他完成資料庫的增刪改查等操作;
- 然後 SqlSession 將具體的操作委託給 Executor 執行,Executor 就是 mybatis 的排程核心了,主要職責有 SQL 語句生成、一二級快取維護和事務的相關操作;
- 然後 Executor 將資料庫相關的操作委託給 StatementHandler,StatementHandler 中完成了 mybatis 最核心的工作,包括引數繫結,指定 SQL 語句,結果集對映等;
具體過程如圖所示:
二、初始化
mybatis 中包含了很多的配置項,具體每一項的講解 官網 也很詳細,其結構大致如下:(另外正如上面說的 mybatis 的配置項最後都由 Configuration 類維護,這其實就是外觀模式)
configuration(配置) properties(屬性) settings(設定) typeAliases(類型別名) typeHandlers(型別處理器) objectFactory(物件工廠) plugins(外掛) environments(環境配置) environment(環境變數) transactionManager(事務管理器) dataSource(資料來源) mappers(對映器)
1. Java API 初始化
Java API 初始化的方式雖然不常用,但是相較於 XML 的方式可以更清楚的看到 Configuration 的構成,其示例如下:
PooledDataSource dataSource = new PooledDataSource();
dataSource.setDriver("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT");
dataSource.setUsername("root");
dataSource.setPassword("root");
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(UserMapper.class);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
2. XML 配置初始化
相交於 Java API 的方式,XML 配置初始化,必然會多出 XML 的解析部分;程式碼如下:
String resource = "org/apache/ibatis/builder/MapperConfig.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
下面是一個相對完整的配置示例:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="org/apache/ibatis/databases/blog/blog-derby.properties"/>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="false"/>
...
</settings>
<typeAliases>
<typeAlias alias="Author" type="org.apache.ibatis.domain.blog.Author"/>
<typeAlias alias="Blog" type="org.apache.ibatis.domain.blog.Blog"/>
...
</typeAliases>
<typeHandlers>
<typeHandler javaType="String" jdbcType="VARCHAR" handler="org.apache.ibatis.builder.CustomStringTypeHandler"/>
</typeHandlers>
<objectFactory type="org.apache.ibatis.builder.ExampleObjectFactory">
<property name="objectFactoryProperty" value="100"/>
</objectFactory>
<plugins>
<plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">
<property name="pluginProperty" value="100"/>
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<!--<dataSource type="UNPOOLED">-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/apache/ibatis/builder/AuthorMapper.xml"/>
<mapper resource="org/apache/ibatis/builder/BlogMapper.xml"/>
...
</mappers>
</configuration>
其解析的流程如下:
主要程式碼如下:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) { }
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
從上面的程式碼和流程圖中可以看到,XML 初始化的主要流程被封裝到了 XMLConfigBuilder 當中;主要的程式碼邏輯如下:
public Configuration parse() {
if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); }
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
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);
}
}
三、SqlSession 使用方式
1. 直接指定 MappedStatement
try (SqlSession session = sqlMapper.openSession()) {
Author author = session.selectOne("org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor", new Author(101));
}
這種方式通過 namespace + sqlId 的方式直接指定 MappedStatement;這種方式因為直接編寫字串和強型別轉換,既不安全也稍顯麻煩,所以現在已經不推薦使用了;
@Override
public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
registerCursor(cursor);
return cursor;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
2. 動態代理 Mapper 的方式
try (SqlSession session = sqlMapper.openSession()) {
AuthorMapper mapper = session.getMapper(AuthorMapper.class);
Author author = mapper.selectAuthor(500);
}
這種方式不經避免了以上的問題,同時也能夠使用註解的方式編寫 sql,而且可以使用 IDE 提示;現在一般都推薦使用這種方式;但是其最終也是呼叫了上面的介面;
首先在初始化的時候通過 bindMapperForNamespace,註冊對應的 Mapper(要求namespace和Mapper的全限定名保持一致);
// XMLMapperBuilder
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) { //ignore, bound type is not required }
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
// MapperRegistry
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); }
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type)); // 新增代理工廠
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
使用的時候,通過 class 類名獲取 MapperProxyFactory 代理工廠,製造一個新的 Mapper 代理(注意這裡時每次都要生成一個代理類,因為其中包含了 SqlSession,而 SqlSession 是執行緒不安全的所以不能快取,但是我覺得這裡任然是可以優化的,有興趣你可以自己嘗試一下);
try (SqlSession session = sqlMapper.openSession()) {
AuthorMapper mapper = session.getMapper(AuthorMapper.class); // 代理類
}
// MapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); }
try {
return mapperProxyFactory.newInstance(sqlSession); // 建立代理物件
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
// MapperProxyFactory
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
// MapperProxy
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) { // 從Object中繼承的方法
return method.invoke(this, args);
} else if (method.isDefault()) { // 有預設實現的介面方法
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args); // 然後由 MapperMethod 執行,這裡使用策略模式,後面還會詳細講解
}
總結
SqlSession 是執行緒不安全的,所以在示例程式碼中每次使用都會將其關閉?
在 mybatis 中還有一個類 SqlSessionManager 裡面有一個 ThreadLocal 用來管理 SqlSession,在 Spring 中也同樣是用 SqlSessionHolder 來管理的,所以並不會每次都建立一個新的 SqlSession;
以上內容只是大致將了 mybatis 的主要結構,後面的章節還會分模組進行講解;
另外本文主要參考了《MyBatis技術內幕》,有興趣的可以自行檢視