搭建原始碼環境
在這裡我提一下,在早期Mybatis版本中,Dao開發方式都是有Mapper介面和其實現類的,實現類是需要我們自己編寫的,後來Mybatis使用JDK動態代理針對Mapper介面做了代理,替我們實現了實現類; 但是其底層也是使用了Mapper介面的實現類,不可能說只有一個即可就能和JDBC進行通訊 ! 其基礎環境搭建可參照官方教程 www.mybatis.org/mybatis-3/z… 本章末尾最後給出了本章中的測試環境
快速進入Debug跟蹤
我們可以在此處打上斷點,Debug模式啟動進入斷點,再按F7跟蹤入其方法
原始碼分析準備
在進行Mybatis的初始化過程之前,我們需要把整個大綱拎出來放在前面,讓大家能夠有所瞭解,然後在進行每個步驟的時候心裡有個大概;
- 什麼是Mybatis的初始化過程?
從程式碼上來看 "SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);" 這行程式碼就是執行的是Mybatis的初始化操作,這個操作通常在應用中只會操作一次,構建完成SqlSessionFactory就不再使用,而SqlSessionFactory會跟隨整個應用生命週期;
從應用階段上來說 : Mybatis根據全域性XML配置檔案生成SqlSessionFactory的過程就是Mybatis的初始化過程.
- 淺析一詞含義
既然標題為淺析某某....相比大家也能看出說明本章不會深度挖掘底層程式碼,我個人認為淺析一次的主要意義是 ""能夠快速地在我們心中建立底層原始碼的架構圖,快速瀏覽程式碼,與概念進行核對 "",當然也不包含某些大牛謙虛的說法哈~~ 在這裡提的主要目的是,本次淺析Mybatis是快速瀏覽程式碼; 稍後會出新的篇章對核心方法進行剖析
- Mybatis初始化過程中的主要步驟
- 將全域性配置檔案XML解析到Configuration物件
- 將對映配置檔案XML解析到Configuration的mapperRegistry物件
- 將對映配置檔案XML中的宣告(Statement)解析成MappedStatement物件存入Configuration物件的mappedStatements集合中
- 最後將Configuration最為引數構建DefaultSqlSessionFactory物件
原始碼分析
第一步: 將全域性配置檔案XML載入到Configuration物件
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
parser.parse();
複製程式碼
主要功能 : 將全域性配置檔案中的配置載入到一個Configuration物件的屬性中
這是第一步,我們從Main
方法的new SqlSessionFactoryBuilder().build(inputStream)
進入斷點,可以看到在構建完畢SqlSessionFactoryBuilder
物件後由呼叫了過載的build
方法
//SqlSessionFactoryBuilder的構造方法
public SqlSessionFactoryBuilder() {
}
//build方法
public SqlSessionFactory build(InputStream inputStream) {
return this.build((InputStream)inputStream, (String)null, (Properties)null);
}
//build方法(過載)
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
//第一步: 建立XML配置構建器,用來解析全域性XML檔案內容
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
複製程式碼
在繼續深入之前我們需要了解一下XMLConfigBuilder
這個物件,從名字上來看就可以知道是解析XML配置檔案的;XMLConfigBuilder
又繼承了BaseBuilder
類,而在BaseBuilder
類中有一個屬性Configuration
,這個Configuration
物件就是用來儲存全域性配置檔案和其他Mapper的配置資訊, 同時我們從下圖也可以看到XMLMapperBuilder
,XMLStatementBuilder
,MapperBuilderAssistant
也繼承了BaseBuilder
類
XMLxxxBuilder是用來解析XML配置檔案的,不同型別XMLxxxBuilder用來解析MyBatis配置檔案的不同部位。
- XMLConfigBuilder用來解析MyBatis的全域性配置檔案
- XMLMapperBuilder用來解析MyBatis中的對映檔案
- XMLStatementBuilder用來解析對映檔案中的statement語句。
- MapperBuilderAssistant用來輔助解析對映檔案並生成MappedStatement物件
這些XMLxxxBuilder都有一個共同的父類——BaseBuilder
。這個父類維護了一個全域性的Configuration
物件,MyBatis的配置檔案解析後就以Configuration物件的形式儲存
看原始碼果然能發現貓膩,不錯不錯,可以看到在new
這個XMLConfigBuilder
物件的時候,下圖的斷點位置super(new Configuration());
可以看到Configuration
的構造方法如下所示,這也正解釋了我們我們可以在全域性配置檔案中寫個JDBC就行,因為在Configuration
物件在構建的時候就載入了一些預設的別名. 別告訴我你不知道別名是啥哈~~
public Configuration() {
this.safeResultHandlerEnabled = true;
this.multipleResultSetsEnabled = true;
this.useColumnLabel = true;
this.cacheEnabled = true;
this.useActualParamName = true;
this.localCacheScope = LocalCacheScope.SESSION;
this.jdbcTypeForNull = JdbcType.OTHER;
this.lazyLoadTriggerMethods = new HashSet(Arrays.asList("equals", "clone", "hashCode", "toString"));
this.defaultExecutorType = ExecutorType.SIMPLE;
this.autoMappingBehavior = AutoMappingBehavior.PARTIAL;
this.autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
this.variables = new Properties();
this.reflectorFactory = new DefaultReflectorFactory();
this.objectFactory = new DefaultObjectFactory();
this.objectWrapperFactory = new DefaultObjectWrapperFactory();
this.lazyLoadingEnabled = false;
this.proxyFactory = new JavassistProxyFactory();
this.mapperRegistry = new MapperRegistry(this);
this.interceptorChain = new InterceptorChain();
this.typeHandlerRegistry = new TypeHandlerRegistry();
this.typeAliasRegistry = new TypeAliasRegistry();
this.languageRegistry = new LanguageDriverRegistry();
this.mappedStatements = new Configuration.StrictMap("Mapped Statements collection");
this.caches = new Configuration.StrictMap("Caches collection");
this.resultMaps = new Configuration.StrictMap("Result Maps collection");
this.parameterMaps = new Configuration.StrictMap("Parameter Maps collection");
this.keyGenerators = new Configuration.StrictMap("Key Generators collection");
this.loadedResources = new HashSet();
this.sqlFragments = new Configuration.StrictMap("XML fragments parsed from previous mappers");
this.incompleteStatements = new LinkedList();
this.incompleteCacheRefs = new LinkedList();
this.incompleteResultMaps = new LinkedList();
this.incompleteMethods = new LinkedList();
this.cacheRefMap = new HashMap();
this.typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
this.typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
this.typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
this.typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
this.typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
this.typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
this.typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
this.typeAliasRegistry.registerAlias("LRU", LruCache.class);
this.typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
this.typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
this.typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
this.typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
this.typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
this.typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
this.typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
this.typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
this.typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
this.typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
this.typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
this.typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
this.typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
this.typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
this.languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
this.languageRegistry.register(RawLanguageDriver.class);
}
複製程式碼
第一步還沒有執行完? 是的上述中我們在看構建XMLConfigBuilder
物件過程,現在構建完成了我們就需要看這一行程式碼了parser.parse();
; 當有了XMLConfigBuilder
物件之後,接下來就可以用它來解析配置檔案了
public Configuration parse() {
//判斷是否已經解析,只能解析一次全域性配置檔案
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
//將parsed標記為已經解析
this.parsed = true;
//解析全域性配置檔案的XML中的configuration節點
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
複製程式碼
//主要看一下解析全域性配置檔案的configuration節點的方法
private void parseConfiguration(XNode root) {
try {
//解析全域性配置檔案中的properties節點的配置資訊儲存到Configuration物件的variables屬性中
this.propertiesElement(root.evalNode("properties"));
//解析全域性配置檔案中的settings節點的配置資訊設定到Configuration物件的各個屬性中
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfs(settings);
this.settingsElement(settings);
//解析全域性配置檔案中的typeAliases節點的配置資訊設定到BaseBuilder物件的TypeAliasRegistry屬性中
this.typeAliasesElement(root.evalNode("typeAliases"));
//解析全域性配置檔案的plugins
this.pluginElement(root.evalNode("plugins"));
//解析全域性配置檔案中的objectFactory設定到Configuration物件的objectFactory屬性中
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
//解析全域性配置檔案中的Environment節點儲存到Configuration物件中的Environment屬性中
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlerElement(root.evalNode("typeHandlers"));
//第二步 : 解析全域性配置檔案中的mappers節點 注意這是一個核心的方法 我們點進去看一下
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}
複製程式碼
從上述程式碼中可以看到,XMLConfigBuilder會依次解析配置檔案中的、、、、、等屬性。
第二步 : 解析對映配置檔案XML到Configuration的mapperRegistry容器
this.mapperElement(root.evalNode("mappers"));
複製程式碼
主要功能 : MyBatis會遍歷下所有的子節點,如果當前遍歷到的節點是,則MyBatis會將該包下的所有Mapper Class註冊到configuration的mapperRegistry容器中。如果當前節點為,則會依次獲取resource、url、class屬性,解析對映檔案,並將對映檔案對應的Mapper Class註冊到configuration的mapperRegistry容器中。
XMLConfigBuilder
解析全域性配置檔案中有一個比較重要的一步;就是解析對映檔案this.mapperElement(root.evalNode("mappers"))
這句程式碼開始解析對映檔案,我們開看一下下圖中構建了一個XMLMapperBuilder
物件,這個物件是負責解析對映檔案的;而第一步的XMLConfigBuilder
物件是解析全域性配置檔案的
上圖中紅色圈中的是Mybatis解析對映檔案的方法,我們進去看一下 mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
//首先會初始化父類BaseBuilder,並將configuration賦給BaseBuilder;
super(configuration);
//然後建立MapperBuilderAssistant物件,該物件為XMLMapperBuilder的協助者,用來協助XMLMapperBuilder完成一些解析對映檔案的動作
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
public void parse() {
if (!this.configuration.isResourceLoaded(this.resource)) {
this.configurationElement(this.parser.evalNode("/mapper"));
this.configuration.addLoadedResource(this.resource);
this.bindMapperForNamespace();
}
this.parsePendingResultMaps();
this.parsePendingCacheRefs();
this.parsePendingStatements();
}
複製程式碼
再看一下mapperParser.parse();
public void parse() {
//如果對映檔案沒有被載入過
if (!this.configuration.isResourceLoaded(this.resource)) {
//執行載入對映檔案XML方法configurationElement
this.configurationElement(this.parser.evalNode("/mapper"));
//將此對映檔案新增已經解析了的集合中
this.configuration.addLoadedResource(this.resource);
//繫結Namespace
this.bindMapperForNamespace();
}
this.parsePendingResultMaps();
this.parsePendingCacheRefs();
this.parsePendingStatements();
}
複製程式碼
下面是具體Mybatis解析對映檔案中的Statement的過程
private void configurationElement(XNode context) {
try {
//獲取namespace
String namespace = context.getStringAttribute("namespace");
//判斷namespace,如果為空直接丟擲異常
if (namespace != null && !namespace.equals("")) {
//設定namespace
this.builderAssistant.setCurrentNamespace(namespace);
//下面就是解析各個Statement中的各個XML節點
this.cacheRefElement(context.evalNode("cache-ref"));
this.cacheElement(context.evalNode("cache"));
this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
this.resultMapElements(context.evalNodes("/mapper/resultMap"));
this.sqlElement(context.evalNodes("/mapper/sql"));
//第三步 : 解析Statement宣告 核心方法
this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} else {
throw new BuilderException("Mapper's namespace cannot be empty");
}
} catch (Exception var3) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);
}
}
複製程式碼
從上述程式碼中可以看到,XMLMapperBuilder
藉助MapperBuilderAssistant
會對Mapper對映檔案進行解析,在解析到最後,會將每一箇中的節點解析為MappedStatement
物件
第三步 : 解析對映檔案的Statement為MappedStatement物件
this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
主要功能 : 將對映檔案的子節點解析為MappedStatement
物件
我們進入 this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
這個方法看一下
private void buildStatementFromContext(List<XNode> list) {
if (this.configuration.getDatabaseId() != null) {
this.buildStatementFromContext(list, this.configuration.getDatabaseId());
}
this.buildStatementFromContext(list, (String)null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
Iterator var3 = list.iterator();
while(var3.hasNext()) {
XNode context = (XNode)var3.next();
XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException var7) {
this.configuration.addIncompleteStatement(statementParser);
}
}
}
複製程式碼
其中主要的邏輯都在下示圖中的兩行程式碼中
接下來我們進入XMLStatementBuilder類的parseStatementNode
去看看
最終由MapperBuilderAssistant
完成MappedStatement
物件的封裝,並且將MappedStatement
物件放入Configuration
物件的**mappedStatements
**容器中
初始化完成
主要功能 : 將已經裝載了各種XML資訊的Configuration物件作為引數構建DefaultSqlSessionFactory返回,Mybatis初始化完成!!!
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
複製程式碼
搭建原始碼Debug環境
POM依賴
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
複製程式碼
測試SQL
CREATE TABLE `user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(10) NOT NULL,
`password` varchar(52) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=latin1
複製程式碼
Mybatis全域性配置檔案
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="jimisun"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
複製程式碼
UserMapper介面
public interface UserMapper {
User selectUser(Integer id);
}
複製程式碼
UserMapper配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="user">
<select id="selectUser" resultType="com.jimisun.mybatis.domain.User">
select *
from user
where id = #{id}
</select>
</mapper>
複製程式碼
User實體
public class User {
private int id;
private String username;
private String password;
getter and setter .....
}
複製程式碼
Main方法
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("user.selectUser", 2);
System.out.println(user.toString());
}
複製程式碼
該教程所屬Java工程師之Spring Framework深度剖析專欄,本系列相關博文目錄 Java工程師之Spring Framework深度剖析專欄