mybatis 3.x源碼深度解析與最佳實踐
html版離線文件可從https://files.cnblogs.com/files/zhjh256/mybatis3.x%E6%BA%90%E7%A0%81%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90%E4%B8%8E%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.rar下載。
-
- 1 環境准備
- 2 容器的加載與初始化
- 3 關鍵對象總結與回顧
- 4 SQL語句的執行流程
- 5 執行期主要類總結
- 6 插件
- 7 與spring集成
1 環境准備
1.1 mybatis介紹以及框架源碼的學習目標
MyBatis是當前最流行的java持久層框架之一,其通過XML配置的方式消除了絕大部分JDBC重復代碼以及參數的設置,結果集的映射。雖然mybatis是最為流行的持久層框架之一,但是相比其他開源框架比如spring/netty的源碼來說,其注釋相對而言顯得比較少。為了更好地學習和理解mybatis背后的設計思路,作為高級開發人員,有必要深入研究了解優秀框架的源碼,以便更好的借鑒其思想。同時,框架作為設計模式的主要應用場景,通過研究優秀框架的源碼,可以更好的領會設計模式的精髓。學習框架源碼和學習框架本身不同,我們不僅要首先比較細致完整的熟悉框架提供的每個特性,還要理解框架本身的初始化過程等,除此之外,更重要的是,我們不能泛泛的快速瀏覽哪個功能是通過哪個類或者接口實現和封裝的,對於核心特性和初始化過程的實現,我們應該達到下列目標:
- 對於核心特性和內部功能,具體是如何實現的,采用什么數據結構,邊研究邊思考這么做是否合理,或者不合理,尤其是很多的特性和功能的調用頻率是很低的,或者很多集合中包含的元素數量在99%以上的情況下都是1,這種情況下很多設計決定並不需要特別去選擇或者適合於大數據量或者高並發的復雜實現。對於很多內部的數據結構和輔助方法,不僅需要知道其功能本身,還需要知道他們在上下文中發揮的作用。
- 對於核心特性和內部功能,具體實現采用了哪些設計模式,使用這個設計模式的合理性;
- 絕大部分框架都被設計為可擴展的,mybatis也不例外,它提供了很多的擴展點,比如最常用的插件,語言驅動器,執行器,對象工廠,對象包裝器工廠等等都可以擴展,所以,我們應該知道如有必要的話,如何按照要求進行擴展以滿足自己的需求;
1.2 本系列源碼解析的方式
首先,和從使用層面相同,我們在理解實現細節的時候,也會涉及到學習A的時候,引用到B中的部分概念,B又引用了C的部分概念,C反過來又依賴於D接口和A的相關接口操作,D又有很多不同的具體實現。在使用層面,我們通常不需要深入去理解B的具體實現,只要知道B的概念和可以提供什么特性就可以了。在實現層面,我們必須去全部掌握這所有依賴的實現,直到最底層JDK原生操作或者某些我們熟悉的類庫為止。這實際上涉及到橫向流程和縱向切面的交叉引用,以及兩個概念的接口和多種實現之間的交叉調用。所以,我們在整個系列中會先按照業務流程橫向的方式進行整體分析,並附上必要的流程圖,在整個大流程完成之后,在一個專門的章節安排對關鍵的類進行詳細分析,它們的適用場景,這樣就可以交叉引用,又不會在主流程中夾雜太多的干擾內容。
其次,因為我們免不了會對源碼的主要部分做必要的注釋,所以,對源碼的講解和注釋會穿插進行,對於某些部分通過注釋后已經完全能夠知道其含義的實現,就不會在多此一舉的重述。
1.3 環境搭建
從https://github.com/mybatis/mybatis-3下載mybatis 3源代碼,導入eclipse工程,執行maven clean install,本書所使用的mybatis版本是3.4.6-SNAPSHOT,編寫時的最新版本,讀者如果使用其他版本,可能代碼上會有細微差別,但其基本不影響我們對主要核心代碼的分析。為了更好地理解mybatis的核心實現,我們需要創建一個演示工程mybatis-internal-example,讀者可從github上下載,並將依賴的mybatis坐標改成mybatis-3.4.6-SNAPSHOT。
1.4 從Hello World開始
要理解mybatis的內部原理,最好的方式是直接從頭開始,而不是從和spring的集成開始。每一個MyBatis的應用程序都以一個SqlSessionFactory對象的實例為核心。SqlSessionFactory對象的實例可以又通過SqlSessionFactoryBuilder對象來獲得。SqlSessionFactoryBuilder對象可以從XML配置文件加載配置信息,然后創建SqlSessionFactory。先看下面的例子:
主程序:
package org.mybatis.internal.example;
import java.io.IOException;
import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.mybatis.internal.example.pojo.User;
public class MybatisHelloWorld {
public static void main(String[] args) {
String resource = "org/mybatis/internal/example/Configuration.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlMapper.openSession();
try {
User user = (User) session.selectOne("org.mybatis.internal.example.mapper.UserMapper.getUser", 1);
System.out.println(user.getLfPartyId() + "," + user.getPartyName());
} finally {
session.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
配置文件:
<?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://10.7.12.4:3306/lfBase?useUnicode=true"/>
<property name="username" value="lfBase"/>
<property name="password" value="eKffQV6wbh3sfQuFIG6M"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/internal/example/UserMapper.xml"/>
</mappers>
</configuration>
mapper文件
<?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="org.mybatis.internal.example.mapper.UserMapper">
<select id="getUser" parameterType="int" resultType="org.mybatis.internal.example.pojo.User">
select lfPartyId,partyName from LfParty where lfPartyId = #{id}
</select>
</mapper>
mapper接口
package org.mybatis.internal.example.mapper;
import org.mybatis.internal.example.pojo.User;
public interface UserMapper {
public User getUser(int lfPartyId);
}
運行上述程序,不出意外的話,將輸出:
1,匯金超級管理員
從上述代碼可以看出,SqlSessionFactoryBuilder是一個關鍵的入口類,其中承擔了mybatis配置文件的加載,解析,內部構建等職責。下一章,我們將重點分析mybatis配置文件的加載,配置類的構建等一系列細節。
2 容器的加載與初始化
SqlSessionFactory是通過SqlSessionFactoryBuilder工廠類創建的,而不是直接使用構造器。容器的配置文件加載和初始化流程如下:
SqlSessionFactoryBuilder的主要代碼如下:
public class SqlSessionFactoryBuilder {
// 使用java.io.Reader構建
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
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.
}
}
}
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
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) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
根據上述接口可知,SqlSessionFactory提供了根據字節流、字符流以及直接使用org.apache.ibatis.session.Configuration配置類(后續我們會詳細講到)三種途徑的讀取配置信息方式,無論是字符流還是字節流方式,首先都是將XML配置文件構建為Configuration配置類,然后將Configuration設置到SqlSessionFactory默認實現DefaultSqlSessionFactory的configurationz字段並返回。所以,它本身很簡單,解析配置文件的關鍵邏輯都委托給XMLConfigBuilder了,我們以字符流也就是java.io.Reader為例進行分析,SqlSessionFactoryBuilder使用了XMLConfigBuilder作為解析器。
public class XMLConfigBuilder extends BaseBuilder {
private boolean parsed;
private XPathParser parser;
private String environment;
private ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
....
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
....
XMLConfigBuilder以及解析Mapper文件的XMLMapperBuilder都繼承於BaseBuilder。他們對於XML文件本身技術上的加載和解析都委托給了XPathParser,最終用的是jdk自帶的xml解析器而非第三方比如dom4j,底層使用了xpath方式進行節點解析。new XPathParser(reader, true, props, new XMLMapperEntityResolver())的參數含義分別是Reader,是否進行DTD 校驗,屬性配置,XML實體節點解析器。
entityResolver比較好理解,跟Spring的XML標簽解析器一樣,有默認的解析器,也有自定義的比如tx,dubbo等,主要使用了策略模式,在這里mybatis硬編碼為了XMLMapperEntityResolver。
XMLMapperEntityResolver的定義如下:
public class XMLMapperEntityResolver implements EntityResolver {
private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
/*
* Converts a public DTD into a local one
* 將公共的DTD轉換為本地模式
*
* @param publicId The public id that is what comes after "PUBLIC"
* @param systemId The system id that is what comes after the public id.
* @return The InputSource for the DTD
*
* @throws org.xml.sax.SAXException If anything goes wrong
*/
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e.toString());
}
}
private InputSource getInputSource(String path, String publicId, String systemId) {
InputSource source = null;
if (path != null) {
try {
InputStream in = Resources.getResourceAsStream(path);
source = new InputSource(in);
source.setPublicId(publicId);
source.setSystemId(systemId);
} catch (IOException e) {
// ignore, null is ok
}
}
return source;
}
}
從上述代碼可以看出,mybatis解析的時候,引用了本地的DTD文件,和本類在同一個package下,其中的ibatis-3-config.dtd應該主要是用於兼容用途。在其中getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId)的調用里面有兩個參數publicId(公共標識符)和systemId(系統標示符),他們是XML 1.0規范的一部分。他們的使用如下:
<?xml version="1.0"?>
<!DOCTYPE greeting SYSTEM "hello.dtd">
<greeting>Hello, world!</greeting>
系統標識符 “hello.dtd” 給出了此文件的 DTD 的地址(一個 URI 引用)。在mybatis中,這里指定的是mybatis-3-config.dtd和ibatis-3-config.dtd。publicId則一般從XML文檔的DOCTYPE中獲取,如下:
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
繼續往下看,
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(reader));
}
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
可知,commonConstructor並沒有做什么。回過頭到createDocument上,其使用了org.xml.sax.InputSource作為參數,createDocument的關鍵代碼如下:
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation);
//設置由本工廠創建的解析器是否支持XML命名空間 TODO 什么是XML命名空間
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
//設置是否將CDATA節點轉換為Text節點
factory.setCoalescing(false);
//設置是否展開實體引用節點,這里應該是sql片段引用的關鍵
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
//設置解析mybatis xml文檔節點的解析器,也就是上面的XMLMapperEntityResolver
builder.setEntityResolver(entityResolver);
builder.setErrorHandler(new ErrorHandler() {
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
}
});
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
主要是根據mybatis自身需要創建一個文檔解析器,然后調用parse將輸入input source解析為DOM XML文檔並返回。
得到XPathParser實例之后,就調用另一個使用XPathParser作為配置來源的重載構造函數了,如下:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
其中調用了父類BaseBuilder的構造器(主要是設置類型別名注冊器,以及類型處理器注冊器):
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
我們等下再來看Configuration這個核心類的關鍵信息,主要是xml配置文件里面的所有配置的實現表示(幾乎所有的情況下都要依賴與Configuration這個類)。
設置關鍵配置environment以及properties文件(mybatis在這里的實現和spring的機制有些不同),最后將上面構建的XPathParser設置為XMLConfigBuilder的parser屬性值。
XMLConfigBuilder創建完成之后,SqlSessionFactoryBuild調用parser.parse()創建Configuration。所有,真正Configuration構建邏輯就在XMLConfigBuilder.parse()里面,如下所示:
public class XMLConfigBuilder extends BaseBuilder {
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//mybatis配置文件解析的主流程
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
}
2.1 config文件解析XMLConfigBuilder.parseConfiguration
首先判斷有沒有解析過配置文件,只有沒有解析過才允許解析。其中調用了parser.evalNode(“/configuration”)返回根節點的org.apache.ibatis.parsing.XNode表示,XNode里面主要把關鍵的節點屬性和占位符變量結構化出來,后面我們再看。然后調用parseConfiguration根據mybatis的主要配置進行解析,如下所示:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(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);
}
}
所有的root.evalNode底層都是調用XML DOM的evaluate()方法,根據給定的節點表達式來計算指定的 XPath 表達式,並且返回一個XPathResult對象,返回類型在Node.evalNode()方法中均被指定為NODE。
在詳細解釋每個節點的解析前,我們先來粗略看下mybatis-3-config.dtd的聲明:
<?xml version="1.0" encoding="UTF-8" ?>
<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>
<!ELEMENT databaseIdProvider (property*)>
<!ATTLIST databaseIdProvider
type CDATA #REQUIRED
>
<!ELEMENT properties (property*)>
<!ATTLIST properties
resource CDATA #IMPLIED
url CDATA #IMPLIED
>
<!ELEMENT property EMPTY>
<!ATTLIST property
name CDATA #REQUIRED
value CDATA #REQUIRED
>
<!ELEMENT settings (setting+)>
<!ELEMENT setting EMPTY>
<!ATTLIST setting
name CDATA #REQUIRED
value CDATA #REQUIRED
>
<!ELEMENT typeAliases (typeAlias*,package*)>
<!ELEMENT typeAlias EMPTY>
<!ATTLIST typeAlias
type CDATA #REQUIRED
alias CDATA #IMPLIED
>
<!ELEMENT typeHandlers (typeHandler*,package*)>
<!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
handler CDATA #REQUIRED
>
<!ELEMENT objectFactory (property*)>
<!ATTLIST objectFactory
type CDATA #REQUIRED
>
<!ELEMENT objectWrapperFactory EMPTY>
<!ATTLIST objectWrapperFactory
type CDATA #REQUIRED
>
<!ELEMENT reflectorFactory EMPTY>
<!ATTLIST reflectorFactory
type CDATA #REQUIRED
>
<!ELEMENT plugins (plugin+)>
<!ELEMENT plugin (property*)>
<!ATTLIST plugin
interceptor CDATA #REQUIRED
>
<!ELEMENT environments (environment+)>
<!ATTLIST environments
default CDATA #REQUIRED
>
<!ELEMENT environment (transactionManager,dataSource)>
<!ATTLIST environment
id CDATA #REQUIRED
>
<!ELEMENT transactionManager (property*)>
<!ATTLIST transactionManager
type CDATA #REQUIRED
>
<!ELEMENT dataSource (property*)>
<!ATTLIST dataSource
type CDATA #REQUIRED
>
<!ELEMENT mappers (mapper*,package*)>
<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>
<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>
從上述DTD可知,mybatis-config文件最多有11個配置項,分別是properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?,但是所有的配置都是可選的,這意味着mybatis-config配置文件本身可以什么都不包含。因為所有的配置最后保存到org.apache.ibatis.session.Configuration中,所以在詳細查看每塊配置的解析前,我們來看下Configuration的內部完整結構:
package org.apache.ibatis.session;
...省略import
public class Configuration {
protected Environment environment;
// 允許在嵌套語句中使用分頁(RowBounds)。如果允許使用則設置為false。默認為false
protected boolean safeRowBoundsEnabled;
// 允許在嵌套語句中使用分頁(ResultHandler)。如果允許使用則設置為false。
protected boolean safeResultHandlerEnabled = true;
// 是否開啟自動駝峰命名規則(camel case)映射,即從經典數據庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似映射。默認false
protected boolean mapUnderscoreToCamelCase;
// 當開啟時,任何方法的調用都會加載該對象的所有屬性。否則,每個屬性會按需加載。默認值false (true in ≤3.4.1)
protected boolean aggressiveLazyLoading;
// 是否允許單一語句返回多結果集(需要兼容驅動)。
protected boolean multipleResultSetsEnabled = true;
// 允許 JDBC 支持自動生成主鍵,需要驅動兼容。這就是insert時獲取mysql自增主鍵/oracle sequence的開關。注:一般來說,這是希望的結果,應該默認值為true比較合適。
protected boolean useGeneratedKeys;
// 使用列標簽代替列名,一般來說,這是希望的結果
protected boolean useColumnLabel = true;
// 是否啟用緩存
protected boolean cacheEnabled = true;
// 指定當結果集中值為 null 的時候是否調用映射對象的 setter(map 對象時為 put)方法,這對於有 Map.keySet() 依賴或 null 值初始化的時候是有用的。
protected boolean callSettersOnNulls;
// 允許使用方法簽名中的名稱作為語句參數名稱。 為了使用該特性,你的工程必須采用Java 8編譯,並且加上-parameters選項。(從3.4.1開始)
protected boolean useActualParamName = true;
//當返回行的所有列都是空時,MyBatis默認返回null。 當開啟這個設置時,MyBatis會返回一個空實例。 請注意,它也適用於嵌套的結果集 (i.e. collectioin and association)。(從3.4.2開始) 注:這里應該拆分為兩個參數比較合適, 一個用於結果集,一個用於單記錄。通常來說,我們會希望結果集不是null,單記錄仍然是null
protected boolean returnInstanceForEmptyRow;
// 指定 MyBatis 增加到日志名稱的前綴。
protected String logPrefix;
// 指定 MyBatis 所用日志的具體實現,未指定時將自動查找。一般建議指定為slf4j或log4j
protected Class <? extends Log> logImpl;
// 指定VFS的實現, VFS是mybatis提供的用於訪問AS內資源的一個簡便接口
protected Class <? extends VFS> vfsImpl;
// MyBatis 利用本地緩存機制(Local Cache)防止循環引用(circular references)和加速重復嵌套查詢。 默認值為 SESSION,這種情況下會緩存一個會話中執行的所有查詢。 若設置值為 STATEMENT,本地會話僅用在語句執行上,對相同 SqlSession 的不同調用將不會共享數據。
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
// 當沒有為參數提供特定的 JDBC 類型時,為空值指定 JDBC 類型。 某些驅動需要指定列的 JDBC 類型,多數情況直接用一般類型即可,比如 NULL、VARCHAR 或 OTHER。
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
// 指定對象的哪個方法觸發一次延遲加載。
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
// 設置超時時間,它決定驅動等待數據庫響應的秒數。默認不超時
protected Integer defaultStatementTimeout;
// 為驅動的結果集設置默認獲取數量。
protected Integer defaultFetchSize;
// SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements); BATCH 執行器將重用語句並執行批量更新。
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
// 指定 MyBatis 應如何自動映射列到字段或屬性。 NONE 表示取消自動映射;PARTIAL 只會自動映射沒有定義嵌套結果集映射的結果集。 FULL 會自動映射任意復雜的結果集(無論是否嵌套)。
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
// 指定發現自動映射目標未知列(或者未知屬性類型)的行為。這個值應該設置為WARNING比較合適
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
// settings下的properties屬性
protected Properties variables = new Properties();
// 默認的反射器工廠,用於操作屬性、構造器方便
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
// 對象工廠, 所有的類resultMap類都需要依賴於對象工廠來實例化
protected ObjectFactory objectFactory = new DefaultObjectFactory();
// 對象包裝器工廠,主要用來在創建非原生對象,比如增加了某些監控或者特殊屬性的代理類
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
// 延遲加載的全局開關。當開啟時,所有關聯對象都會延遲加載。特定關聯關系中可通過設置fetchType屬性來覆蓋該項的開關狀態。
protected boolean lazyLoadingEnabled = false;
// 指定 Mybatis 創建具有延遲加載能力的對象所用到的代理工具。MyBatis 3.3+使用JAVASSIST
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
// MyBatis 可以根據不同的數據庫廠商執行不同的語句,這種多廠商的支持是基於映射語句中的 databaseId 屬性。
protected String databaseId;
/**
* Configuration factory class.
* Used to create Configuration for loading deserialized unread properties.
* 指定一個提供Configuration實例的類. 這個被返回的Configuration實例是用來加載被反序列化對象的懶加載屬性值. 這個類必須包含一個簽名方法static Configuration getConfiguration(). (從 3.2.3 版本開始)
*/
protected Class<?> configurationFactory;
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// mybatis插件列表
protected final InterceptorChain interceptorChain = new InterceptorChain();
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
// 類型注冊器, 用於在執行sql語句的出入參映射以及mybatis-config文件里的各種配置比如<transactionManager type="JDBC"/><dataSource type="POOLED">時使用簡寫, 后面會詳細解釋
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
protected final Set<String> loadedResources = new HashSet<String>();
protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();
/*
* A map holds cache-ref relationship. The key is the namespace that
* references a cache bound to another namespace and the value is the
* namespace which the actual cache is bound to.
*/
protected final Map<String, String> cacheRefMap = new HashMap<String, String>();
public Configuration(Environment environment) {
this();
this.environment = environment;
}
public Configuration() {
// 內置別名注冊
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
... 省去不必要的getter/setter
public Class<? extends VFS> getVfsImpl() {
return this.vfsImpl;
}
public void setVfsImpl(Class<? extends VFS> vfsImpl) {
if (vfsImpl != null) {
this.vfsImpl = vfsImpl;
VFS.addImplClass(this.vfsImpl);
}
}
public ProxyFactory getProxyFactory() {
return proxyFactory;
}
public void setProxyFactory(ProxyFactory proxyFactory) {
if (proxyFactory == null) {
proxyFactory = new JavassistProxyFactory();
}
this.proxyFactory = proxyFactory;
}
/**
* @since 3.2.2
*/
public List<Interceptor> getInterceptors() {
return interceptorChain.getInterceptors();
}
public LanguageDriverRegistry getLanguageRegistry() {
return languageRegistry;
}
public void setDefaultScriptingLanguage(Class<?> driver) {
if (driver == null) {
driver = XMLLanguageDriver.class;
}
getLanguageRegistry().setDefaultDriverClass(driver);
}
public LanguageDriver getDefaultScriptingLanguageInstance() {
return languageRegistry.getDefaultDriver();
}
/** @deprecated Use {@link #getDefaultScriptingLanguageInstance()} */
@Deprecated
public LanguageDriver getDefaultScriptingLanuageInstance() {
return getDefaultScriptingLanguageInstance();
}
public MetaObject newMetaObject(Object object) {
return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
}
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
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;
}
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
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;
}
public void addKeyGenerator(String id, KeyGenerator keyGenerator) {
keyGenerators.put(id, keyGenerator);
}
public Collection<String> getKeyGeneratorNames() {
return keyGenerators.keySet();
}
public Collection<KeyGenerator> getKeyGenerators() {
return keyGenerators.values();
}
public KeyGenerator getKeyGenerator(String id) {
return keyGenerators.get(id);
}
public boolean hasKeyGenerator(String id) {
return keyGenerators.containsKey(id);
}
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}
public Collection<String> getCacheNames() {
return caches.keySet();
}
public Collection<Cache> getCaches() {
return caches.values();
}
public Cache getCache(String id) {
return caches.get(id);
}
public boolean hasCache(String id) {
return caches.containsKey(id);
}
public void addResultMap(ResultMap rm) {
resultMaps.put(rm.getId(), rm);
checkLocallyForDiscriminatedNestedResultMaps(rm);
checkGloballyForDiscriminatedNestedResultMaps(rm);
}
public Collection<String> getResultMapNames() {
return resultMaps.keySet();
}
public Collection<ResultMap> getResultMaps() {
return resultMaps.values();
}
public ResultMap getResultMap(String id) {
return resultMaps.get(id);
}
public boolean hasResultMap(String id) {
return resultMaps.containsKey(id);
}
public void addParameterMap(ParameterMap pm) {
parameterMaps.put(pm.getId(), pm);
}
public Collection<String> getParameterMapNames() {
return parameterMaps.keySet();
}
public Collection<ParameterMap> getParameterMaps() {
return parameterMaps.values();
}
public ParameterMap getParameterMap(String id) {
return parameterMaps.get(id);
}
public boolean hasParameterMap(String id) {
return parameterMaps.containsKey(id);
}
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
public Collection<String> getMappedStatementNames() {
buildAllStatements();
return mappedStatements.keySet();
}
public Collection<MappedStatement> getMappedStatements() {
buildAllStatements();
return mappedStatements.values();
}
public Collection<XMLStatementBuilder> getIncompleteStatements() {
return incompleteStatements;
}
public void addIncompleteStatement(XMLStatementBuilder incompleteStatement) {
incompleteStatements.add(incompleteStatement);
}
public Collection<CacheRefResolver> getIncompleteCacheRefs() {
return incompleteCacheRefs;
}
public void addIncompleteCacheRef(CacheRefResolver incompleteCacheRef) {
incompleteCacheRefs.add(incompleteCacheRef);
}
public Collection<ResultMapResolver> getIncompleteResultMaps() {
return incompleteResultMaps;
}
public void addIncompleteResultMap(ResultMapResolver resultMapResolver) {
incompleteResultMaps.add(resultMapResolver);
}
public void addIncompleteMethod(MethodResolver builder) {
incompleteMethods.add(builder);
}
public Collection<MethodResolver> getIncompleteMethods() {
return incompleteMethods;
}
public MappedStatement getMappedStatement(String id) {
return this.getMappedStatement(id, true);
}
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
buildAllStatements();
}
return mappedStatements.get(id);
}
public Map<String, XNode> getSqlFragments() {
return sqlFragments;
}
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
public void addMappers(String packageName, Class<?> superType) {
mapperRegistry.addMappers(packageName, superType);
}
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public boolean hasMapper(Class<?> type) {
return mapperRegistry.hasMapper(type);
}
public boolean hasStatement(String statementName) {
return hasStatement(statementName, true);
}
public boolean hasStatement(String statementName, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
buildAllStatements();
}
return mappedStatements.containsKey(statementName);
}
public void addCacheRef(String namespace, String referencedNamespace) {
cacheRefMap.put(namespace, referencedNamespace);
}
/*
* Parses all the unprocessed statement nodes in the cache. It is recommended
* to call this method once all the mappers are added as it provides fail-fast
* statement validation.
*/
protected void buildAllStatements() {
if (!incompleteResultMaps.isEmpty()) {
synchronized (incompleteResultMaps) {
// This always throws a BuilderException.
incompleteResultMaps.iterator().next().resolve();
}
}
if (!incompleteCacheRefs.isEmpty()) {
synchronized (incompleteCacheRefs) {
// This always throws a BuilderException.
incompleteCacheRefs.iterator().next().resolveCacheRef();
}
}
if (!incompleteStatements.isEmpty()) {
synchronized (incompleteStatements) {
// This always throws a BuilderException.
incompleteStatements.iterator().next().parseStatementNode();
}
}
if (!incompleteMethods.isEmpty()) {
synchronized (incompleteMethods) {
// This always throws a BuilderException.
incompleteMethods.iterator().next().resolve();
}
}
}
/*
* Extracts namespace from fully qualified statement id.
*
* @param statementId
* @return namespace or null when id does not contain period.
*/
protected String extractNamespace(String statementId) {
int lastPeriod = statementId.lastIndexOf('.');
return lastPeriod > 0 ? statementId.substring(0, lastPeriod) : null;
}
// Slow but a one time cost. A better solution is welcome.
protected void checkGloballyForDiscriminatedNestedResultMaps(ResultMap rm) {
if (rm.hasNestedResultMaps()) {
for (Map.Entry<String, ResultMap> entry : resultMaps.entrySet()) {
Object value = entry.getValue();
if (value instanceof ResultMap) {
ResultMap entryResultMap = (ResultMap) value;
if (!entryResultMap.hasNestedResultMaps() && entryResultMap.getDiscriminator() != null) {
Collection<String> discriminatedResultMapNames = entryResultMap.getDiscriminator().getDiscriminatorMap().values();
if (discriminatedResultMapNames.contains(rm.getId())) {
entryResultMap.forceNestedResultMaps();
}
}
}
}
}
}
// Slow but a one time cost. A better solution is welcome.
protected void checkLocallyForDiscriminatedNestedResultMaps(ResultMap rm) {
if (!rm.hasNestedResultMaps() && rm.getDiscriminator() != null) {
for (Map.Entry<String, String> entry : rm.getDiscriminator().getDiscriminatorMap().entrySet()) {
String discriminatedResultMapName = entry.getValue();
if (hasResultMap(discriminatedResultMapName)) {
ResultMap discriminatedResultMap = resultMaps.get(discriminatedResultMapName);
if (discriminatedResultMap.hasNestedResultMaps()) {
rm.forceNestedResultMaps();
break;
}
}
}
}
}
protected static class StrictMap<V> extends HashMap<String, V> {
private static final long serialVersionUID = -4950446264854982944L;
private final String name;
public StrictMap(String name, int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
this.name = name;
}
public StrictMap(String name, int initialCapacity) {
super(initialCapacity);
this.name = name;
}
public StrictMap(String name) {
super();
this.name = name;
}
public StrictMap(String name, Map<String, ? extends V> m) {
super(m);
this.name = name;
}
@SuppressWarnings("unchecked")
public V put(String key, V value) {
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains value for " + key);
}
if (key.contains(".")) {
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
return super.put(key, value);
}
public V get(Object key) {
V value = super.get(key);
if (value == null) {
throw new IllegalArgumentException(name + " does not contain value for " + key);
}
if (value instanceof Ambiguity) {
throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
+ " (try using the full name including the namespace, or rename one of the entries)");
}
return value;
}
private String getShortName(String key) {
final String[] keyParts = key.split("\\.");
return keyParts[keyParts.length - 1];
}
protected static class Ambiguity {
final private String subject;
public Ambiguity(String subject) {
this.subject = subject;
}
public String getSubject() {
return subject;
}
}
}
}
mybatis中所有環境配置、resultMap集合、sql語句集合、插件列表、緩存、加載的xml列表、類型別名、類型處理器等全部都維護在Configuration中。Configuration中包含了一個內部靜態類StrictMap,它繼承於HashMap,對HashMap的裝飾在於增加了put時防重復的處理,get時取不到值時候的異常處理,這樣核心應用層就不需要額外關心各種對象異常處理,簡化應用層邏輯。
從設計上來說,我們可以說Configuration並不是一個thin類(也就是僅包含了屬性以及getter/setter),而是一個rich類,它對部分邏輯進行了封裝便於用戶直接使用,而不是讓用戶各自散落處理,比如addResultMap方法和getMappedStatement方法等等。
最新的完整mybatis每個配置屬性含義可參考http://www.mybatis.org/mybatis-3/zh/configuration.html。
從Configuration構造器和protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();可以看出,所有我們在mybatis-config和mapper文件中使用的類似int/string/JDBC/POOLED等字面常量最終解析為具體的java類型都是在typeAliasRegistry構造器和Configuration構造器執行期間初始化的。下面我們來每塊分析。
2.1.1 屬性解析propertiesElement
解析properties的方法為:
propertiesElement(root.evalNode("properties"));
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 加載property節點為property
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
// 必須至少包含resource或者url屬性之一
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
總體邏輯比較簡單,首先加載properties節點下的property屬性,比如:
<properties resource="org/mybatis/internal/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
然后從url或resource加載配置文件,都先和configuration.variables合並,然后賦值到XMLConfigBuilder.parser和BaseBuilder.configuration。此時開始所有的屬性就可以在隨后的整個配置文件中使用了。
2.1.2 加載settings節點settingsAsProperties
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
// 檢查所有從settings加載的設置,確保它們都在Configuration定義的范圍內
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
首先加載settings下面的setting節點為property,然后檢查所有屬性,確保它們都在Configuration中已定義,而非未知的設置。注:MetaClass是一個保存對象定義比如getter/setter/構造器等的元數據類,localReflectorFactory則是mybatis提供的默認反射工廠實現,這個ReflectorFactory主要采用了工廠類,其內部使用的Reflector采用了facade設計模式,簡化反射的使用。如下所示:
public class MetaClass {
private ReflectorFactory reflectorFactory;
private Reflector reflector;
private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
this.reflectorFactory = reflectorFactory;
this.reflector = reflectorFactory.findForClass(type);
}
public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
return new MetaClass(type, reflectorFactory);
}
...
}
@Override
public Reflector findForClass(Class<?> type) {
if (classCacheEnabled) {
// synchronized (type) removed see issue #461
Reflector cached = reflectorMap.get(type);
if (cached == null) {
cached = new Reflector(type);
reflectorMap.put(type, cached);
}
return cached;
} else {
return new Reflector(type);
}
}
public Reflector(Class<?> clazz) {
type = clazz;
addDefaultConstructor(clazz);
addGetMethods(clazz);
addSetMethods(clazz);
addFields(clazz);
readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writeablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
得到setting之后,調用settingsElement(Properties props)將各值賦值給configuration,同時在這里有重新設置了默認值,所有這一點很重要,configuration中的默認值不一定是真正的默認值。
2.1.3 加載自定義VFS loadCustomVfs
VFS主要用來加載容器內的各種資源,比如jar或者class文件。mybatis提供了2個實現 JBoss6VFS 和 DefaultVFS,並提供了用戶擴展點,用於自定義VFS實現,加載順序是自定義VFS實現 > 默認VFS實現 取第一個加載成功的,默認情況下會先加載JBoss6VFS,如果classpath下找不到jboss的vfs實現才會加載默認VFS實現,啟動打印的日志如下:
org.apache.ibatis.io.VFS.getClass(VFS.java:111) Class not found: org.jboss.vfs.VFS
org.apache.ibatis.io.JBoss6VFS.setInvalid(JBoss6VFS.java:142) JBoss 6 VFS API is not available in this environment.
org.apache.ibatis.io.VFS.getClass(VFS.java:111) Class not found: org.jboss.vfs.VirtualFile
org.apache.ibatis.io.VFS$VFSHolder.createVFS(VFS.java:63) VFS implementation org.apache.ibatis.io.JBoss6VFS is not valid in this environment.
org.apache.ibatis.io.VFS$VFSHolder.createVFS(VFS.java:77) Using VFS adapter org.apache.ibatis.io.DefaultVFS
jboss vfs的maven倉庫坐標為:
<dependency>
<groupId>org.jboss</groupId>
<artifactId>jboss-vfs</artifactId>
<version>3.2.12.Final</version>
</dependency>
找到jboss vfs實現后,輸出的日志如下:
org.apache.ibatis.io.VFS$VFSHolder.createVFS(VFS.java:77) Using VFS adapter org.apache.ibatis.io.JBoss6VFS
2.1.4 解析類型別名typeAliasesElement
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
從上述代碼可以看出,mybatis主要提供兩種類型的別名設置,具體類的別名以及包的別名設置。類型別名是為 Java 類型設置一個短的名字,存在的意義僅在於用來減少類完全限定名的冗余。
<typeAliases>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>
當這樣配置時,Blog可以用在任何使用domain.blog.Blog的地方。設置為package之后,MyBatis 會在包名下面搜索需要的 Java Bean。如:
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
每一個在包 domain.blog 中的 Java Bean,在沒有注解的情況下,會使用 Bean的首字母小寫的非限定類名來作為它的別名。 比如 domain.blog.Author 的別名為author;若有注解,則別名為其注解值。所以所有的別名,無論是內置的還是自定義的,都一開始被保存在configuration.typeAliasRegistry中了,這樣就可以確保任何時候使用別名和FQN的效果是一樣的。
2.1.5 加載插件pluginElement
幾乎所有優秀的框架都會預留插件體系以便擴展,mybatis調用pluginElement(root.evalNode(“plugins”));加載mybatis插件,最常用的插件應該算是分頁插件PageHelper了,再比如druid連接池提供的各種監控、攔截、預發檢查功能,在使用其它連接池比如dbcp的時候,在不修改連接池源碼的情況下,就可以借助mybatis的插件體系實現。加載插件的實現如下:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
//將interceptor指定的名稱解析為Interceptor類型
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
插件在具體實現的時候,采用的是攔截器模式,要注冊為mybatis插件,必須實現org.apache.ibatis.plugin.Interceptor接口,每個插件可以有自己的屬性。interceptor屬性值既可以完整的類名,也可以是別名,只要別名在typealias中存在即可,如果啟動時無法解析,會拋出ClassNotFound異常。實例化插件后,將設置插件的屬性賦值給插件實現類的屬性字段。mybatis提供了兩個內置的插件例子,如下所示:
我們會在第6章詳細講解自定義插件的實現。
2.1.6 加載對象工廠objectFactoryElement
什么是對象工廠?MyBatis 每次創建結果對象的新實例時,它都會使用一個對象工廠(ObjectFactory)實例來完成。 默認的對象工廠DefaultObjectFactory做的僅僅是實例化目標類,要么通過默認構造方法,要么在參數映射存在的時候通過參數構造方法來實例化。如下所示:
public class DefaultObjectFactory implements ObjectFactory, Serializable {
private static final long serialVersionUID = -8855120656740914948L;
@Override
public <T> T create(Class<T> type) {
return create(type, null, null);
}
@SuppressWarnings("unchecked")
@Override
public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
Class<?> classToCreate = resolveInterface(type);
// we know types are assignable
return (T) instantiateClass(classToCreate, constructorArgTypes, constructorArgs);
}
@Override
public void setProperties(Properties properties) {
// no props for default
}
...
protected Class<?> resolveInterface(Class<?> type) {
Class<?> classToCreate;
if (type == List.class || type == Collection.class || type == Iterable.class) {
classToCreate = ArrayList.class;
} else if (type == Map.class) {
classToCreate = HashMap.class;
} else if (type == SortedSet.class) { // issue #510 Collections Support
classToCreate = TreeSet.class;
} else if (type == Set.class) {
classToCreate = HashSet.class;
} else {
classToCreate = type;
}
return classToCreate;
}
...
}
無論是創建集合類型、Map類型還是其他類型,都素hi同樣的處理方式。如果想覆蓋對象工廠的默認行為,則可以通過創建自己的對象工廠來實現。ObjectFactory 接口很簡單,它包含兩個創建用的方法,一個是處理默認構造方法的,另外一個是處理帶參數的構造方法的。最后,setProperties 方法可以被用來配置 ObjectFactory,在初始化你的 ObjectFactory 實例后,objectFactory元素體中定義的屬性會被傳遞給setProperties方法。例如:
public class ExampleObjectFactory extends DefaultObjectFactory {
public Object create(Class type) {
return super.create(type);
}
public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
return super.create(type, constructorArgTypes, constructorArgs);
}
public void setProperties(Properties properties) {
super.setProperties(properties);
}
public <T> boolean isCollection(Class<T> type) {
return Collection.class.isAssignableFrom(type);
}
}
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
2.1.7 創建對象包裝器工廠objectWrapperFactoryElement
對象包裝器工廠主要用來包裝返回result對象,比如說可以用來設置某些敏感字段脫敏或者加密等。默認對象包裝器工廠是DefaultObjectWrapperFactory,也就是不使用包裝器工廠。既然看到包裝器工廠,我們就得看下對象包裝器,如下:
BeanWrapper是BaseWrapper的默認實現。其中的兩個關鍵接口是getBeanProperty和setBeanProperty,它們是實現包裝的主要位置:
private Object getBeanProperty(PropertyTokenizer prop, Object object) {
try {
Invoker method = metaClass.getGetInvoker(prop.getName());
try {
return method.invoke(object, NO_ARGUMENTS);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (RuntimeException e) {
throw e;
} catch (Throwable t) {
throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t);
}
}
private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
try {
Invoker method = metaClass.getSetInvoker(prop.getName());
Object[] params = {value};
try {
method.invoke(object, params);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (Throwable t) {
throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
}
}
要實現自定義的對象包裝器工廠,只要實現ObjectWrapperFactory中的兩個接口hasWrapperFor和getWrapperFor即可,如下:
public class CustomBeanWrapperFactory implements ObjectWrapperFactory {
@Override
public boolean hasWrapperFor(Object object) {
if (object instanceof Author) {
return true;
} else {
return false;
}
}
@Override
public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
return new CustomBeanWrapper(metaObject, object);
}
}
2.1.8 加載反射工廠reflectorFactoryElement
因為加載配置文件中的各種插件類等等,為了提供更好的靈活性,mybatis支持用戶自定義反射工廠,不過總體來說,用的不多,要實現反射工廠,只要實現ReflectorFactory接口即可。默認的反射工廠是DefaultReflectorFactory。一般來說,使用默認的反射工廠就可以了。
2.1.9 加載環境配置environmentsElement
環境可以說是mybatis-config配置文件中最重要的部分,它類似於spring和maven里面的profile,允許給開發、生產環境同時配置不同的environment,根據不同的環境加載不同的配置,這也是常見的做法,如果在SqlSessionFactoryBuilder調用期間沒有傳遞使用哪個環境的話,默認會使用一個名為default”的環境。找到對應的environment之后,就可以加載事務管理器和數據源了。事務管理器和數據源類型這里都用到了類型別名,JDBC/POOLED都是在mybatis內置提供的,在Configuration構造器執行期間注冊到TypeAliasRegister。
mybatis內置提供JDBC和MANAGED兩種事務管理方式,前者主要用於簡單JDBC模式,后者主要用於容器管理事務,一般使用JDBC事務管理方式。mybatis內置提供JNDI、POOLED、UNPOOLED三種數據源工廠,一般情況下使用POOLED數據源。
<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://10.7.12.4:3306/lfBase?useUnicode=true"/>
<property name="username" value="lfBase"/>
<property name="password" value="eKffQV6wbh3sfQuFIG6M"/>
</dataSource>
</environment>
</environments>
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
//查找匹配的environment
if (isSpecifiedEnvironment(id)) {
// 事務配置並創建事務工廠
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 數據源配置加載並實例化數據源, 數據源是必備的
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// 創建Environment.Builder
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
2.1.10 數據庫廠商標識加載databaseIdProviderElement
MyBatis 可以根據不同的數據庫廠商執行不同的語句,這種多廠商的支持是基於映射語句中的 databaseId 屬性。 MyBatis 會加載不帶 databaseId 屬性和帶有匹配當前數據庫 databaseId 屬性的所有語句。 如果同時找到帶有 databaseId 和不帶 databaseId 的相同語句,則后者會被舍棄。 為支持多廠商特性只要像下面這樣在 mybatis-config.xml 文件中加入 databaseIdProvider 即可:
<databaseIdProvider type="DB_VENDOR" />
這里的 DB_VENDOR 會通過 DatabaseMetaData#getDatabaseProductName() 返回的字符串進行設置。 由於通常情況下這個字符串都非常長而且相同產品的不同版本會返回不同的值,所以最好通過設置屬性別名來使其變短,如下:
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
在有 properties 時,DB_VENDOR databaseIdProvider 的將被設置為第一個能匹配數據庫產品名稱的屬性鍵對應的值,如果沒有匹配的屬性將會設置為 “null”。
因為每個數據庫在實現的時候,getDatabaseProductName() 返回的通常並不是直接的Oracle或者MySQL,而是“Oracle (DataDirect)”,所以如果希望使用多數據庫特性,一般需要實現 org.apache.ibatis.mapping.DatabaseIdProvider接口 並在 mybatis-config.xml 中注冊來構建自己的 DatabaseIdProvider:
public interface DatabaseIdProvider {
void setProperties(Properties p);
String getDatabaseId(DataSource dataSource) throws SQLException;
}
典型的實現比如:
public class VendorDatabaseIdProvider implements DatabaseIdProvider {
private static final Log log = LogFactory.getLog(VendorDatabaseIdProvider.class);
private Properties properties;
@Override
public String getDatabaseId(DataSource dataSource) {
if (dataSource == null) {
throw new NullPointerException("dataSource cannot be null");
}
try {
return getDatabaseName(dataSource);
} catch (Exception e) {
log.error("Could not get a databaseId from dataSource", e);
}
return null;
}
...
private String getDatabaseName(DataSource dataSource) throws SQLException {
String productName = getDatabaseProductName(dataSource);
if (this.properties != null) {
for (Map.Entry<Object, Object> property : properties.entrySet()) {
// 只要包含productName中包含了property名稱,就算匹配,而不是使用精確匹配
if (productName.contains((String) property.getKey())) {
return (String) property.getValue();
}
}
// no match, return null
return null;
}
return productName;
}
private String getDatabaseProductName(DataSource dataSource) throws SQLException {
Connection con = null;
try {
con = dataSource.getConnection();
DatabaseMetaData metaData = con.getMetaData();
return metaData.getDatabaseProductName();
} finally {
if (con != null) {
try {
con.close();
} catch (SQLException e) {
// ignored
}
}
}
}
}
2.1.11 加載類型處理器typeHandlerElement
private void typeHandlerElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
無論是 MyBatis 在預處理語句(PreparedStatement)中設置一個參數時,還是從結果集中取出一個值時, 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。
mybatis提供了兩種方式注冊類型處理器,package自動檢索方式和顯示定義方式。使用自動檢索(autodiscovery)功能的時候,只能通過注解方式來指定 JDBC 的類型。
<!-- mybatis-config.xml -->
<typeHandlers>
<package name="org.mybatis.example"/>
</typeHandlers>
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
//Ignore inner classes and interfaces (including package-info.java) and abstract classes
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
register(type);
}
}
}
為了簡化使用,mybatis在初始化TypeHandlerRegistry期間,自動注冊了大部分的常用的類型處理器比如字符串、數字、日期等。對於非標准的類型,用戶可以自定義類型處理器來處理。要實現一個自定義類型處理器,只要實現 org.apache.ibatis.type.TypeHandler 接口, 或繼承一個實用類 org.apache.ibatis.type.BaseTypeHandler, 並將它映射到一個 JDBC 類型即可。例如:
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
使用這個的類型處理器將會覆蓋已經存在的處理 Java 的 String 類型屬性和 VARCHAR 參數及結果的類型處理器。 要注意 MyBatis 不會窺探數據庫元信息來決定使用哪種類型,所以你必須在參數和結果映射中指明那是 VARCHAR 類型的字段, 以使其能夠綁定到正確的類型處理器上。 這是因為:MyBatis 直到語句被執行才清楚數據類型。
通過類型處理器的泛型,MyBatis 可以得知該類型處理器處理的 Java 類型,不過這種行為可以通過兩種方法改變:
- 在類型處理器的配置元素(typeHandler element)上增加一個 javaType 屬性(比如:javaType=”String”);
- 在類型處理器的類上(TypeHandler class)增加一個 @MappedTypes 注解來指定與其關聯的 Java 類型列表。 如果在 javaType 屬性中也同時指定,則注解方式將被忽略。
可以通過兩種方式來指定被關聯的 JDBC 類型:
- 在類型處理器的配置元素上增加一個 jdbcType 屬性(比如:jdbcType=”VARCHAR”);
- 在類型處理器的類上(TypeHandler class)增加一個 @MappedJdbcTypes 注解來指定與其關聯的 JDBC 類型列表。 如果在兩個位置同時指定,則注解方式將被忽略。
當決定在ResultMap中使用某一TypeHandler時,此時java類型是已知的(從結果類型中獲得),但是JDBC類型是未知的。 因此Mybatis使用javaType=[TheJavaType], jdbcType=null的組合來選擇一個TypeHandler。 這意味着使用@MappedJdbcTypes注解可以限制TypeHandler的范圍,同時除非顯示的設置,否則TypeHandler在ResultMap中將是無效的。 如果希望在ResultMap中使用TypeHandler,那么設置@MappedJdbcTypes注解的includeNullJdbcType=true即可。 然而從Mybatis 3.4.0開始,如果只有一個注冊的TypeHandler來處理Java類型,那么它將是ResultMap使用Java類型時的默認值(即使沒有includeNullJdbcType=true)。
還可以創建一個泛型類型處理器,它可以處理多於一個類。為達到此目的, 需要增加一個接收該類作為參數的構造器,這樣在構造一個類型處理器的時候 MyBatis 就會傳入一個具體的類。
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {
private Class<E> type;
public GenericTypeHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
this.type = type;
}
我們映射枚舉使用的EnumTypeHandler 和 EnumOrdinalTypeHandler 都是泛型類型處理器(generic TypeHandlers)。
處理枚舉類型映射
若想映射枚舉類型 Enum,則需要從 EnumTypeHandler 或者 EnumOrdinalTypeHandler 中選一個來使用。
比如說我們想存儲取近似值時用到的舍入模式。默認情況下,MyBatis 會利用 EnumTypeHandler 來把 Enum 值轉換成對應的名字。
注意 EnumTypeHandler 在某種意義上來說是比較特別的,其他的處理器只針對某個特定的類,而它不同,它會處理任意繼承了 Enum 的類。
不過,我們可能不想存儲名字,相反我們的 DBA 會堅持使用整形值代碼。那也一樣輕而易舉: 在配置文件中把 EnumOrdinalTypeHandler 加到 typeHandlers 中即可, 這樣每個 RoundingMode 將通過他們的序數值來映射成對應的整形。
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
</typeHandlers>
但是怎樣能將同樣的 Enum 既映射成字符串又映射成整形呢?
自動映射器(auto-mapper)會自動地選用EnumOrdinalTypeHandler來處理,所以如果我們想用普通的 EnumTypeHandler,就需要為那些SQL 語句顯式地設置要用到的類型處理器。比如:
<result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
2.1.12 加載mapper文件mapperElement
mapper文件是mybatis框架的核心之處,所有的用戶sql語句都編寫在mapper文件中,所以理解mapper文件對於所有的開發人員來說都是必備的要求。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 如果要同時使用package自動掃描和通過mapper明確指定要加載的mapper,一定要確保package自動掃描的范圍不包含明確指定的mapper,否則在通過package掃描的interface的時候,嘗試加載對應xml文件的loadXmlResource()的邏輯中出現判重出錯,報org.apache.ibatis.binding.BindingException異常,即使xml文件中包含的內容和mapper接口中包含的語句不重復也會出錯,包括加載mapper接口時自動加載的xml mapper也一樣會出錯。
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
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.");
}
}
}
}
}
mybatis提供了兩類配置mapper的方法,第一類是使用package自動搜索的模式,這樣指定package下所有接口都會被注冊為mapper,例如:
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
另外一類是明確指定mapper,這又可以通過resource、url或者class進行細分。例如:
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
需要注意的是,如果要同時使用package自動掃描和通過mapper明確指定要加載的mapper,則必須先聲明mapper,然后聲明package,否則DTD校驗會失敗。同時一定要確保package自動掃描的范圍不包含明確指定的mapper,否則在通過package掃描的interface的時候,嘗試加載對應xml文件的loadXmlResource()的邏輯中出現判重出錯,報org.apache.ibatis.binding.BindingException異常。
對於通過package加載的mapper文件,調用mapperRegistry.addMappers(packageName);進行加載,其核心邏輯在org.apache.ibatis.binding.MapperRegistry中,對於每個找到的接口或者mapper文件,最后調用用XMLMapperBuilder進行具體解析。
對於明確指定的mapper文件或者mapper接口,則主要使用XMLMapperBuilder進行具體解析。
下一章節,我們進行詳細說明mapper文件的解析加載與初始化。
2.2 mapper加載與初始化
前面說過mybatis mapper文件的加載主要有兩大類,通過package加載和明確指定的方式。
一般來說,對於簡單語句來說,使用注解代碼會更加清晰,然而Java注解對於復雜語句比如同時包含了構造器、鑒別器、resultMap來說就會非常混亂,應該限制使用,此時應該使用XML文件,因為注解至少至今為止不像XML/Gradle一樣能夠很好的表示嵌套關系。mybatis完整的注解列表以及含義可參考http://www.mybatis.org/mybatis-3/java-api.html或者http://blog.51cto.com/computerdragon/1399742或者源碼org.apache.ibatis.annotations包:
為了更好的理解上下文語義,建議讀者對XML配置對應的注解先了解,這樣看起源碼來會更加順暢。我們先來回顧一下通過注解配置的典型mapper接口:
@Select("select *from User where id=#{id} and userName like #{name}")
public User retrieveUserByIdAndName(@Param("id")int id,@Param("name")String names);
@Insert("INSERT INTO user(userName,userAge,userAddress) VALUES(#{userName},#{userAge},#{userAddress})")
public void addNewUser(User user);
@Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
@SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);
@Results(id = "userResult", value = {
@Result(property = "id", column = "uid", id = true),
@Result(property = "firstName", column = "first_name"),
@Result(property = "lastName", column = "last_name")
})
@TypeDiscriminator(column = "type",
cases={
@Case(value="1",type=RegisterEmployee.class,results={
@Result(property="salay")
}),
@Case(value="2",type=TimeEmployee.class,results={
@Result(property="time")
})
}
)
@Select("select * from users where id = #{id}")
User getUserById(Integer id);
@Results(id = "companyResults")
@ConstructorArgs({
@Arg(property = "id", column = "cid", id = true),
@Arg(property = "name", column = "name")
})
@Select("select * from company where id = #{id}")
Company getCompanyById(Integer id);
@ResultMap(id = "xmlUserResults")
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(String name);
// 注:建議盡可能避免使用SqlBuild的模式生成的,如果因為功能需要必須動態生成SQL的話,也是直接寫SQL拼接返回,而不是一堆類似SELECT()、FROM()的函數調用,這只會讓維護成為噩夢,這思路的設計者不是知道怎么想的, 此處僅用於演示XXXProvider功能,但是XXXProvider模式本身的設計在關鍵時候還是比較清晰的。
class UserSqlBuilder {
public String buildGetUsersByName(final String name) {
return new SQL(){{
SELECT("*");
FROM("users");
if (name != null) {
WHERE("name like #{value} || '%'");
}
ORDER_BY("id");
}}.toString();
}
}
我們先來看通過package自動搜索加載的方式,它的范圍由addMappers的參數packageName指定的包名以及父類superType確定,其整體流程如下:
/**
* @since 3.2.2
*/
public void addMappers(String packageName, Class<?> superType) {
// mybatis框架提供的搜索classpath下指定package以及子package中符合條件(注解或者繼承於某個類/接口)的類,默認使用Thread.currentThread().getContextClassLoader()返回的加載器,和spring的工具類殊途同歸。
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
// 無條件的加載所有的類,因為調用方傳遞了Object.class作為父類,這也給以后的指定mapper接口預留了余地
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 所有匹配的calss都被存儲在ResolverUtil.matches字段中
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
//調用addMapper方法進行具體的mapper類/接口解析
addMapper(mapperClass);
}
}
/**
* 外部調用的入口
* @since 3.2.2
*/
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
public <T> void addMapper(Class<T> type) {
// 對於mybatis mapper接口文件,必須是interface,不能是class
if (type.isInterface()) {
// 判重,確保只會加載一次不會被覆蓋
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 為mapper接口創建一個MapperProxyFactory代理
knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//剔除解析出現異常的接口
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
knownMappers是MapperRegistry的主要字段,維護了Mapper接口和代理類的映射關系,key是mapper接口類,value是MapperProxyFactory,其定義如下:
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
從定義看出,MapperProxyFactory主要是維護mapper接口的方法與對應mapper文件中具體CRUD節點的關聯關系。其中每個Method與對應MapperMethod維護在一起。MapperMethod是mapper中具體映射語句節點的內部表示。
首先為mapper接口創建MapperProxyFactory,然后創建MapperAnnotationBuilder進行具體的解析,MapperAnnotationBuilder在解析前的構造器中完成了下列工作:
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
String resource = type.getName().replace('.', '/') + ".java (best guess)";
this.assistant = new MapperBuilderAssistant(configuration, resource);
this.configuration = configuration;
this.type = type;
sqlAnnotationTypes.add(Select.class);
sqlAnnotationTypes.add(Insert.class);
sqlAnnotationTypes.add(Update.class);
sqlAnnotationTypes.add(Delete.class);
sqlProviderAnnotationTypes.add(SelectProvider.class);
sqlProviderAnnotationTypes.add(InsertProvider.class);
sqlProviderAnnotationTypes.add(UpdateProvider.class);
sqlProviderAnnotationTypes.add(DeleteProvider.class);
}
其中的MapperBuilderAssistant和XMLConfigBuilder一樣,都是繼承於BaseBuilder。Select.class/Insert.class等注解指示該方法對應的真實sql語句類型分別是select/insert。
SelectProvider.class/InsertProvider.class主要用於動態SQL,它們允許你指定一個類名和一個方法在具體執行時返回要運行的SQL語句。MyBatis會實例化這個類,然后執行指定的方法。
MapperBuilderAssistant初始化完成之后,就調用build.parse()進行具體的mapper接口文件加載與解析,如下所示:
public void parse() {
String resource = type.toString();
//首先根據mapper接口的字符串表示判斷是否已經加載,避免重復加載,正常情況下應該都沒有加載
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
// 每個mapper文件自成一個namespace,通常自動匹配就是這么來的,約定俗成代替人工設置最簡化常見的開發
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
整體流程為:
1、首先加載mapper接口對應的xml文件並解析。loadXmlResource和通過resource、url解析相同,都是解析mapper文件中的定義,他們的入口都是XMLMapperBuilder.parse(),我們稍等會兒專門專門分析,這一節先來看通過注解方式配置的mapper的解析(注:對於一個mapper接口,不能同時使用注解方式和xml方式,任何時候只能之一,但是不同的mapper接口可以混合使用這兩種方式)。
2、解析緩存注解;
mybatis中緩存注解的定義為:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace {
Class<? extends org.apache.ibatis.cache.Cache> implementation() default PerpetualCache.class;
Class<? extends org.apache.ibatis.cache.Cache> eviction() default LruCache.class;
long flushInterval() default 0;
int size() default 1024;
boolean readWrite() default true;
boolean blocking() default false;
/**
* Property values for a implementation object.
* @since 3.4.2
*/
Property[] properties() default {};
}
從上面的定義可以看出,和在XML中的是一一對應的。緩存的解析很簡單,這里不展開細細講解。
3、解析緩存參照注解。緩存參考的解析也很簡單,這里不展開細細講解。
4、解析非橋接方法。在正式開始之前,我們先來看下什么是橋接方法。橋接方法是 JDK 1.5 引入泛型后,為了使Java的泛型方法生成的字節碼和 1.5 版本前的字節碼相兼容,由編譯器自動生成的方法。那什么時候,編譯器會生成橋接方法呢,舉個例子,一個子類在繼承(或實現)一個父類(或接口)的泛型方法時,在子類中明確指定了泛型類型,那么在編譯時編譯器會自動生成橋接方法。參考:
http://blog.csdn.net/mhmyqn/article/details/47342577
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6
https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.4.5
所以正常情況下,只要在實現mybatis mapper接口的時候,沒有繼承根Mapper或者繼承了根Mapper但是沒有寫死泛型類型的時候,是不會成為橋接方法的。現在來看parseStatement的主要實現代碼(提示:因為注解方式通常不用於復雜的配置,所以這里我們進行簡單的解析,在XML部分進行詳細說明):
void parseStatement(Method method) {
// 獲取參數類型,如果有多個參數,這種情況下就返回org.apache.ibatis.binding.MapperMethod.ParamMap.class,ParamMap是一個繼承於HashMap的類,否則返回實際類型
Class<?> parameterTypeClass = getParameterType(method);
// 獲取語言驅動器
LanguageDriver languageDriver = getLanguageDriver(method);
// 獲取方法的SqlSource對象,只有指定了@Select/@Insert/@Update/@Delete或者對應的Provider的方法才會被當作mapper,否則只是和mapper文件中對應語句的一個運行時占位符
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
// 獲取方法的屬性設置,對應<select>中的各種屬性
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
// 獲取語句的CRUD類型
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
KeyGenerator keyGenerator;
String keyProperty = "id";
String keyColumn = null;
// 只有INSERT/UPDATE才解析SelectKey選項,總體來說,它的實現邏輯和XML基本一致,這里不展開詳述
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
keyGenerator = NoKeyGenerator.INSTANCE;
}
if (options != null) {
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
resultSetType = options.resultSetType();
}
String resultMapId = null;
// 解析@ResultMap注解,如果有@ResultMap注解,就是用它,否則才解析@Results
// @ResultMap注解用於給@Select和@SelectProvider注解提供在xml配置的<resultMap>,如果一個方法上同時出現@Results或者@ConstructorArgs等和結果映射有關的注解,那么@ResultMap會覆蓋后面兩者的注解
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
String[] resultMaps = resultMapAnnotation.value();
StringBuilder sb = new StringBuilder();
for (String resultMap : resultMaps) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(resultMap);
}
resultMapId = sb.toString();
} else if (isSelect) {
//如果是查詢,且沒有明確設置ResultMap,則根據返回類型自動解析生成ResultMap
resultMapId = parseResultMap(method);
}
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
重點來看沒有帶@ResultMap注解的查詢方法parseResultMap(Method):
private String parseResultMap(Method method) {
// 獲取方法的返回類型
Class<?> returnType = getReturnType(method);
// 獲取構造器
ConstructorArgs args = method.getAnnotation(ConstructorArgs.class);
// 獲取@Results注解,也就是注解形式的結果映射
Results results = method.getAnnotation(Results.class);
// 獲取鑒別器
TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
// 產生resultMapId
String resultMapId = generateResultMapName(method);
applyResultMap(resultMapId, returnType, argsIf(args), resultsIf(results), typeDiscriminator);
return resultMapId;
}
// 如果有resultMap設置了Id,就直接返回類名.resultMapId. 否則返回類名.方法名.以-分隔拼接的方法參數
private String generateResultMapName(Method method) {
Results results = method.getAnnotation(Results.class);
if (results != null && !results.id().isEmpty()) {
return type.getName() + "." + results.id();
}
StringBuilder suffix = new StringBuilder();
for (Class<?> c : method.getParameterTypes()) {
suffix.append("-");
suffix.append(c.getSimpleName());
}
if (suffix.length() < 1) {
suffix.append("-void");
}
return type.getName() + "." + method.getName() + suffix;
}
private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results, TypeDiscriminator discriminator) {
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
applyConstructorArgs(args, returnType, resultMappings);
applyResults(results, returnType, resultMappings);
Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator);
// TODO add AutoMappingBehaviour
assistant.addResultMap(resultMapId, returnType, null, disc, resultMappings, null);
createDiscriminatorResultMaps(resultMapId, returnType, discriminator);
}
private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
if (discriminator != null) {
// 對於鑒別器來說,和XML配置的差別在於xml中可以外部公用的resultMap,在注解中,則只提供了內嵌式的resultMap定義
for (Case c : discriminator.cases()) {
// 從內部實現的角度,因為內嵌式的resultMap定義也會創建resultMap,所以XML的實現也一樣,對於內嵌式鑒別器每個分支resultMap,其命名為映射方法的resultMapId-Case.value()。這樣在運行時,只要知道resultMap中包含了鑒別器之后,獲取具體的鑒別器映射就很簡單了,map.get()一下就得到了。
String caseResultMapId = resultMapId + "-" + c.value();
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
// issue #136
applyConstructorArgs(c.constructArgs(), resultType, resultMappings);
applyResults(c.results(), resultType, resultMappings);
// TODO add AutoMappingBehaviour
assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null);
}
}
}
private Discriminator applyDiscriminator(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
if (discriminator != null) {
String column = discriminator.column();
Class<?> javaType = discriminator.javaType() == void.class ? String.class : discriminator.javaType();
JdbcType jdbcType = discriminator.jdbcType() == JdbcType.UNDEFINED ? null : discriminator.jdbcType();
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>)
(discriminator.typeHandler() == UnknownTypeHandler.class ? null : discriminator.typeHandler());
Case[] cases = discriminator.cases();
Map<String, String> discriminatorMap = new HashMap<String, String>();
for (Case c : cases) {
String value = c.value();
String caseResultMapId = resultMapId + "-" + value;
discriminatorMap.put(value, caseResultMapId);
}
return assistant.buildDiscriminator(resultType, column, javaType, jdbcType, typeHandler, discriminatorMap);
}
return null;
}
5、二次解析pending的方法。
2.3 解析mapper文件XMLMapperBuilder
Mapper文件的解析主要由XMLMapperBuilder類完成,Mapper文件的加載流程如下:
我們以package掃描中的loadXmlResource()為入口開始。
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
根據package自動搜索加載的時候,約定俗稱從classpath下加載接口的完整名,比如org.mybatis.example.mapper.BlogMapper,就加載org/mybatis/example/mapper/BlogMapper.xml。對於從package和class進來的mapper,如果找不到對應的文件,就忽略,因為這種情況下是允許SQL語句作為注解打在接口上的,所以xml文件不是必須的,而對於直接聲明的xml mapper文件,如果找不到的話會拋出IOException異常而終止,這在使用注解模式的時候需要注意。加載到對應的mapper.xml文件后,調用XMLMapperBuilder進行解析。在創建XMLMapperBuilder時,我們發現用到了configuration.getSqlFragments(),這就是我們在mapper文件中經常使用的可以被包含在其他語句中的SQL片段,但是我們並沒有初始化過,所以很有可能它是在解析過程中動態添加的,創建了XMLMapperBuilder之后,在調用其parse()接口進行具體xml的解析,這和mybatis-config的邏輯基本上是一致的思路。再來看XMLMapperBuilder的初始化邏輯:
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
this(inputStream, configuration, resource, sqlFragments);
this.builderAssistant.setCurrentNamespace(namespace);
}
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
加載的基本邏輯和加載mybatis-config一樣的過程,使用XPathParser進行總控,XMLMapperEntityResolver進行具體判斷。
接下去來看XMLMapperBuilder.parse()的具體實現。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
其中,解析mapper的核心又在configurationElement中,如下所示:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
其主要過程是:
1、解析緩存參照cache-ref。參照緩存顧名思義,就是共用其他緩存的設置。
private void cacheRefElement(XNode context) {
if (context != null) {
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
<cache-ref namespace=”com.someone.application.data.SomeMapper”/>
緩存參考因為通過namespace指向其他的緩存。所以會出現第一次解析的時候指向的緩存還不存在的情況,所以需要在所有的mapper文件加載完成后進行二次處理,不僅僅是緩存參考,其他的CRUD也一樣。所以在XMLMapperBuilder.configuration中有很多的incompleteXXX,這種設計模式類似於JVM GC中的mark and sweep,標記、然后處理。所以當捕獲到IncompleteElementException異常時,沒有終止執行,而是將指向的緩存不存在的cacheRefResolver添加到configuration.incompleteCacheRef中。
2、解析緩存cache
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
默認情況下,mybatis使用的是永久緩存PerpetualCache,讀取或設置各個屬性默認值之后,調用builderAssistant.useNewCache構建緩存,其中的CacheBuilder使用了build模式(在effective里面,建議有4個以上可選屬性時,應該為對象提供一個builder便於使用),只要實現org.apache.ibatis.cache.Cache接口,就是合法的mybatis緩存。
我們先來看下緩存的DTD定義:
<!ELEMENT cache (property*)>
<!ATTLIST cache
type CDATA #IMPLIED
eviction CDATA #IMPLIED
flushInterval CDATA #IMPLIED
size CDATA #IMPLIED
readOnly CDATA #IMPLIED
blocking CDATA #IMPLIED
>
所以,最簡單的情況下只要聲明就可以啟用當前mapper下的緩存。
3、解析參數映射parameterMap
private void parameterMapElement(List<XNode> list) throws Exception {
for (XNode parameterMapNode : list) {
String id = parameterMapNode.getStringAttribute("id");
String type = parameterMapNode.getStringAttribute("type");
Class<?> parameterClass = resolveClass(type);
List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
for (XNode parameterNode : parameterNodes) {
String property = parameterNode.getStringAttribute("property");
String javaType = parameterNode.getStringAttribute("javaType");
String jdbcType = parameterNode.getStringAttribute("jdbcType");
String resultMap = parameterNode.getStringAttribute("resultMap");
String mode = parameterNode.getStringAttribute("mode");
String typeHandler = parameterNode.getStringAttribute("typeHandler");
Integer numericScale = parameterNode.getIntAttribute("numericScale");
ParameterMode modeEnum = resolveParameterMode(mode);
Class<?> javaTypeClass = resolveClass(javaType);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
parameterMappings.add(parameterMapping);
}
builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
}
}
<!ELEMENT parameterMap (parameter+)?>
<!ATTLIST parameterMap
id CDATA #REQUIRED
type CDATA #REQUIRED
>
<!ELEMENT parameter EMPTY>
<!ATTLIST parameter
property CDATA #REQUIRED
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
mode (IN | OUT | INOUT) #IMPLIED
resultMap CDATA #IMPLIED
scale CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
總體來說,目前已經不推薦使用參數映射,而是直接使用內聯參數。所以我們這里就不展開細講了。如有必要,我們后續會補上。
4、解析結果集映射resultMap
結果集映射早期版本可以說是用的最多的輔助節點了,不過有了mapUnderscoreToCamelCase屬性之后,如果命名規范控制做的好的話,resultMap也是可以省略的。每個mapper文件可以有多個結果集映射。最終來說,它還是使用頻率很高的。我們先來看下DTD定義:
<!ELEMENT resultMap (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ATTLIST resultMap
id CDATA #REQUIRED
type CDATA #REQUIRED
extends CDATA #IMPLIED
autoMapping (true|false) #IMPLIED
>
<!ELEMENT constructor (idArg*,arg*)>
<!ELEMENT id EMPTY>
<!ATTLIST id
property CDATA #IMPLIED
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
<!ELEMENT result EMPTY>
<!ATTLIST result
property CDATA #IMPLIED
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
<!ELEMENT idArg EMPTY>
<!ATTLIST idArg
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
select CDATA #IMPLIED
resultMap CDATA #IMPLIED
name CDATA #IMPLIED
>
<!ELEMENT arg EMPTY>
<!ATTLIST arg
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
select CDATA #IMPLIED
resultMap CDATA #IMPLIED
name CDATA #IMPLIED
>
<!ELEMENT collection (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ATTLIST collection
property CDATA #REQUIRED
column CDATA #IMPLIED
javaType CDATA #IMPLIED
ofType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
select CDATA #IMPLIED
resultMap CDATA #IMPLIED
typeHandler CDATA #IMPLIED
notNullColumn CDATA #IMPLIED
columnPrefix CDATA #IMPLIED
resultSet CDATA #IMPLIED
foreignColumn CDATA #IMPLIED
autoMapping (true|false) #IMPLIED
fetchType (lazy|eager) #IMPLIED
>
<!ELEMENT association (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ATTLIST association
property CDATA #REQUIRED
column CDATA #IMPLIED
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
select CDATA #IMPLIED
resultMap CDATA #IMPLIED
typeHandler CDATA #IMPLIED
notNullColumn CDATA #IMPLIED
columnPrefix CDATA #IMPLIED
resultSet CDATA #IMPLIED
foreignColumn CDATA #IMPLIED
autoMapping (true|false) #IMPLIED
fetchType (lazy|eager) #IMPLIED
>
<!ELEMENT discriminator (case+)>
<!ATTLIST discriminator
column CDATA #IMPLIED
javaType CDATA #REQUIRED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
<!ELEMENT case (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ATTLIST case
value CDATA #REQUIRED
resultMap CDATA #IMPLIED
resultType CDATA #IMPLIED
>
從DTD的復雜程度就可知,resultMap相當於前面的cache/parameterMap等來說,是相當靈活的。下面我們來看resultMap在運行時到底是如何表示的。
private void resultMapElements(List<XNode> list) throws Exception {
for (XNode resultMapNode : list) {
try {
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
// 在內部實現中將未完成的元素添加到configuration.incomplete中了
}
}
}
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
Class<?> typeClass = resolveClass(type);
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
List<XNode> argChildren = resultChild.getChildren();
for (XNode argChild : argChildren) {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
}
}
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String typeHandler = context.getStringAttribute("typeHandler");
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Map<String, String> discriminatorMap = new HashMap<String, String>();
for (XNode caseChild : context.getChildren()) {
String value = caseChild.getStringAttribute("value");
String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
discriminatorMap.put(value, resultMap);
}
return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}
我們先來看下ResultMapping的定義:
public class ResultMapping {
private Configuration configuration;
private String property;
private String column;
private Class<?> javaType;
private JdbcType jdbcType;
private TypeHandler<?> typeHandler;
private String nestedResultMapId;
private String nestedQueryId;
private Set<String> notNullColumns;
private String columnPrefix;
private List<ResultFlag> flags;
private List<ResultMapping> composites;
private String resultSet;
private String foreignColumn;
private boolean lazy;
...
}
所有下的最底層子元素比如、、等本質上都屬於一個映射,只不過有着額外的標記比如是否嵌套,是否構造器等。
總體邏輯是先解析resultMap節點本身,然后解析子節點構造器,鑒別器discriminator,id。最后組裝成真正的resultMappings。我們先來看個實際的復雜resultMap例子,便於我們更好的理解代碼的邏輯:
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
<arg column="blog_name" javaType="string"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
<result property="favouriteSection" column="author_favourite_section"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>
resultMap里面可以包含多種子節點,每個節點都有具體的方法進行解析,這也體現了單一職責原則。在resultMapElement中,主要是解析resultMap節點本身並循環遍歷委托給具體的方法處理。下面來看構造器的解析。構造器主要用於沒有默認構造器或者有多個構造器的情況,比如:
public class User {
//...
public User(Integer id, String username, int age) {
//...
}
//...
}
就可以使用下列的構造器設置屬性值,比如:
<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
<arg column="age" javaType="_int"/>
</constructor>
遍歷構造器元素很簡單:
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
List<XNode> argChildren = resultChild.getChildren();
for (XNode argChild : argChildren) {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
}
}
構造器的解析比較簡單,除了遍歷構造參數外,還可以構造器參數的ID也識別出來。最后調用buildResultMappingFromContext建立具體的resultMap。buildResultMappingFromContext是個公共工具方法,會被反復使用,我們來看下它的具體實現(不是所有元素都包含所有屬性):
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
property = context.getStringAttribute("property");
}
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");
// resultMap中可以包含association或collection復合類型,這些復合類型可以使用外部定義的公用resultMap或者內嵌resultMap, 所以這里的處理邏輯是如果有resultMap就獲取resultMap,如果沒有,那就動態生成一個。如果自動生成的話,他的resultMap id通過調用XNode.getValueBasedIdentifier()來獲得
String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.<ResultMapping> emptyList()));
String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
String typeHandler = context.getStringAttribute("typeHandler");
String resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
上述過程主要用於獲取各個屬性,其中唯一值得注意的是processNestedResultMappings,它用於解析包含的association或collection復合類型,這些復合類型可以使用外部定義的公用resultMap或者內嵌resultMap, 所以這里的處理邏輯是如果是外部resultMap就獲取對應resultMap的名稱,如果沒有,那就動態生成一個。如果自動生成的話,其resultMap id通過調用XNode.getValueBasedIdentifier()來獲得。由於colletion和association、discriminator里面還可以包含復合類型,所以將進行遞歸解析直到所有的子元素都為基本列位置,它在使用層面的目的在於將關系模型映射為對象樹模型。例如:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>
注意“ofType”屬性,這個屬性用來區分JavaBean(或字段)屬性類型和集合中存儲的對象類型。
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception {
if ("association".equals(context.getName())
|| "collection".equals(context.getName())
|| "case".equals(context.getName())) {
if (context.getStringAttribute("select") == null) {
ResultMap resultMap = resultMapElement(context, resultMappings);
return resultMap.getId();
}
}
return null;
}
對於其中的每個非select屬性映射,調用resultMapElement進行遞歸解析。其中的case節點主要用於鑒別器情況,后面我們會細講。
注:select的用途在於指定另外一個映射語句的ID,加載這個屬性映射需要的復雜類型。在列屬性中指定的列的值將被傳遞給目標 select 語句作為參數。在上面的例子中,id的值會作為selectPostsForBlog的參數,這個語句會為每條映射到blogResult的記錄執行一次selectPostsForBlog,並將返回的值添加到blog.posts屬性中,其類型為Post。
得到各屬性之后,調用builderAssistant.buildResultMapping最后創建ResultMap。其中除了 javaType,column外,其他都是可選的,property也就是中的name屬性或者中的property屬性,主要用於根據@Param或者jdk 8 -parameters形參名而非依賴聲明順序進行映射。
public ResultMapping buildResultMapping(
Class<?> resultType,
String property,
String column,
Class<?> javaType,
JdbcType jdbcType,
String nestedSelect,
String nestedResultMap,
String notNullColumn,
String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler,
List<ResultFlag> flags,
String resultSet,
String foreignColumn,
boolean lazy) {
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
List<ResultMapping> composites = parseCompositeColumnName(column);
return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
.jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
.resultSet(resultSet)
.typeHandler(typeHandlerInstance)
.flags(flags == null ? new ArrayList<ResultFlag>() : flags)
.composites(composites)
.notNullColumns(parseMultipleColumnNames(notNullColumn))
.columnPrefix(columnPrefix)
.foreignColumn(foreignColumn)
.lazy(lazy)
.build();
}
在實際使用中,一般不會在構造器中包含association和collection。
2.3.1 鑒別器discriminator的解析
鑒別器非常容易理解,它的表現很像Java語言中的switch語句。定義鑒別器也是通過column和javaType屬性來唯一標識,column是用來確定某個字段是否為鑒別器, JavaType是需要被用來保證等價測試的合適類型。例如:
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultMap="carResult"/>
<case value="2" resultMap="truckResult"/>
<case value="3" resultMap="vanResult"/>
<case value="4" resultMap="suvResult"/>
</discriminator>
對於上述的鑒別器,如果 vehicle_type=1, 那就會使用下列這個結果映射。
<resultMap id="carResult" type="Car">
<result property="doorCount" column="door_count" />
</resultMap>
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String typeHandler = context.getStringAttribute("typeHandler");
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Map<String, String> discriminatorMap = new HashMap<String, String>();
for (XNode caseChild : context.getChildren()) {
String value = caseChild.getStringAttribute("value");
String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
discriminatorMap.put(value, resultMap);
}
return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}
其邏輯和之前處理構造器的時候類似,同樣的使用build建立鑒別器並返回鑒別器實例,鑒別器中也可以嵌套association和collection。他們的實現邏輯和常規resultMap中的處理方式完全相同,這里就不展開重復講。和構造器不一樣的是,鑒別器中包含了case分支和對應的resultMap的映射。
最后所有的子節點都被解析到resultMappings中, 在解析完整個resultMap中的所有子元素之后,調用ResultMapResolver進行解析,如下所示:
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
// 將id/extend填充為完整模式,也就是帶命名空間前綴,true不需要和當前resultMap所在的namespace相同,比如extend和cache,否則只能是當前的namespace
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
// 首先檢查繼承的resultMap是否已存在,如果不存在則標記為incomplete,會進行二次處理
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
ResultMap resultMap = configuration.getResultMap(extend);
List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(resultMap.getResultMappings());
// 剔除所繼承的resultMap里已經在當前resultMap中的那個基本映射
extendedResultMappings.removeAll(resultMappings);
// Remove parent constructor if this resultMap declares a constructor.
// 如果本resultMap已經包含了構造器,則剔除繼承的resultMap里面的構造器
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
Iterator<ResultMapping> extendedResultMappingsIter = extendedResultMappings.iterator();
while (extendedResultMappingsIter.hasNext()) {
if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {
extendedResultMappingsIter.remove();
}
}
}
// 都處理完成之后,將繼承的resultMap里面剩下那部分不重復的resultMap子元素添加到當前的resultMap中,所以這個addResultMap方法的用途在於啟動時就創建了完整的resultMap,這樣運行時就不需要去檢查繼承的映射和構造器,有利於性能提升。
resultMappings.addAll(extendedResultMappings);
}
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
configuration.addResultMap(resultMap);
return resultMap;
}
mybatis源碼分析到這里,應該來說上面部分代碼不是特別顯而易見,它主要是處理extends這個屬性,將resultMap各子元素節點的去對象關系,也就是去重、合並屬性、構造器。處理完繼承的邏輯之后,調用了ResultMap.Builder.build()進行最后的resultMap構建。build應該來說是真正創建根resultMap對象的邏輯入口。我們先看下ResultMap的定義:
public class ResultMap {
private Configuration configuration;
// resultMap的id屬性
private String id;
// resultMap的type屬性,有可能是alias
private Class<?> type;
// resultMap下的所有節點
private List<ResultMapping> resultMappings;
// resultMap下的id節點比如<id property="id" column="user_id" />
private List<ResultMapping> idResultMappings;
// resultMap下的構造器節點<constructor>
private List<ResultMapping> constructorResultMappings;
// resultMap下的property節點比如<result property="password" column="hashed_password"/>
private List<ResultMapping> propertyResultMappings;
//映射的列名
private Set<String> mappedColumns;
// 映射的javaBean屬性名,所有映射不管是id、構造器或者普通的
private Set<String> mappedProperties;
// 鑒別器
private Discriminator discriminator;
// 是否有嵌套的resultMap比如association或者collection
private boolean hasNestedResultMaps;
// 是否有嵌套的查詢,也就是select屬性
private boolean hasNestedQueries;
// autoMapping屬性,這個屬性會覆蓋全局的屬性autoMappingBehavior
private Boolean autoMapping;
...
}
其中定義了節點下所有的子元素,以及必要的輔助屬性,包括最重要的各種ResultMapping。
再來看下build的實現:
public ResultMap build() {
if (resultMap.id == null) {
throw new IllegalArgumentException("ResultMaps must have an id");
}
resultMap.mappedColumns = new HashSet<String>();
resultMap.mappedProperties = new HashSet<String>();
resultMap.idResultMappings = new ArrayList<ResultMapping>();
resultMap.constructorResultMappings = new ArrayList<ResultMapping>();
resultMap.propertyResultMappings = new ArrayList<ResultMapping>();
final List<String> constructorArgNames = new ArrayList<String>();
for (ResultMapping resultMapping : resultMap.resultMappings) {
// 判斷是否有嵌套查詢, nestedQueryId是在buildResultMappingFromContext的時候通過讀取節點的select屬性得到的
resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
// 判斷是否嵌套了association或者collection, nestedResultMapId是在buildResultMappingFromContext的時候通過讀取節點的resultMap屬性得到的或者內嵌resultMap的時候自動計算得到的。注:這里的resultSet沒有地方set進來,DTD中也沒有看到,不確定是不是有意預留的,但是association/collection的子元素中倒是有聲明
resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
// 獲取column屬性, 包括復合列,復合列是在org.apache.ibatis.builder.MapperBuilderAssistant.parseCompositeColumnName(String)中解析的。所有的數據庫列都被按順序添加到resultMap.mappedColumns中
final String column = resultMapping.getColumn();
if (column != null) {
resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
} else if (resultMapping.isCompositeResult()) {
for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
final String compositeColumn = compositeResultMapping.getColumn();
if (compositeColumn != null) {
resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
}
}
}
// 所有映射的屬性都被按順序添加到resultMap.mappedProperties中,ID單獨存儲
final String property = resultMapping.getProperty();
if(property != null) {
resultMap.mappedProperties.add(property);
}
// 所有映射的構造器被按順序添加到resultMap.constructorResultMappings
// 如果本元素具有CONSTRUCTOR標記,則添加到構造函數參數列表,否則添加到普通屬性映射列表resultMap.propertyResultMappings
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
resultMap.constructorResultMappings.add(resultMapping);
if (resultMapping.getProperty() != null) {
constructorArgNames.add(resultMapping.getProperty());
}
} else {
resultMap.propertyResultMappings.add(resultMapping);
}
// 如果本元素具有ID標記, 則添加到ID映射列表resultMap.idResultMappings
if (resultMapping.getFlags().contains(ResultFlag.ID)) {
resultMap.idResultMappings.add(resultMapping);
}
}
// 如果沒有聲明ID屬性,就把所有屬性都作為ID屬性
if (resultMap.idResultMappings.isEmpty()) {
resultMap.idResultMappings.addAll(resultMap.resultMappings);
}
// 根據聲明的構造器參數名和類型,反射聲明的類,檢查其中是否包含對應參數名和類型的構造器,如果不存在匹配的構造器,就拋出運行時異常,這是為了確保運行時不會出現異常
if (!constructorArgNames.isEmpty()) {
final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
if (actualArgNames == null) {
throw new BuilderException("Error in result map '" + resultMap.id
+ "'. Failed to find a constructor in '"
+ resultMap.getType().getName() + "' by arg names " + constructorArgNames
+ ". There might be more info in debug log.");
}
//構造器參數排序
Collections.sort(resultMap.constructorResultMappings, new Comparator<ResultMapping>() {
@Override
public int compare(ResultMapping o1, ResultMapping o2) {
int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
return paramIdx1 - paramIdx2;
}
});
}
// lock down collections
// 為了避免用於無意或者有意事后修改resultMap的內部結構, 克隆一個不可修改的集合提供給用戶
resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
return resultMap;
}
private List<String> argNamesOfMatchingConstructor(List<String> constructorArgNames) {
Constructor<?>[] constructors = resultMap.type.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
Class<?>[] paramTypes = constructor.getParameterTypes();
if (constructorArgNames.size() == paramTypes.length) {
List<String> paramNames = getArgNames(constructor);
if (constructorArgNames.containsAll(paramNames)
&& argTypesMatch(constructorArgNames, paramTypes, paramNames)) {
return paramNames;
}
}
}
return null;
}
private boolean argTypesMatch(final List<String> constructorArgNames,
Class<?>[] paramTypes, List<String> paramNames) {
for (int i = 0; i < constructorArgNames.size(); i++) {
Class<?> actualType = paramTypes[paramNames.indexOf(constructorArgNames.get(i))];
Class<?> specifiedType = resultMap.constructorResultMappings.get(i).getJavaType();
if (!actualType.equals(specifiedType)) {
if (log.isDebugEnabled()) {
log.debug("While building result map '" + resultMap.id
+ "', found a constructor with arg names " + constructorArgNames
+ ", but the type of '" + constructorArgNames.get(i)
+ "' did not match. Specified: [" + specifiedType.getName() + "] Declared: ["
+ actualType.getName() + "]");
}
return false;
}
}
return true;
}
private List<String> getArgNames(Constructor<?> constructor) {
List<String> paramNames = new ArrayList<String>();
List<String> actualParamNames = null;
final Annotation[][] paramAnnotations = constructor.getParameterAnnotations();
int paramCount = paramAnnotations.length;
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
name = ((Param) annotation).value();
break;
}
}
if (name == null && resultMap.configuration.isUseActualParamName() && Jdk.parameterExists) {
if (actualParamNames == null) {
actualParamNames = ParamNameUtil.getParamNames(constructor);
}
if (actualParamNames.size() > paramIndex) {
name = actualParamNames.get(paramIndex);
}
}
paramNames.add(name != null ? name : "arg" + paramIndex);
}
return paramNames;
}
}
ResultMap構建完成之后,添加到Configuration的resultMaps中。我們發現在addResultMap方法中,還有兩個針對鑒別器嵌套resultMap處理:
public void addResultMap(ResultMap rm) {
resultMaps.put(rm.getId(), rm);
// 檢查本resultMap內的鑒別器有沒有嵌套resultMap
checkLocallyForDiscriminatedNestedResultMaps(rm);
// 檢查所有resultMap的鑒別器有沒有嵌套resultMap
checkGloballyForDiscriminatedNestedResultMaps(rm);
}
應該來說,設置resultMap的鑒別器有沒有嵌套的resultMap在解析resultMap子元素的時候就可以設置,當然放在最后統一處理也未嘗不可,也不見得放在這里就一定更加清晰,只能說實現的方式有多種。
到此為止,一個根resultMap的解析就完整的結束了。不得不說resutMap的實現確實是很復雜。
5、解析sql片段
sql元素可以被用來定義可重用的SQL代碼段,包含在其他語句中。比如,他常被用來定義重用的列:
<sql id=”userColumns”> id,username,password </sql>
sql片段的解析邏輯為:
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
sqlFragments.put(id, context);
}
}
}
前面講過,mybatis可以根據不同的數據庫執行不同的sql,這就是通過sql元素上的databaseId屬性來區別的。同樣,首先設置sql元素的id,它必須是當前mapper文件所定義的命名空間。sql元素本身的處理很簡單,只是簡單的過濾出databaseId和當前加載的配置文件相同的語句以備以后再解析crud遇到時進行引用。之所以不進行解析,是因為首先能夠作為sql元素子元素的所有節點都可以作為crud的子元素,而且sql元素不會在運行時單獨使用,所以也沒有必要專門解析一番。下面我們重點來看crud的解析與內部表示。
6、解析CRUD語句
CRUD是SQL的核心部分,也是mybatis針對用戶提供的最有價值的部分,所以研究源碼有必要對CRUD的完整實現進行分析。不理解這一部分的核心實現,就談不上對mybatis的實現有深刻理解。CRUD解析和加載的整體流程如下:
crud的解析從buildStatementFromContext(context.evalNodes(“select|insert|update|delete”));語句開始,透過調用調用鏈,我們可以得知SQL語句的解析主要在XMLStatementBuilder中實現。
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
在看具體實現之前,我們先大概看下DTD的定義(因為INSERT/UPDATE/DELETE屬於一種類型,所以我們以INSERT為例),這樣我們就知道整體關系和脈絡:
<!ELEMENT select (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
<!ATTLIST select
id CDATA #REQUIRED
parameterMap CDATA #IMPLIED
parameterType CDATA #IMPLIED
resultMap CDATA #IMPLIED
resultType CDATA #IMPLIED
resultSetType (FORWARD_ONLY | SCROLL_INSENSITIVE | SCROLL_SENSITIVE) #IMPLIED
statementType (STATEMENT|PREPARED|CALLABLE) #IMPLIED
fetchSize CDATA #IMPLIED
timeout CDATA #IMPLIED
flushCache (true|false) #IMPLIED
useCache (true|false) #IMPLIED
databaseId CDATA #IMPLIED
lang CDATA #IMPLIED
resultOrdered (true|false) #IMPLIED
resultSets CDATA #IMPLIED
>
<!ELEMENT insert (#PCDATA | selectKey | include | trim | where | set | foreach | choose | if | bind)*>
<!ATTLIST insert
id CDATA #REQUIRED
parameterMap CDATA #IMPLIED
parameterType CDATA #IMPLIED
timeout CDATA #IMPLIED
flushCache (true|false) #IMPLIED
statementType (STATEMENT|PREPARED|CALLABLE) #IMPLIED
keyProperty CDATA #IMPLIED
useGeneratedKeys (true|false) #IMPLIED
keyColumn CDATA #IMPLIED
databaseId CDATA #IMPLIED
lang CDATA #IMPLIED
>
<!ELEMENT selectKey (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
<!ATTLIST selectKey
resultType CDATA #IMPLIED
statementType (STATEMENT|PREPARED|CALLABLE) #IMPLIED
keyProperty CDATA #IMPLIED
keyColumn CDATA #IMPLIED
order (BEFORE|AFTER) #IMPLIED
databaseId CDATA #IMPLIED
>
我們先來看statementParser.parseStatementNode()的實現主體部分。
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
// MyBatis 從 3.2 開始支持可插拔的腳本語言,因此你可以在插入一種語言的驅動(language driver)之后來寫基於這種語言的動態 SQL 查詢。
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
// 結果集的類型,FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一個,默認值為 unset (依賴驅動)。
String resultSetType = context.getStringAttribute("resultSetType");
// 解析crud語句的類型,mybatis目前支持三種,prepare、硬編碼、以及存儲過程調用
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
// 解析SQL命令類型,目前主要有UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// insert/delete/update后是否刷新緩存
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// select是否使用緩存
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
// 這個設置僅針對嵌套結果 select 語句適用:如果為 true,就是假設包含了嵌套結果集或是分組了,這樣的話當返回一個主結果行的時候,就不會發生有對前面結果集的引用的情況。這就使得在獲取嵌套的結果集的時候不至於導致內存不夠用。默認值:false。我猜測這個屬性為true的意思是查詢的結果字段根據定義的嵌套resultMap進行了排序,后面在分析sql執行源碼的時候,我們會具體看到他到底是干嗎用的
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
// 解析語句中包含的sql片段,也就是
// <select id="select" resultType="map">
// select
// field1, field2, field3
// <include refid="someinclude"></include>
// </select>
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
從DTD可以看出,sql/crud元素里面可以包含這些元素:#PCDATA(文本節點) | include | trim | where | set | foreach | choose | if | bind,除了文本節點外,其他類型的節點都可以嵌套,我們看下解析包含的sql片段的邏輯。
/**
* Recursively apply includes through all SQL fragments.
* @param source Include node in DOM tree
* @param variablesContext Current context for static variables with values
*/
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && source.getNodeType() == Node.TEXT_NODE
&& !variablesContext.isEmpty()) {
// replace variables in text node
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
總的來說,將節點分為文本節點、include、非include三類進行處理。因為一開始傳遞進來的是CRUD節點本身,所以第一次執行的時候,是第一個else if,也就是source.getNodeType() == Node.ELEMENT_NODE,然后在這里開始遍歷所有的子節點。
對於include節點:根據屬性refid調用findSqlFragment找到sql片段,對節點中包含的占位符進行替換解析,然后調用自身進行遞歸解析,解析到文本節點返回之后。判斷下include的sql片段是否和包含它的節點是同一個文檔,如果不是,則把它從原來的文檔包含進來。然后使用include指向的sql節點替換include節點,最后剝掉sql節點本身,也就是把sql下的節點上移一層,這樣就合法了。舉例來說,這里完成的功能就是把:
<sql id=”userColumns”> id,username,password </sql>
<select id=”selectUsers” parameterType=”int” resultType=”hashmap”>
select <include refid=”userColumns”/>
from some_table
where id = #{id}
</select>
轉換為下面的形式:
<select id=”selectUsers” parameterType=”int” resultType=”hashmap”>
select id,username,password
from some_table
where id = #{id}
</select>
對於文本節點:根據入參變量上下文將變量設置替換進去;
對於其他節點:首先判斷是否為根節點,如果是非根且變量上下文不為空,則先解析屬性值上的占位符。然后對於子節點,遞歸進行調用直到所有節點都為文本節點為止。
上述解析完成之后,CRUD就沒有嵌套的sql片段了,這樣就可以進行直接解析了。現在,我們回到parseStatementNode()剛才中止的部分繼續往下看。接下去是解析selectKey節點。selectKey節點用於支持數據庫比如Oracle不支持自動生成主鍵,或者可能JDBC驅動不支持自動生成主鍵時的情況。對於數據庫支持自動生成主鍵的字段(比如MySQL和SQL Server),那么你可以設置useGeneratedKeys=”true”,而且設置keyProperty到你已經做好的目標屬性上就可以了,不需要使用selectKey節點。由於已經處理了SQL片段節點,當前在處理CRUD節點,所以先將包含的SQL片段展開,然后解析selectKey是正確的,因為selectKey可以包含在SQL片段中。
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
String resultType = nodeToHandle.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
//defaults
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
id = builderAssistant.applyCurrentNamespace(id, false);
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
解析SelectKey節點總的來說比較簡單:
1、加載相關屬性;
2、使用語言驅動器創建SqlSource,關於創建SqlSource的細節,我們會在下一節詳細分析mybatis語言驅動器時詳細展開。SqlSource是一個接口,它代表了從xml或者注解上讀取到的sql映射語句的內容,其中參數使用占位符進行了替換,在運行時,其代表的SQL會發送給數據庫,如下:
/**
* Represents the content of a mapped statement read from an XML file or an annotation.
* It creates the SQL that will be passed to the database out of the input parameter received from the user.
*
* @author Clinton Begin
*/
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
mybatis提供了兩種類型的實現org.apache.ibatis.scripting.xmltags.DynamicSqlSource和org.apache.ibatis.scripting.defaults.RawSqlSource。
3、SelectKey在實現上內部和其他的CRUD一樣,被當做一個MappedStatement進行存儲;我們來看下MappedStatement的具體構造以及代表SelectKey的sqlSource是如何組裝成MappedStatement的。
public final class MappedStatement {
private String resource;
private Configuration configuration;
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType;
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
...
}
對於每個語句而言,在運行時都需要知道結果映射,是否使用緩存,語句類型,sql文本,超時時間,是否自動生成key等等。為了方便運行時的使用以及高效率,MappedStatement被設計為直接包含了所有這些屬性。
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
// 創建語句參數映射
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
MappedStatement本身構建過程很簡單,沒什么復雜的邏輯,關鍵部分都在SqlSource的創建上。其中configuration.addMappedStatement里面進行了防重復處理。
4、最后為SelectKey對應的sql語句創建並維護一個KeyGenerator。
解析SQL主體
這一部分到mybatis語言驅動器一節一起講解。
到此為止,crud部分的解析和加載就完成了。現在我們來看看這個語言驅動器。
sql語句解析的核心:mybatis語言驅動器XMLLanguageDriver
雖然官方名稱叫做LanguageDriver,其實叫做解析器可能更加合理。MyBatis 從 3.2 開始支持可插拔的腳本語言,因此你可以在插入一種語言的驅動(language driver)之后來寫基於這種語言的動態 SQL 查詢比如mybatis除了XML格式外,還提供了mybatis-velocity,允許使用velocity表達式編寫SQL語句。可以通過實現LanguageDriver接口的方式來插入一種語言:
public interface LanguageDriver {
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
一旦有了自定義的語言驅動,你就可以在 mybatis-config.xml 文件中將它設置為默認語言:
<typeAliases>
<typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
<setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>
除了設置默認語言,你也可以針對特殊的語句指定特定語言,這可以通過如下的 lang 屬性來完成:
<select id="selectBlog" lang="myLanguage">
SELECT * FROM BLOG
</select>
默認情況下,mybatis使用org.apache.ibatis.scripting.xmltags.XMLLanguageDriver。通過調用createSqlSource方法來創建SqlSource,如下:
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
他有兩個重載版本,第二個主要是傳遞文本的格式,所以我們重點分析第一個版本。XMLScriptBuilder構造器中設置相關字段外,還硬編碼了每個支持的動態元素對應的處理器。如果我們要支持額外的動態元素比如else/elsif,只要在map中添加對應的key/value對即可。
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
這里采用了內部類的設計,內部類只有在有外部類實例的情況下才會存在。在這里因為內部類不會單獨被使用,所以應該設計為內部類而不是內部靜態類。每個動態元素處理類都實現了NodeHandler接口且有對應的SqlNode比如IfSqlNode。
現在來看parseScriptNode()的實現:
public SqlSource parseScriptNode() {
// 解析動態標簽
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
// 動態標簽解析實現
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) { // 判斷文本節點中是否包含了${},如果包含則為動態文本節點,否則為靜態文本節點,靜態文本節點在運行時不需要二次處理
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 標簽節點
String nodeName = child.getNode().getNodeName();
// 首先根據節點名稱獲取到對應的節點處理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 使用對應的節點處理器處理本節點
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
在解析mapper語句的時候,很重要的一個步驟是解析動態標簽(動態指的是SQL文本里面包含了${}動態變量或者包含等元素的sql節點,它將合成為SQL語句的一部分發送給數據庫),然后根據是否動態sql決定實例化的SqlSource為DynamicSqlSource或RawSqlSource。
可以說,mybatis是通過動態標簽的實現來解決傳統JDBC編程中sql語句拼接這個步驟的(就像現代web前端開發使用模板或vdom自動綁定代替jquery字符串拼接一樣),mybatis動態標簽被設計為可以相互嵌套,所以對於動態標簽的解析需要遞歸直到解析至文本節點。一個映射語句下可以包含多個根動態標簽,因此最后返回的是一個MixedSqlNode,其中有一個List類型的屬性,包含樹狀層次嵌套的多種SqlNode實現類型的列表,也就是單向鏈表的其中一種衍生形式。MixedSqlNode的定義如下:
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
// 遍歷每個根SqlNode
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
這樣在運行的時候,就只要根據對應的表達式結果(mybatis采用ONGL作為動態表達式語言)調用下一個SqlNode進行計算即可。
IfHandler
IfHandler的實現如下:
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 獲取if屬性的值,將值設置為IfSqlNode的屬性,便於運行時解析
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
OtherwiseHandler
otherwise標簽可以說並不是一個真正有意義的標簽,它不做任何處理,用在choose標簽的最后默認分支,如下所示:
private class OtherwiseHandler implements NodeHandler {
public OtherwiseHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
targetContents.add(mixedSqlNode);
}
}
BindHandler
bind 元素可以使用 OGNL 表達式創建一個變量並將其綁定到當前SQL節點的上下文。
<select id="selectBlogsLike" parameterType="BlogQuery" resultType="Blog">
<bind name="pattern" value="'%' + title + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
對於這種情況,bind還可以用來預防 SQL 注入。
private class BindHandler implements NodeHandler {
public BindHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 變量名稱
final String name = nodeToHandle.getStringAttribute("name");
// OGNL表達式
final String expression = nodeToHandle.getStringAttribute("value");
final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
targetContents.add(node);
}
}
ChooseHandler
choose節點應該說和switch是等價的,其中的when就是各種條件判斷。如下所示:
private class ChooseHandler implements NodeHandler {
public ChooseHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> whenSqlNodes = new ArrayList<SqlNode>();
List<SqlNode> otherwiseSqlNodes = new ArrayList<SqlNode>();
// 拆分出when 和 otherwise 節點
handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
targetContents.add(chooseSqlNode);
}
private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) {
List<XNode> children = chooseSqlNode.getChildren();
for (XNode child : children) {
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler instanceof IfHandler) {
handler.handleNode(child, ifSqlNodes);
} else if (handler instanceof OtherwiseHandler) {
handler.handleNode(child, defaultSqlNodes);
}
}
}
private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
SqlNode defaultSqlNode = null;
if (defaultSqlNodes.size() == 1) {
defaultSqlNode = defaultSqlNodes.get(0);
} else if (defaultSqlNodes.size() > 1) {
throw new BuilderException("Too many default (otherwise) elements in choose statement.");
}
return defaultSqlNode;
}
}
ForEachHandler
foreach可以將任何可迭代對象(如列表、集合等)和任何的字典或者數組對象傳遞給foreach作為集合參數。當使用可迭代對象或者數組時,index是當前迭代的次數,item的值是本次迭代獲取的元素。當使用字典(或者Map.Entry對象的集合)時,index是鍵,item是值。
private class ForEachHandler implements NodeHandler {
public ForEachHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String collection = nodeToHandle.getStringAttribute("collection");
String item = nodeToHandle.getStringAttribute("item");
String index = nodeToHandle.getStringAttribute("index");
String open = nodeToHandle.getStringAttribute("open");
String close = nodeToHandle.getStringAttribute("close");
String separator = nodeToHandle.getStringAttribute("separator");
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
SetHandler
set標簽主要用於解決update動態字段,例如:
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
一般來說,在實際中我們應該增加至少一個額外的最后更新時間字段(mysql內置)或者更新人比較合適,並不需要使用動態set。
private class SetHandler implements NodeHandler {
public SetHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
targetContents.add(set);
}
}
因為在set中內容為空的時候,set會被trim掉,所以set實際上是trim的一種特殊實現。
TrimHandler
trim使用最多的情況是截掉where條件中的前置OR和AND,update的set子句中的后置”,”,同時在內容不為空的時候加上where和set。
比如:
select * from user
<trim prefix="WHERE" prefixoverride="AND |OR">
<if test="name != null and name.length()>0"> AND name=#{name}</if>
<if test="gender != null and gender.length()>0"> AND gender=#{gender}</if>
</trim>
update user
<trim prefix="set" suffixoverride="," suffix=" where id = #{id} ">
<if test="name != null and name.length()>0"> name=#{name} , </if>
<if test="gender != null and gender.length()>0"> gender=#{gender} , </if>
</trim>
雖然這兩者都可以通過非動態標簽來解決。從性能角度來說,這兩者是可以避免的。
private class TrimHandler implements NodeHandler {
public TrimHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 包含的子節點解析后SQL文本不為空時要添加的前綴內容
String prefix = nodeToHandle.getStringAttribute("prefix");
// 要覆蓋的子節點解析后SQL文本前綴內容
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
// 包含的子節點解析后SQL文本不為空時要添加的后綴內容
String suffix = nodeToHandle.getStringAttribute("suffix");
// 要覆蓋的子節點解析后SQL文本后綴內容
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
WhereHandler
和set一樣,where也是trim的特殊情況,同樣where標簽也不是必須的,可以通過方式解決。
public class WhereSqlNode extends TrimSqlNode {
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
靜態SQL創建RawSqlSource
先來看靜態SQL的創建,這是兩方面原因:一、靜態SQL是動態SQL的一部分;二、靜態SQL最終被傳遞給JDBC原生API發送到數據庫執行。
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}
...
}
其中的關鍵之處在getSql()方法的邏輯實現,因為給DynamicContext()構造器傳遞的parameterObject為空,所以沒有參數,也不需要反射,反之就通過反射將object轉為map。因為rootSqlNode是StaticTextSqlNode類型,所以getSql就直接返回原文本,隨后調用第二個構造器,首先創建一個SqlSourceBuilder實例,然后調用其parse()方法,其中ParameterMappingTokenHandler符號處理器的目的是把sql參數解析出來,如下所示:
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
其中參數的具體解析在ParameterMappingTokenHandler.buildParameterMapping()中,如下:
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + parameterProperties);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}
其總體邏輯還是比較簡單的,唯一值得注意的是,只不過在這里可以看出每個參數被綁定的javaType獲取的優先級分別為:#{}中明確指定的,調用parse時傳遞的map(metaParameters)中包含的(這主要用於動態SQL的場景),調用parse時傳遞的參數類型(如果該類型已經在typeHandlerRegistry中的話),參數指定的jdbcType類型為JdbcType.CURSOR,Object.class(如果該類型是Map的子類或者參數本身為null),參數包含在調用parse時傳遞的參數類型對象的字段中,最后是Object.class。最后構建出ParameterMapping,如下所示:
public class ParameterMapping {
private Configuration configuration;
private String property;
private ParameterMode mode;
private Class<?> javaType = Object.class;
private JdbcType jdbcType;
private Integer numericScale;
private TypeHandler<?> typeHandler;
private String resultMapId;
private String jdbcTypeName;
private String expression;
...
}
對於下列調用:
RawSqlSource rawSqlSource = new RawSqlSource(conf, "SELECT * FROM PERSON WHERE ID = #{id}", int.class);
將返回
sql語句:SELECT * FROM PERSON WHERE ID = ?
以及參數列表:[ParameterMapping{property='id', mode=IN, javaType=int, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}]
到此為止,靜態SQL的解析就完成了。
動態SQL創建 DynamicSqlSource
再來看動態SQL的創建。動態SQL主要在運行時由上下文調用SqlSource.getBoundSql()接口獲取。它在處理了動態標簽以及${}之后,調用靜態SQL構建器返回PreparedStatement,也就是靜態SQL形式。
到此為止,整個mapper文件的第一輪解析就完成了。
4、二次解析未完成的結果映射、緩存參照、CRUD語句;
在第一次解析期間,有可能因為加載順序或者其他原因,導致部分依賴的resultMap或者其他沒有完全完成解析,所以針對pending的結果映射、緩存參考、CRUD語句進行遞歸重新解析直到完成。
3 關鍵對象總結與回顧
3.1 SqlSource
SqlSource是XML文件或者注解方法中映射語句的實現時表示,通過SqlSourceBuilder.parse()方法創建,SqlSourceBuilder中符號解析器將mybatis中的查詢參數#{}轉換為?,並記錄了參數的順序。它只有一個方法getBoundSql用於獲取映射語句對象的各個組成部分,它的定義如下:
/**
* Represents the content of a mapped statement read from an XML file or an annotation.
* It creates the SQL that will be passed to the database out of the input parameter received from the user.
* 代表從XML文件或者注解讀取的映射語句的內容,它創建的SQL會被傳遞給數據庫。
* @author Clinton Begin
*/
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
根據SQL語句的類型不同,mybatis提供了多種SqlSource的具體實現,如下所示:
- StaticSqlSource:最終靜態SQL語句的封裝,其他類型的SqlSource最終都委托給StaticSqlSource。
- RawSqlSource:原始靜態SQL語句的封裝,在加載時就已經確定了SQL語句,沒有、等動態標簽和${} SQL拼接,比動態SQL語句要快,因為不需要運行時解析SQL節點。
- DynamicSqlSource:動態SQL語句的封裝,在運行時需要根據參數處理、等標簽或者${} SQL拼接之后才能生成最后要執行的靜態SQL語句。
- ProviderSqlSource:當SQL語句通過指定的類和方法獲取時(使用@XXXProvider注解),需要使用本類,它會通過反射調用相應的方法得到SQL語句。
3.2 SqlNode
SqlNode接口主要用來處理CRUD節點下的各類動態標簽比如、,對每個動態標簽,mybatis都提供了對應的SqlNode實現,這些動態標簽可以相互嵌套且實現上采用單向鏈表進行應用,這樣后面如果需要增加其他動態標簽,就只需要新增對應的SqlNode實現就能支持。mybatis使用OGNL表達式語言。對sqlNode的調用在SQL執行期間的DynamicSqlSource.getBoundSql()方法中,SQL執行過程我們后面會講解。
當前版本的SqlNode有下列實現:
其中MixedSqlNode代表了所有具體SqlNode的集合,其他分別代表了一種類型的SqlNode。下面對每個SqlNode的實現做簡單的分析:
ChooseSqlNode
public class ChooseSqlNode implements SqlNode {
private final SqlNode defaultSqlNode;
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
@Override
public boolean apply(DynamicContext context) {
// 遍歷所有when分支節點,只要遇到第一個為true就返回
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
// 全部when都為false時,走otherwise分支
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
}
ForEachSqlNode
public class ForEachSqlNode implements SqlNode {
public static final String ITEM_PREFIX = "__frch_";
private final ExpressionEvaluator evaluator;
private final String collectionExpression;
private final SqlNode contents;
private final String open;
private final String close;
private final String separator;
private final String item;
private final String index;
private final Configuration configuration;
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
this.evaluator = new ExpressionEvaluator();
this.collectionExpression = collectionExpression;
this.contents = contents;
this.open = open;
this.close = close;
this.separator = separator;
this.index = index;
this.item = item;
this.configuration = configuration;
}
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
// 將Map/Array/List統一包裝為迭代器接口
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
int i = 0;
// 遍歷集合
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) { //Map條目處理
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else { // List條目處理
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
// 子節點SqlNode處理,很重要的一個邏輯就是將#{item.XXX}轉換為#{__frch_item_N.XXX},這樣在JDBC設置參數的時候就能夠找到對應的參數值了
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
context.bind(index, o);
context.bind(itemizeItem(index, i), o);
}
}
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
}
private void applyOpen(DynamicContext context) {
if (open != null) {
context.appendSql(open);
}
}
private void applyClose(DynamicContext context) {
if (close != null) {
context.appendSql(close);
}
}
private static String itemizeItem(String item, int i) {
return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
}
private static class FilteredDynamicContext extends DynamicContext {
private final DynamicContext delegate;
private final int index;
private final String itemIndex;
private final String item;
public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) {
super(configuration, null);
this.delegate = delegate;
this.index = i;
this.itemIndex = itemIndex;
this.item = item;
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public String getSql() {
return delegate.getSql();
}
@Override
public void appendSql(String sql) {
GenericTokenParser parser = new GenericTokenParser("#{", "}", new TokenHandler() {
@Override
// 將#{item.XXX}轉換為#{__frch_item_N.XXX}
public String handleToken(String content) {
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
return new StringBuilder("#{").append(newContent).append("}").toString();
}
});
delegate.appendSql(parser.parse(sql));
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
private class PrefixedContext extends DynamicContext {
private final DynamicContext delegate;
private final String prefix;
private boolean prefixApplied;
public PrefixedContext(DynamicContext delegate, String prefix) {
super(configuration, null);
this.delegate = delegate;
this.prefix = prefix;
this.prefixApplied = false;
}
public boolean isPrefixApplied() {
return prefixApplied;
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public void appendSql(String sql) {
if (!prefixApplied && sql != null && sql.trim().length() > 0) {
delegate.appendSql(prefix);
prefixApplied = true;
}
delegate.appendSql(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
}
IfSqlNode
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator; //表達式執行器
private final String test; //條件表達式
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
ExpressionEvaluator的定義如下:
public class ExpressionEvaluator {
// 布爾表達式解析,對於返回值為數字的if表達式,0為假,非0為真
public boolean evaluateBoolean(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof Number) {
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
return value != null;
}
// 循環表達式解析,主要用於foreach標簽
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) {
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
}
if (value instanceof Iterable) {
return (Iterable<?>) value;
}
if (value.getClass().isArray()) {
// the array may be primitive, so Arrays.asList() may throw
// a ClassCastException (issue 209). Do the work manually
// Curse primitives! :) (JGB)
int size = Array.getLength(value);
List<Object> answer = new ArrayList<Object>();
for (int i = 0; i < size; i++) {
Object o = Array.get(value, i);
answer.add(o);
}
return answer;
}
if (value instanceof Map) {
return ((Map) value).entrySet();
}
throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
}
}
StaticTextSqlNode
靜態文本節點不做任何處理,直接將本文本節點的內容追加到已經解析了的SQL文本的后面。
public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}
TextSqlNode
TextSqlNode主要是用來將${}轉換為實際的參數值,並返回拼接后的SQL語句,為了防止SQL注入,可以通過標簽來創建OGNL上下文變量。
public class TextSqlNode implements SqlNode {
private final String text;
private final Pattern injectionFilter;
public TextSqlNode(String text) {
this(text, null);
}
public TextSqlNode(String text, Pattern injectionFilter) {
this.text = text;
this.injectionFilter = injectionFilter;
}
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
// 將${}中的值替換為查詢參數中實際的值並返回,在StaticTextSqlNode中,#{}返回的是?
@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
}
private static class DynamicCheckerTokenParser implements TokenHandler {
private boolean isDynamic;
public DynamicCheckerTokenParser() {
// Prevent Synthetic Access
}
public boolean isDynamic() {
return isDynamic;
}
@Override
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
}
}
VarDeclSqlNode
public class VarDeclSqlNode implements SqlNode {
private final String name;
private final String expression;
public VarDeclSqlNode(String var, String exp) {
name = var;
expression = exp;
}
@Override
public boolean apply(DynamicContext context) {
final Object value = OgnlCache.getValue(expression, context.getBindings());
// 直接將ognl表達式加到當前映射語句的上下文中,這樣就可以直接獲取到了
context.bind(name, value);
return true;
}
}
DynamicContext.bind方法的實現如下:
private final ContextMap bindings;
public void bind(String name, Object value) {
bindings.put(name, value);
}
TrimSqlNode
public class TrimSqlNode implements SqlNode {
private final SqlNode contents;
private final String prefix;
private final String suffix;
private final List<String> prefixesToOverride; // 要trim多個文本的話,|分隔即可
private final List<String> suffixesToOverride; // 要trim多個文本的話,|分隔即可
private final Configuration configuration;
public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}
protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}
@Override
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
// trim節點只有在至少有一個子節點不為空的時候才有意義
boolean result = contents.apply(filteredDynamicContext);
// 所有子節點處理完成之后,filteredDynamicContext.delegate里面就包含解析后的靜態SQL文本了,此時就可以處理前后的trim了
filteredDynamicContext.applyAll();
return result;
}
private static List<String> parseOverrides(String overrides) {
if (overrides != null) {
final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
final List<String> list = new ArrayList<String>(parser.countTokens());
while (parser.hasMoreTokens()) {
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
}
private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate;
private boolean prefixApplied;
private boolean suffixApplied;
private StringBuilder sqlBuffer;
public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
}
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
@Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
// 處理前綴
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
// 處理后綴
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
if (suffixesToOverride != null) {
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
}
}
SetSqlNode
SetSqlNode直接委托給TrimSqlNode處理。參見TrimSqlNode。
WhereSqlNode
WhereSqlNode直接委托給TrimSqlNode處理。參見TrimSqlNode。
3.3 BaseBuilder
從整個設計角度來說,BaseBuilder的目的是為了統一解析的使用,但在實現上卻出入較大。首先,BaseBuilder是所有解析類的MapperBuilderAssistant、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder等的父類。如下所示:
BaseBuilder中提供類型處理器、JDBC類型、結果集類型、別名等的解析,因為在mybatis配置文件、mapper文件解析、SQL映射語句解析、基於注解的mapper文件解析過程中,都會頻繁的遇到類型處理相關的解析。但是BaseBuilder也沒有定義需要子類實現的負責解析的抽象接口,雖然XMLMapperBuilder、XMLConfigBuilder的解析入口是parse方法,XMLStatementBuilder的入口是parseStatementNode,不僅如此,MapperBuilderAssistant繼承了BaseBuilder,而不是MapperAnnotationBuilder,實際上MapperAnnotationBuilder才是解析Mapper接口的主控類。
所以從實現上來說,BaseBuilder如果要作為具體Builder類的抽象父類,那就應該定義一個需要子類實現的parse接口,要么就用組合代替繼承。
3.4 AdditionalParameter
額外參數主要是維護一些在加載時無法確定的參數,比如標簽中的參數在加載時就無法盡最大努力確定,必須通過運行時執行org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql()中的SqlNode.apply()才能確定真正要執行的SQL語句,以及額外參數。比如,對於下列的foreach語句,它的AdditionalParameter內容為:
{frch_index_0=0, item=2, frch_index_1=1, _parameter=org.mybatis.internal.example.pojo.UserReq@5ccddd20, index=1, frch_item_1=2, _databaseId=null, frch_item_0=1}
其中_parameter和_databaseId在DynamicContext構造器中硬編碼,其他值通過調用ForEachSqlNode.apply()計算得到。與此相對應,此時SQL語句在應用ForeachSqlNode之后,對參數名也進行重寫,如下所示:
select lfPartyId,author as authors,subject,comments,title,partyName from LfParty where partyName = #{partyName} AND partyName like #{partyName} and lfPartyId in ( #{__frch_item_0.prop} , #{__frch_item_1} )
然后通過SqlSourceBuilder.parse()調用ParameterMappingTokenHandler計算出該sql的ParameterMapping列表,最后構造出StaticSqlSource。
3.5 TypeHandler
當MyBatis將一個Java對象作為輸入/輸出參數執行CRUD語句操作時,它會創建一個PreparedStatement對象,並且調用setXXX()為占位符設置相應的參數值。XXX可以是Int,String,Date等Java內置類型,或者用戶自定義的類型。在實現上,MyBatis是通過使用類型處理器(type handler)來確定XXX是具體什么類型的。MyBatis對於下列類型使用內建的類型處理器:所有的基本數據類型、基本類型的包裹類型、byte[] 、java.util.Date、java.sql.Date、java,sql.Time、java.sql.Timestamp、java 枚舉類型等。對於用戶自定義的類型,我們可以創建一個自定義的類型處理器。要創建自定義類型處理器,只要實現TypeHandler接口即可,TypeHandler接口的定義如下:
public interface TypeHandler<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;
}
雖然我們可以直接實現TypeHandler接口,但是在實踐中,我們一般選擇繼承BaseTypeHandler,BaseTypeHandler為TypeHandler提供了部分骨架代碼,使得用戶使用方便,幾乎所有mybatis內置類型處理器都繼承於BaseTypeHandler。下面我們實現一個最簡單的自定義類型處理器MobileTypeHandler。
public class MobileTypeHandler extends BaseTypeHandler<Mobile> {
@Override
public Mobile getNullableResult(ResultSet rs, String columnName)
throws SQLException {
// mobile字段是VARCHAR類型,所以使用rs.getString
return new Mobile(rs.getString(columnName));
}
@Override
public Mobile getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
return new Mobile(rs.getString(columnIndex));
}
@Override
public Mobile getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return new Mobile(cs.getString(columnIndex));
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
Mobile param, JdbcType jdbcType) throws SQLException {
ps.setString(i, param.getFullNumber());
}
}
我們實現了自定義的類型處理器后,只要在mybatis配置文件mybatis-config.xml中注冊就可以使用了,如下:
<typeHandlers>
<typeHandler handler="org.mybatis.internal.example.MobileTypeHandler" />
</typeHandlers>
上述完成之后,當我們在parameterType或者resultType或者resultMap中遇到Mobile類型的屬性時,就會調用MobileTypeHandler進行代理出入參的設置和獲取。
3.6 對象包裝器工廠ObjectWrapperFactory
ObjectWrapperFactory是一個對象包裝器工廠,用於對返回的結果對象進行二次處理,它主要在org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue方法中創建對象的MetaObject時作為參數設置進去,這樣MetaObject中的objectWrapper屬性就可以被設置為我們自定義的ObjectWrapper實現而不是mybatis內置實現,如下所示:
private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
this.originalObject = object;
this.objectFactory = objectFactory;
this.objectWrapperFactory = objectWrapperFactory;
this.reflectorFactory = reflectorFactory;
if (object instanceof ObjectWrapper) {
this.objectWrapper = (ObjectWrapper) object;
} else if (objectWrapperFactory.hasWrapperFor(object)) { // 如果有自定義的ObjectWrapperFactory,就不會總是返回false了,這樣對於特定類就啟用了的我們自定義的ObjectWrapper
this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
} else if (object instanceof Map) {
this.objectWrapper = new MapWrapper(this, (Map) object);
} else if (object instanceof Collection) {
this.objectWrapper = new CollectionWrapper(this, (Collection) object);
} else {
this.objectWrapper = new BeanWrapper(this, object);
}
}
典型的下划線轉駝峰,我們就可以使用ObjectWrapperFactory來統一處理(當然,在實際中,我們一般不會這么做,而是通過設置mapUnderscoreToCamelCase來實現)。ObjectWrapperFactory 接口如下:
public interface ObjectWrapperFactory {
boolean hasWrapperFor(Object object);
ObjectWrapper getWrapperFor(MetaObject metaObject, Object object);
}
通過實現這個接口,可以判斷當object是特定類型時,返回true,然后在下面的getWrapperFor中返回一個可以處理key為駝峰的ObjectWrapper 實現類即可。ObjectWrapper類可以說是對象反射信息的facade模式,它的定義如下:
public interface ObjectWrapper {
Object get(PropertyTokenizer prop);
void set(PropertyTokenizer prop, Object value);
String findProperty(String name, boolean useCamelCaseMapping);
String[] getGetterNames();
String[] getSetterNames();
Class<?> getSetterType(String name);
Class<?> getGetterType(String name);
boolean hasSetter(String name);
boolean hasGetter(String name);
MetaObject instantiatePropertyValue(String name, PropertyTokenizer prop, ObjectFactory objectFactory);
boolean isCollection();
void add(Object element);
<E> void addAll(List<E> element);
}
當然,我們不需要從頭實現ObjectWrapper接口,可以選擇繼承BeanWrapper或者MapWrapper。比如對於Map類型,我們可以繼承MapWrapper,讓參數useCamelCaseMapping起作用。MapWrapper默認的findProperty方法並沒有做駝峰轉換處理,如下::
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
return name;
}
我們可以改成:
public class CamelMapWrapper extends MapWrapper {
public CamelMapWrapper(MetaObject metaObject, Map<String, Object> map) {
super(metaObject, map);
}
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
if (useCamelCaseMapping
&& ((name.charAt(0) >= 'A' && name.charAt(0) <= 'Z')
|| name.indexOf("_") >= 0)) {
return underlineToCamelhump(name);
}
return name;
}
/**
* 將下划線風格替換為駝峰風格
*/
public String underlineToCamelhump(String inputString) {
StringBuilder sb = new StringBuilder();
boolean nextUpperCase = false;
for (int i = 0; i < inputString.length(); i++) {
char c = inputString.charAt(i);
if (c == '_') {
if (sb.length() > 0) {
nextUpperCase = true;
}
} else {
if (nextUpperCase) {
sb.append(Character.toUpperCase(c));
nextUpperCase = false;
} else {
sb.append(Character.toLowerCase(c));
}
}
}
return sb.toString();
}
}
同時,創建一個自定義的objectWrapperFactory如下:
public class CustomWrapperFactory implements ObjectWrapperFactory {
@Override
public boolean hasWrapperFor(Object object) {
return object != null && object instanceof Map;
}
@Override
public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
return new CamelMapWrapper(metaObject, (Map) object);
}
}
然后,在 MyBatis 配置文件中配置上objectWrapperFactory:
<objectWrapperFactory type="org.mybatis.internal.example.CustomWrapperFactory"/>
同樣,useCamelCaseMapping最終是通過mapUnderscoreToCamelCase設置注入進來的,所以settings要加上這個設置:
<setting name="mapUnderscoreToCamelCase" value="true"/>
此時,如果resultType是map類型的話,就可以看到key已經是駝峰式而不是columnName了。
注意:mybatis提供了一個什么都不做的默認實現DefaultObjectWrapperFactory。
3.7 MetaObject
MetaObject是一個對象包裝器,其性質上有點類似ASF提供的commons類庫,其中包裝了對象的元數據信息,對象本身,對象反射工廠,對象包裝器工廠等。使得根據OGNL表達式設置或者獲取對象的屬性更為便利,也可以更加方便的判斷對象中是否包含指定屬性、指定屬性是否具有getter、setter等。主要的功能是通過其ObjectWrapper類型的屬性完成的,它包裝了操作對象元數據以及對象本身的主要接口,操作標准對象的實現是BeanWrapper。BeanWrapper類型有個MetaClass類型的屬性,MetaClass中有個Reflector屬性,其中包含了可讀、可寫的屬性、方法以及構造器信息。
3.8 對象工廠ObjectFactory
MyBatis 每次創建結果對象的新實例時,都會使用一個對象工廠(ObjectFactory)實例來完成。 默認的對象工廠DefaultObjectFactory僅僅是實例化目標類,要么通過默認構造方法,要么在參數映射存在的時候通過參數構造方法來實例化。如果想覆蓋對象工廠的默認行為比如給某些屬性設置默認值(有些時候直接修改對象不可行,或者由於不是自己擁有的代碼或者改動太大),則可以通過創建自己的對象工廠來實現。ObjectFactory接口定義如下:
public interface ObjectFactory {
/**
* Sets configuration properties.
* @param properties configuration properties
*/
void setProperties(Properties properties);
/**
* Creates a new object with default constructor.
* @param type Object type
* @return
*/
<T> T create(Class<T> type);
/**
* Creates a new object with the specified constructor and params.
* @param type Object type
* @param constructorArgTypes Constructor argument types
* @param constructorArgs Constructor argument values
* @return
*/
<T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);
/**
* Returns true if this object can have a set of other objects.
* It's main purpose is to support non-java.util.Collection objects like Scala collections.
*
* @param type Object type
* @return whether it is a collection or not
* @since 3.1.0
*/
<T> boolean isCollection(Class<T> type);
}
從這個接口定義可以看出,它包含了兩種通過反射機制構造實體類對象的方法,一種是通過無參構造函數,一種是通過帶參數的構造函數。同時,為了使工廠類能設置其他屬性,還提供了setProperties()方法。
要自定義對象工廠類,我們可以實現ObjectFactory這個接口,但是這樣我們就需要自己去實現一些在DefaultObjectFactory已經實現好了的東西,所以也可以繼承這個DefaultObjectFactory類,這樣可以使得實現起來更為簡單。例如,我們希望給Order對象的屬性hostname設置為本地機器名,可以像下面這么實現:
public class CustomObjectFactory extends DefaultObjectFactory{
private static String hostname;
static {
InetAddress addr = InetAddress.getLocalHost();
String ip=addr.getHostAddress().toString(); //獲取本機ip
hostName=addr.getHostName().toString(); //獲取本機計算機名稱
}
private static final long serialVersionUID = 1128715667301891724L;
@Override
public <T> T create(Class<T> type) {
T result = super.create(type);
if(type.equals(Order.class)){
((Order)result).setIp(hostname);
}
return result;
}
}
接下來,在配置文件中配置對象工廠類為我們創建的對象工廠類CustomObjectFactory。
<objectFactory type="org.mybatis.internal.example.CustomObjectFactory"></objectFactory>
此時執行代碼,就會發現返回的Order對象中ip字段的值為本機名。
3.9 MappedStatement
mapper文件或者mapper接口中每個映射語句都對應一個MappedStatement實例,它包含了所有運行時需要的信息比如結果映射、參數映射、是否需要刷新緩存等。MappedStatement定義如下:
public final class MappedStatement {
private String resource;
private Configuration configuration;
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType;
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
…
public MappedStatement build() {
assert mappedStatement.configuration != null;
assert mappedStatement.id != null;
assert mappedStatement.sqlSource != null;
assert mappedStatement.lang != null;
mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
return mappedStatement;
}
}
…
}
唯一值得注意的是resultMaps被設計為只讀,這樣應用可以查看但是不能修改。
3.10 ParameterMapping
每個參數映射<>標簽都被創建為一個ParameterMapping實例,其中包含和結果映射類似的信息,如下:
public class ParameterMapping {
private Configuration configuration;
private String property;
private ParameterMode mode;
private Class<?> javaType = Object.class;
private JdbcType jdbcType;
private Integer numericScale;
private TypeHandler<?> typeHandler;
private String resultMapId;
private String jdbcTypeName;
private String expression;
private ParameterMapping() {
}
…
}
3.11 KeyGenerator
package org.apache.ibatis.executor.keygen;
public interface KeyGenerator {
// before key generator 主要用於oracle等使用序列機制的ID生成方式
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
// after key generator 主要用於mysql等使用自增機制的ID生成方式
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
3.12 各種Registry
mybatis將類型處理器,類型別名,mapper定義,語言驅動器等各種信息包裝在Registry中維護,如下所示:
public class Configuration {
...
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
protected final InterceptorChain interceptorChain = new InterceptorChain();
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
...
}
各Registry中提供了相關的方法,比如TypeHandlerRegistry中包含了判斷某個java類型是否有類型處理器以及獲取類型處理器的方法,如下:
public boolean hasTypeHandler(TypeReference<?> javaTypeReference, JdbcType jdbcType) {
return javaTypeReference != null && getTypeHandler(javaTypeReference, jdbcType) != null;
}
public <T> TypeHandler<T> getTypeHandler(Class<T> type) {
return getTypeHandler((Type) type, null);
}
3.13 LanguageDriver
從3.2版本開始,mybatis提供了LanguageDriver接口,我們可以使用該接口自定義SQL的解析方式。先來看下LanguageDriver接口中的3個方法:
public interface LanguageDriver {
/**
* Creates a {@link ParameterHandler} that passes the actual parameters to the the JDBC statement.
* 創建一個ParameterHandler對象,用於將實際參數賦值到JDBC語句中
*
* @param mappedStatement The mapped statement that is being executed
* @param parameterObject The input parameter object (can be null)
* @param boundSql The resulting SQL once the dynamic language has been executed.
* @return
* @author Frank D. Martinez [mnesarco]
* @see DefaultParameterHandler
*/
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
/**
* Creates an {@link SqlSource} that will hold the statement read from a mapper xml file.
* It is called during startup, when the mapped statement is read from a class or an xml file.
* 將XML中讀入的語句解析並返回一個sqlSource對象
*
* @param configuration The MyBatis configuration
* @param script XNode parsed from a XML file
* @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
* @return
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
/**
* Creates an {@link SqlSource} that will hold the statement read from an annotation.
* It is called during startup, when the mapped statement is read from a class or an xml file.
* 將注解中讀入的語句解析並返回一個sqlSource對象
*
* @param configuration The MyBatis configuration
* @param script The content of the annotation
* @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
* @return
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
實現了LanguageDriver之后,可以在配置文件中指定該實現類作為SQL的解析器,在XML中我們可以使用 lang 屬性來進行指定,如下:
<typeAliases>
<typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<select id="selectBlog" lang="myLanguage">
SELECT * FROM BLOG
</select>
除了可以在語句級別指定外,也可以全局設置,如下:
<settings>
<setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>
對於mapper接口,也可以使用@Lang注解,如下所示:
public interface Mapper {
@Lang(MyLanguageDriver.class)
@Select("SELECT * FROM users")
List<User> selectUser();
}
LanguageDriver的默認實現類為XMLLanguageDriver。Mybatis默認是XML語言,所以我們來看看XMLLanguageDriver的實現:
public class XMLLanguageDriver implements LanguageDriver {
// 創建參數處理器,返回默認的實現
@Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
}
// 根據XML定義創建SqlSource
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
// 解析注解中的SQL語句
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
}
如上所示,LanguageDriver將實際的實現根據采用的底層不同,委托給了具體的Builder,對於XML配置,委托給XMLScriptBuilder。對於使用Velocity模板的解析器,委托給SQLScriptSource解析具體的SQL。
注:mybatis-velocity還提供了VelocityLanguageDriver和FreeMarkerLanguageDriver,可參見:
3.14 ResultMap
ResultMap類維護了每個標簽中的詳細信息,比如id映射、構造器映射、屬性映射以及完整的映射列表、是否有嵌套的resultMap、是否有鑒別器、是否有嵌套查詢,如下所示:
public class ResultMap {
private Configuration configuration;
private String id;
private Class<?> type;
private List<ResultMapping> resultMappings;
private List<ResultMapping> idResultMappings;
private List<ResultMapping> constructorResultMappings;
private List<ResultMapping> propertyResultMappings;
private Set<String> mappedColumns;
private Set<String> mappedProperties;
private Discriminator discriminator;
private boolean hasNestedResultMaps;
private boolean hasNestedQueries;
private Boolean autoMapping;
...
}
ResultMap除了作為一個ResultMap的數據結構表示外,本身並沒有提供額外的功能。
3.15 ResultMapping
ResultMapping代表下的映射,如下:
public class ResultMapping {
private Configuration configuration;
private String property;
private String column;
private Class<?> javaType;
private JdbcType jdbcType;
private TypeHandler<?> typeHandler;
private String nestedResultMapId;
private String nestedQueryId;
private Set<String> notNullColumns;
private String columnPrefix;
// 標記是否構造器屬性,是否ID屬性
private List<ResultFlag> flags;
private List<ResultMapping> composites;
private String resultSet;
private String foreignColumn;
private boolean lazy;
...
}
3.16 Discriminator
每個鑒別器節點都表示為一個Discriminator,如下所示:
public class Discriminator {
// 所屬的屬性節點<result>
private ResultMapping resultMapping;
// 內部的if then映射
private Map<String, String> discriminatorMap;
...
}
3.17 Configuration
Configuration是mybatis所有配置以及mapper文件的元數據容器。無論是解析mapper文件還是運行時執行SQL語句,都需要依賴與mybatis的環境和配置信息,比如databaseId、類型別名等。mybatis實現將所有這些信息封裝到Configuration中並提供了一系列便利的接口方便各主要的調用方使用,這樣就避免了各種配置和元數據信息到處散落的凌亂。
3.18 ErrorContext
ErrorContext定義了一個mybatis內部統一的日志規范,記錄了錯誤信息、發生錯誤涉及的資源文件、對象、邏輯過程、SQL語句以及出錯原因,但是它不會影響運行,如下所示:
public class ErrorContext {
private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
...
public ErrorContext reset() {
resource = null;
activity = null;
object = null;
message = null;
sql = null;
cause = null;
LOCAL.remove();
return this;
}
@Override
public String toString() {
StringBuilder description = new StringBuilder();
// message
if (this.message != null) {
description.append(LINE_SEPARATOR);
description.append("### ");
description.append(this.message);
}
// resource
if (resource != null) {
description.append(LINE_SEPARATOR);
description.append("### The error may exist in ");
description.append(resource);
}
// object
if (object != null) {
description.append(LINE_SEPARATOR);
description.append("### The error may involve ");
description.append(object);
}
// activity
if (activity != null) {
description.append(LINE_SEPARATOR);
description.append("### The error occurred while ");
description.append(activity);
}
// activity
if (sql != null) {
description.append(LINE_SEPARATOR);
description.append("### SQL: ");
description.append(sql.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ').trim());
}
// cause
if (cause != null) {
description.append(LINE_SEPARATOR);
description.append("### Cause: ");
description.append(cause.toString());
}
return description.toString();
}
}
3.19 BoundSql
/**
* An actual SQL String got from an {@link SqlSource} after having processed any dynamic content.
* The SQL may have SQL placeholders "?" and an list (ordered) of an parameter mappings
* with the additional information for each parameter (at least the property name of the input object to read
* the value from).
* </br>
* Can also have additional parameters that are created by the dynamic language (for loops, bind...).
*
* SqlSource中包含的SQL處理動態內容之后的實際SQL語句,SQL中會包含?占位符,也就是最終給JDBC的SQL語句,以及他們的參數信息
* @author Clinton Begin
*/
public class BoundSql {
// sql文本
private final String sql;
// 靜態參數說明
private final List<ParameterMapping> parameterMappings;
// 運行時參數對象
private final Object parameterObject;
// 額外參數,也就是for loops、bind生成的
private final Map<String, Object> additionalParameters;
// 額外參數的facade模式包裝
private final MetaObject metaParameters;
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.parameterObject = parameterObject;
this.additionalParameters = new HashMap<String, Object>();
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public String getSql() {
return sql;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public Object getParameterObject() {
return parameterObject;
}
public boolean hasAdditionalParameter(String name) {
String paramName = new PropertyTokenizer(name).getName();
return additionalParameters.containsKey(paramName);
}
public void setAdditionalParameter(String name, Object value) {
metaParameters.setValue(name, value);
}
public Object getAdditionalParameter(String name) {
return metaParameters.getValue(name);
}
}
4 SQL語句的執行流程
4.1 傳統JDBC用法
在原生jdbc中,我們要執行一個sql語句,它的流程是這樣的:
- 注冊驅動;
- 獲取jdbc連接;
- 創建參數化預編譯SQL;
- 綁定參數;
- 發送SQL給數據庫進行執行;
- 對於查詢,獲取結果集到應用;
我們先回顧下典型JDBC的用法:
package org.mybatis.internal.example;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
public class JdbcHelloWord {
/**
* 入口函數
* @param arg
*/
public static void main(String arg[]) {
try {
Connection con = null; //定義一個MYSQL鏈接對象
Class.forName("com.mysql.jdbc.Driver").newInstance(); //MYSQL驅動
con = DriverManager.getConnection("jdbc:mysql://10.7.12.4:3306/lfBase?useUnicode=true", "lfBase", "eKffQV6wbh3sfQuFIG6M"); //鏈接本地MYSQL
//更新一條數據
String updateSql = "UPDATE LfParty SET remark1 = 'mybatis internal example' WHERE lfPartyId = ?";
PreparedStatement pstmt = con.prepareStatement(updateSql);
pstmt.setString(1, "1");
long updateRes = pstmt.executeUpdate();
System.out.print("UPDATE:" + updateRes);
//查詢數據並輸出
String sql = "select lfPartyId,partyName from LfParty where lfPartyId = ?";
PreparedStatement pstmt2 = con.prepareStatement(sql);
pstmt2.setString(1, "1");
ResultSet rs = pstmt2.executeQuery();
while (rs.next()) { //循環輸出結果集
String lfPartyId = rs.getString("lfPartyId");
String partyName = rs.getString("partyName");
System.out.print("\r\n\r\n");
System.out.print("lfPartyId:" + lfPartyId + "partyName:" + partyName);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.2 mybatis執行SQL語句
同樣的,在mybatis中,要執行sql語句,首先要拿到代表JDBC底層連接的一個對象,這在mybatis中的實現就是SqlSession。mybatis提供了下列實現:
獲取SqlSession的API如下:
SqlSession session = SqlSessionFactory.openSession();
try {
User user = (User) session.selectOne("org.mybatis.internal.example.mapper.UserMapper.getUser", 1);
System.out.println("sql from xml:" + user.getLfPartyId() + "," + user.getPartyName());
UserMapper2 mapper = session.getMapper(UserMapper2.class);
List<User> users = mapper.getUser2(293);
System.out.println("sql from mapper:" + users.get(0).getLfPartyId() + "," + users.get(0).getPartyName());
} finally {
session.close();
}
同樣,首先調用SqlSessionFactory.openSession()拿到一個session,然后在session上執行各種CRUD操作。簡單來說,SqlSession就是jdbc連接的代表,openSession()就是獲取jdbc連接(當然其背后可能是從jdbc連接池獲取);session中的各種selectXXX方法或者調用mapper的具體方法就是集合了JDBC調用的第3、4、5、6步。SqlSession接口的定義如下:
可知,絕大部分的方法都是泛型方法,也可以說采用了模板方法實現。
4.2.1 獲取openSession
獲取openSession的總體流程為:
我們先來看openSession的具體實現。mybatis提供了兩個SqlSessionFactory實現:SqlSessionManager和DefaultSqlSessionFactory,默認返回的是DefaultSqlSessionFactory,它們的區別我們后面會講到。我們先來看下SqlSessionFactory的接口定義:
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
主要有多種形式的重載,除了使用默認設置外,可以指定自動提交模式、特定的jdbc連接、事務隔離級別,以及指定的執行器類型。關於執行器類型,mybatis提供了三種執行器類型:SIMPLE, REUSE, BATCH。后面我們會詳細分析每種類型的執行器的差別以及各自的適用場景。我們以最簡單的無參方法切入(按照一般的套路,如果定義了多個重載的方法或者構造器,內部實現一定是設置作者認為最合適的默認值,然后調用次多參數的方法,直到最后),它的實現是這樣的:
public class DefaultSqlSessionFactory implements SqlSessionFactory {
...
@Override
public SqlSession openSession() {
// 使用默認的執行器類型(默認是SIMPLE),默認隔離級別,非自動提交 委托給openSessionFromDataSource方法
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
...
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
// 獲取事務管理器, 支持從數據源或者直接獲取
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 從數據源創建一個事務, 同樣,數據源必須配置, mybatis內置了JNDI、POOLED、UNPOOLED三種類型的數據源,其中POOLED對應的實現為org.apache.ibatis.datasource.pooled.PooledDataSource,它是mybatis自帶實現的一個同步、線程安全的數據庫連接池 一般在生產中,我們會使用dbcp或者druid連接池
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}
...
private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
// 如果沒有配置environment或者environment的事務管理器為空,則使用受管的事務管理器
// 除非什么都沒有配置,否則在mybatis-config里面,至少要配置一個environment,此時事務工廠不允許為空
// 對於jdbc類型的事務管理器,則返回JdbcTransactionFactory,其內部操作mybatis的JdbcTransaction實現(采用了Facade模式),后者對jdbc連接操作
if (environment == null || environment.getTransactionFactory() == null) {
return new ManagedTransactionFactory();
}
return environment.getTransactionFactory();
}
}
我們來看下transactionFactory.newTransaction的實現,還是以jdbc事務為例子。
public class JdbcTransactionFactory implements TransactionFactory {
...
@Override
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
}
newTransaction的實現邏輯很簡單,但是此時返回的事務不一定是有底層連接的。
拿到事務后,根據事務和執行器類型創建一個真正的執行器實例。獲取執行器的邏輯如下:
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;
}
如果沒有配置執行器類型,默認是簡單執行器。如果啟用了緩存,則使用緩存執行器。
拿到執行器之后,new一個DefaultSqlSession並返回,這樣一個SqlSession就創建了,它從邏輯上代表一個封裝了事務特性的連接,如果在此期間發生異常,則調用關閉事務(因為此時事務底層的連接可能已經持有了,否則會導致連接泄露)。
DefaultSqlSession的構造很簡單,就是簡單的屬性賦值:
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
private boolean autoCommit;
// 含義是TODO
private boolean dirty;
private List<Cursor<?>> cursorList;
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
...
}
具體的對外API我們后面專門講解。
根據sql語句使用xml進行維護或者在注解上配置,sql語句執行的入口分為兩種:
第一種,調用org.apache.ibatis.session.SqlSession的crud方法比如selectList/selectOne傳遞完整的語句id直接執行;
第二種,先調用SqlSession的getMapper()方法得到mapper接口的一個實現,然后調用具體的方法。除非早期,現在實際開發中,我們一般采用這種方式。
4.2.2 sql語句執行方式一
我們先來看第一種形式的sql語句執行也就是SqlSession.getMapper除外的形式。這里還是以帶參數的session.selectOne為例子。其實現代碼為:
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
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;
}
}
...
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
selectOne調用在內部將具體實現委托給selectList,如果返回的行數大於1就拋異常。我們先看下selectList的總體流程:
selectList的第三個參數是RowBounds.DEFAULT,我們來看下什么是RowBounds。
public class RowBounds {
public static final int NO_ROW_OFFSET = 0;
public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
public static final RowBounds DEFAULT = new RowBounds();
private int offset;
private int limit;
public RowBounds() {
this.offset = NO_ROW_OFFSET;
this.limit = NO_ROW_LIMIT;
}
}
從定義可知,RowBounds是一個分頁查詢的參數封裝,默認是不分頁。
第三個selectList重載首先使用應用調用方傳遞的語句id判斷configuration.mappedStatements里面是否有這個語句,如果沒有將會拋出IllegalArgumentException異常,執行結束。否則將獲取到的映射語句對象連同其他參數一起將具體實現委托給執行器Executor的query方法。
在這里對查詢參數parameter進行了一次封裝,封裝邏輯wrapCollection主要是判斷參數是否為數組或集合類型類型,是的話將他們包裝到StrictMap中。同時設置結果處理器為null。
我們現在來看下Executor的query方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 首先根據傳遞的參數獲取BoundSql對象,對於不同類型的SqlSource,對應的getBoundSql實現不同,具體參見SqlSource詳解一節 TODO
BoundSql boundSql = ms.getBoundSql(parameter);
// 創建緩存key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 委托給重載的query
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 如果需要刷新緩存(默認DML需要刷新,也可以語句層面修改), 且queryStack(應該是用於嵌套查詢的場景)=0
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 如果查詢不需要應用結果處理器,則先從緩存獲取,這樣可以避免數據庫查詢。我們后面會分析到localCache是什么時候被設置進去的
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--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
...
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 根據映射語句id,分頁信息,jdbc規范化的預編譯sql,所有映射參數的值以及環境id的值,計算出緩存Key
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
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);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
// 只處理存儲過程和函數調用的出參, 因為存儲過程和函數的返回不是通過ResultMap而是ParameterMap來的,所以只要把緩存的非IN模式參數取出來設置到parameter對應的屬性上即可
if (ms.getStatementType() == StatementType.CALLABLE) {
final Object cachedParameter = localOutputParameterCache.getObject(key);
if (cachedParameter != null && parameter != null) {
final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
final MetaObject metaParameter = configuration.newMetaObject(parameter);
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
final String parameterName = parameterMapping.getProperty();
final Object cachedValue = metaCachedParameter.getValue(parameterName);
metaParameter.setValue(parameterName, cachedValue);
}
}
}
}
}
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 {
// doQuery是個抽象方法,每個具體的執行器都要自己去實現,我們先看SIMPLE的
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 把真正的查詢結果放到緩存中去
localCache.putObject(key, list);
// 如果是存儲過程類型,則把查詢參數放到本地出參緩存中, 所以第一次一定為空
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
我們先來看下緩存key的定義:
package org.apache.ibatis.cache;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.apache.ibatis.reflection.ArrayUtil;
/**
* @author Clinton Begin
*/
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public int getUpdateCount() {
return updateList.size();
}
public void update(Object object) {
// ArrayUtil提供了可以計算包括數組的對象的hashCode, toString, equals方法
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
...
@Override
public CacheKey clone() throws CloneNotSupportedException {
CacheKey clonedCacheKey = (CacheKey) super.clone();
clonedCacheKey.updateList = new ArrayList<Object>(updateList);
return clonedCacheKey;
}
}
我們來看下SIMPLE執行器的doQuery定義:
@Override
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();
// 根據上下文參數和具體的執行器new一個StatementHandler, 其中包含了所有必要的信息,比如結果處理器、參數處理器、執行器等等,主要有三種類型的語句處理器UNPREPARE、PREPARE、CALLABLE。默認是PREPARE類型,通過mapper語句上的statementType屬性進行設置,一般除了存儲過程外不應該設置
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 這一步是真正和JDBC打交道
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 獲取JDBC連接
Connection connection = getConnection(statementLog);
// 調用語句處理器的prepare方法
stmt = handler.prepare(connection, transaction.getTimeout());
// 設置參數
handler.parameterize(stmt);
return stmt;
}
下面我們來看下PREPARE處理處理的prepare實現,從上述可知,具體的處理器繼承了BaseStatementHandler,
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
// 首先實例化語句,因為PREPARE和非PREPARE不同,所以留給具體子類實現
statement = instantiateStatement(connection);
// 設置語句超時時間
setStatementTimeout(statement, transactionTimeout);
// 設置fetch大小
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
重點來看PREPARE語句處理器的初始化語句過程:
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
// 只處理Jdbc3KeyGenerator,因為它代表的是自增,另外一個是SelectKeyGenerator用於不支持自增的情況
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 {
return connection.prepareStatement(sql);
}
}
基本上就是把我們在MAPPER中定義的屬性轉換為JDBC標准的調用。
接下去再來看參數是如何MAPPER中定義的參數是如何轉換為JDBC參數的,PreparedStatementHandler.parameterize將具體實現委托給了ParameterHandler.setParameters()方法,ParameterHandler目前只有一種實現DefaultParameterHandler。
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
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;
String propertyName = parameterMapping.getProperty();
// 計算參數值的優先級是 先判斷是不是屬於語句的AdditionalParameter;其次參數是不是null;然后判斷是不是屬於注冊類型;都不是,那估計參數一定是object或者map了,這就要借助於MetaObject獲取屬性值了;
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);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// jdbc下標從1開始,由具體的類型處理器進行參數的設置, 對於每個jdbcType, mybatis都提供了一個對應的Handler,具體可參考上文TypeHandler詳解, 其內部調用的是PrepareStatement.setXXX進行設置。
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
我們在mapper中定義的所有ParameterType、ParameterMap、內嵌參數映射等在最后都在這里被作為ParameterMapping轉換為JDBC參數。
Configuration中newStatementHandler的定義如下:
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;
}
回到主體邏輯SimpleExecutor.doQuery,創建了Statement具體實現的實例后,調用SimpleExecutor.query進行具體的查詢,查詢的主體邏輯如下:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
從上述具體邏輯的實現可以看出,內部調用PreparedStatement完成具體查詢后,將ps的結果集傳遞給對應的結果處理器進行處理。查詢結果的映射是mybatis作為ORM框架提供的最有價值的功能,同時也可以說是最復雜的邏輯之一。下面我們來專門分析mybatis查詢結果集的處理。
mybatis結果集處理
對於結果集處理,mybatis默認提供了DefaultResultSetHandler,如下所示:
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
// 返回jdbc ResultSet的包裝形式,主要是將java.sql.ResultSetMetaData做了Facade模式,便於使用
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
// 絕大部分情況下一個查詢只有一個ResultMap, 除非多結果集查詢
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
// 至少執行一次
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 根據resultMap的定義將resultset打包到應用端的multipleResults中
handleResultSet(rsw, resultMap, multipleResults, null);
// 循環直到處理完所有的結果集,一般情況下,一個execute只會返回一個結果集,除非語句比如存儲過程返回多個resultSet
rsw = getNextResultSet(stmt);
// 清空嵌套結果集
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
// 處理關聯或集合
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
// 如果一個映射語句的resultSet數量比jdbc resultset多的話,多的部分就是嵌套結果集
while (rsw != null && resultSetCount < resultSets.length) {
// nextResultMaps初始化的時候為空,它是在處理主查詢每行記錄的時候寫進去的,所以此時就可以得到主記錄是哪個屬性
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
// 得到子結果集的resultMap之后,就可以進行填充了
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
// 因為調用handleResultSet的只有handleResultSets,按說其中第一個調用永遠不會出現parentMapping==null的情況,只有第二個調用才會出現這種情況,而且應該是連續的,因為第二個調用就是為了處理嵌套resultMap。所以在handleRowValues處理resultMap的時候,一定是主的先處理,嵌套的后處理,這樣整個邏輯就比較清晰了。這里需要補個流程圖。
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
// 處理嵌套/關聯的resultMap(collection或association)
if (parentMapping != null) {
// 處理非主記錄 resultHandler傳遞null,RowBounds傳遞默認值,parentMapping不為空
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
// 處理主記錄,這里有個疑問???問什么resultHandler不為空,就不需要添加到multipleREsults中
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
// 處理主記錄,resultHander不為空,rowBounds不使用默認值,parentMapping傳遞null
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
// 處理每一行記錄,不管是主查詢記錄還是關聯嵌套的查詢結果集,他們的入參上通過resultHandler,rowBounds,parentMapping區分,具體見上述調用
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
// 具體實現上,處理嵌套記錄和主記錄的邏輯不一樣
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
// 含有嵌套resultMap的列處理,通常是collection或者association
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
// 處理主記錄
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
// 設置從指定行開始,這是mybatis進行內部分頁,不是依賴服務器端的分頁實現
skipRows(rsw.getResultSet(), rowBounds);
// 確保沒有超過mybatis邏輯分頁限制,同時結果集中還有記錄沒有fetch
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
// 解析鑒別器,得到嵌套的最深的鑒別器對應的ResultMap,如果沒有鑒別器,就返回最頂層的ResultMap
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
// 這個時候resultMap是非常干凈的,沒有嵌套任何其他東西了,但是這也是最關鍵的地方,將ResultSet記錄轉換為業務層配置的對象類型或者Map類型
Object rowValue = getRowValue(rsw, discriminatedResultMap);
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
if (parentMapping != null) {
// 如果不是主記錄,則鏈接到主記錄
linkToParents(rs, parentMapping, rowValue);
} else {
// 否則讓ResultHander(默認是DefaultResultHander處理,直接添加到List<Object>中)
callResultHandler(resultHandler, resultContext, rowValue);
}
}
@SuppressWarnings("unchecked" /* because ResultHandler<?> is always ResultHandler<Object>*/)
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
resultContext.nextResultObject(rowValue);
((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}
// 非主記錄需要連接到主記錄,這里涉及到collection和association的resultMap實現,我們來看下
// MULTIPLE RESULT SETS
private void linkToParents(ResultSet rs, ResultMapping parentMapping, Object rowValue) throws SQLException {
CacheKey parentKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(), parentMapping.getForeignColumn());
// 判斷當前的主記錄是否有待關聯的子記錄,是通過pendingRelations Map進行維護的,pendingRelations是在addPendingChildRelation中添加主記錄的
List<PendingRelation> parents = pendingRelations.get(parentKey);
if (parents != null) {
for (PendingRelation parent : parents) {
if (parent != null && rowValue != null) {
linkObjects(parent.metaObject, parent.propertyMapping, rowValue);
}
}
}
}
// 集合與非集合的處理邏輯對外封裝在一起,這樣便於用戶使用, 內部通過判斷resultMapping中的類型確定是否為集合類型。無論是否為集合類型,最后都添加到parent的metaObject所封裝的原始Object對應的屬性上
private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
if (collectionProperty != null) {
final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
targetMetaObject.add(rowValue);
} else {
metaObject.setValue(resultMapping.getProperty(), rowValue);
}
}
private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
final String propertyName = resultMapping.getProperty();
Object propertyValue = metaObject.getValue(propertyName);
if (propertyValue == null) {
Class<?> type = resultMapping.getJavaType();
if (type == null) {
type = metaObject.getSetterType(propertyName);
}
try {
if (objectFactory.isCollection(type)) {
propertyValue = objectFactory.create(type);
metaObject.setValue(propertyName, propertyValue);
return propertyValue;
}
} catch (Exception e) {
throw new ExecutorException("Error instantiating collection property for result '" + resultMapping.getProperty() + "'. Cause: " + e, e);
}
} else if (objectFactory.isCollection(propertyValue.getClass())) {
return propertyValue;
}
return null;
}
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 有三個createResultObject重載,這三個重載完成的功能從最里面到最外面分別是:
1、使用構造器創建目標對象類型;
先判斷是否有目標對象類型的處理器,有的話,直接調用類型處理器(這里為什么一定是原生類型)創建目標對象
如果沒有,判斷是否有構造器,有的話,使用指定的構造器創建目標對象(構造器里面如果嵌套了查詢或者ResultMap,則進行處理)
如果結果類型是接口或者具有默認構造器,則使用ObjectFactory創建默認目標對象
最后判斷是否可以應用自動映射,默認是對非嵌套查詢,只要沒有明確設置AutoMappingBehavior.NONE就可以,對於嵌套查詢,AutoMappingBehavior.FULL就可以。自動映射的邏輯是先找目標對象上具有@AutomapConstructor注解的構造器,然后根據ResultSet返回的字段清單找匹配的構造器,如果找不到,就報錯
2、如果此時創建的對象不為空,且不需要應用結果對象處理器,判斷有沒有延遲加載且具有嵌套查詢的屬性,如果有的話,則為對象創建一個代理,額外存儲后面fetch的時候進行延遲加載所需的信息。返回對象。
3、如果此時創建的對象不為空,且不需要應用結果對象處理器,如果對象需要自動映射,則先進行自動映射(創建自動映射列表的過程為:先找到在ResultSet、不在ResultMap中的列,如果在目標對象上可以找到屬性且可以類型可以處理,則標記為可以自動映射;然后進行自動映射處理,如果遇到無法處理的屬性,則根據autoMappingUnknownColumnBehavior進行處理,默認忽略),其次進行屬性映射處理
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
// 處理屬性映射,這里會識別出哪些屬性需要nestQuery,哪些是nest ResultMap
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
//
// PROPERTY MAPPINGS
//
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.getNestedResultMapId() != null) {
// the user added a column attribute to a nested result map, ignore it
column = null;
}
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
// issue #541 make property optional
final String property = propertyMapping.getProperty();
if (property == null) {
continue;
} else if (value == DEFERED) {
foundValues = true;
continue;
}
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(property, value);
}
}
}
return foundValues;
}
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
if (propertyMapping.getNestedQueryId() != null) {
// 處理嵌套query類型的屬性
return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
} else if (propertyMapping.getResultSet() != null) {
// 處理resultMap類型的屬性,主要是:1、維護記錄cacheKey和父屬性/記錄對Map的關聯關系,便於在處理嵌套ResultMap時很快可以找到所有需要處理嵌套結果集的父屬性;2、維護父屬性和對應resultSet的關聯關系。這兩者都是為了在處理嵌套結果集是方便
addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
return DEFERED;
} else {
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
return typeHandler.getResult(rs, column);
}
}
對於嵌套resultmap的處理,它的實現是這樣的:
//
// HANDLE NESTED RESULT MAPS
//
private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
final DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
// mybatis 分頁處理
skipRows(rsw.getResultSet(), rowBounds);
// 前一次處理的記錄,只有在映射語句的結果集無序的情況下有意義
Object rowValue = previousRowValue;
// 一直處理直到超出分頁邊界或者結果集處理完
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
//同主記錄,先解析到鑒別器的最底層的ResultMap
final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
// 創建當前處理記錄的rowKey,規則見下文
final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
// 根據rowKey獲取嵌套結果對象map中對應的值,因為在處理主記錄時存儲進去了,具體見上面addPending的流程圖,所以partialObject一般不會為空
Object partialObject = nestedResultObjects.get(rowKey);
// issue #577 && #542
// 根據映射語句的結果集是否有序走不同的邏輯
if (mappedStatement.isResultOrdered()) {
// 對於有序結果集的映射語句,如果嵌套結果對象map中不包含本記錄,則清空嵌套結果對象,因為此時嵌套結果對象之前的記錄已經沒有意義了
if (partialObject == null && rowValue != null) {
nestedResultObjects.clear();
// 添加記錄到resultHandler的list屬性中 或 如果是非主記錄,添加到主記錄對應屬性的list或者object中
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
} else { // 正常邏輯走這里
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
if (partialObject == null) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
}
if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
previousRowValue = null;
} else if (rowValue != null) {
previousRowValue = rowValue;
}
}
//
// GET VALUE FROM ROW FOR NESTED RESULT MAP
// 為嵌套resultMap創建rowValue,和非嵌套記錄的接口分開
// getRowValue和applyNestedResultMappings存在遞歸調用,直到處理到不包含任何嵌套結果集的最后一層resultMap為止
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap/*主記錄的resultMap*/, CacheKey combinedKey, String columnPrefix, Object partialObject/*嵌套resultMap的記錄*/) throws SQLException {
final String resultMapId = resultMap.getId();
Object rowValue = partialObject;
if (rowValue != null) { // 此時rowValue不應該空
final MetaObject metaObject = configuration.newMetaObject(rowValue);
putAncestor(rowValue, resultMapId);
applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
ancestorObjects.remove(resultMapId);
} else {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
// 判斷是否至少找到了一個不為null的屬性值
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, true)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
putAncestor(rowValue, resultMapId);
foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
ancestorObjects.remove(resultMapId);
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
if (combinedKey != CacheKey.NULL_CACHE_KEY) {
nestedResultObjects.put(combinedKey, rowValue);
}
}
return rowValue;
}
private void putAncestor(Object resultObject, String resultMapId) {
ancestorObjects.put(resultMapId, resultObject);
}
//
// NESTED RESULT MAP (JOIN MAPPING)
//
private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {
boolean foundValues = false;
for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
final String nestedResultMapId = resultMapping.getNestedResultMapId();
if (nestedResultMapId != null && resultMapping.getResultSet() == null) { // 僅僅處理嵌套resultMap的屬性
try {
final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
// 得到嵌套resultMap的實際定義
final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
if (resultMapping.getColumnPrefix() == null) {
// try to fill circular reference only when columnPrefix
// is not specified for the nested result map (issue #215)
// 不管循環嵌套resultMap的情況
Object ancestorObject = ancestorObjects.get(nestedResultMapId);
if (ancestorObject != null) {
if (newObject) {
linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
}
continue;
}
}
final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
Object rowValue = nestedResultObjects.get(combinedKey);
boolean knownValue = rowValue != null; // 第一次一定是null
// 為嵌套resultMap屬性創建對象或者集合
instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
// 至少有一個字段不為null
if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
// 為嵌套resultMap創建記錄
rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
if (rowValue != null && !knownValue) { //獲取到記錄,就綁定到主記錄
linkObjects(metaObject, resultMapping, rowValue/*嵌套resultMap的記錄*/);
foundValues = true;
}
}
} catch (SQLException e) {
throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'. Cause: " + e, e);
}
}
}
return foundValues;
}
對於嵌套查詢的屬性處理,它的實現是這樣的:
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
// 獲取queryId
final String nestedQueryId = propertyMapping.getNestedQueryId();
final String property = propertyMapping.getProperty();
// 根據嵌套queryId獲取映射語句對象
final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
Object value = null;
if (nestedQueryParameterObject != null) {
final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
final Class<?> targetType = propertyMapping.getJavaType();
if (executor.isCached(nestedQuery, key)) {
executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
value = DEFERED;
} else {
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
if (propertyMapping.isLazy()) {
lazyLoader.addLoader(property, metaResultObject, resultLoader);
value = DEFERED;
} else {
value = resultLoader.loadResult();
}
}
}
return value;
}
private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
if (resultMapping.isCompositeResult()) {
return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
} else {
return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
}
}
private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
final TypeHandler<?> typeHandler;
if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
} else {
typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
}
return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
}
private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
final Object parameterObject = instantiateParameterObject(parameterType);
final MetaObject metaObject = configuration.newMetaObject(parameterObject);
boolean foundValues = false;
for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
final Class<?> propType = metaObject.getSetterType(innerResultMapping.getProperty());
final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
// issue #353 & #560 do not execute nested query if key is null
if (propValue != null) {
metaObject.setValue(innerResultMapping.getProperty(), propValue);
foundValues = true;
}
}
return foundValues ? parameterObject : null;
}
selectMap實現
看完selectList/selectOne的實現,我們來看下selectMap的實現,需要注意的是,這個selectMap並不等價於方法public List
@Override
public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
final List<? extends V> list = selectList(statement, parameter, rowBounds);
final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<K, V>(mapKey,
configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
final DefaultResultContext<V> context = new DefaultResultContext<V>();
for (V o : list) {
context.nextResultObject(o);
mapResultHandler.handleResult(context);
}
return mapResultHandler.getMappedResults();
}
從方法簽名上,我們可以看到,和selectList不同,selectMap多了一個參數mapKey,mapKey就是用來指定返回類型中作為key的那個字段名,具體的核心邏輯委托給了selectList方法,只是在返回結果后,mapResultHandler進行了二次處理。DefaultMapResultHandler是眾多ResultHandler的實現之一。DefaultMapResultHandler.handleResult()的功能就是把List轉換為Map<object.prop1,object>格式。
update/insert/delete實現
看完select的實現,我們再來看update的實現。update操作的整體流程為:
具體實現代碼如下:
@Override
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
首先設置了字段dirty=true(dirty主要用在非自動提交模式下,用於判斷是否需要提交或回滾,在強行提交模式下,如果dirty=true,則需要提交或者回滾,代表可能有pending的事務),然后調用執行器實例的update()方法,如下:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地緩存, 與本地出參緩存
clearLocalCache();
// 調用具體執行器實現的doUpdate方法
return doUpdate(ms, parameter);
}
我們以SimpleExecutor為例,看下doUpdate的實現:
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
其中的邏輯可以發現,和selectList的實現非常相似,先創建語句處理器,然后創建Statement實例,最后調用語句處理的update,語句處理器里面調用jdbc對應update的方法execute()。和selectList的不同之處在於:
- 在創建語句處理器期間,會根據需要調用KeyGenerator.processBefore生成前置id;
- 在執行完成execute()方法后,會根據需要調用KeyGenerator.processAfter生成后置id;
通過分析delete/insert,我們會發現他們內部都委托給update實現了,所以我們就不做重復的分析了。
4.2.3 SQL語句執行方式二 SqlSession.getMapper實現
通過SqlSession.getMapper執行CRUD語句的流程為:
我們現在來看下SqlSession的getMapper()是如何實現的。DefaultSqlSession將具體創建Mapper實現的任務委托給了Configuration的getMapper泛型方法,如下所示:
public class DefaultSqlSession {
...
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
}
public class Configuration {
...
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}
最后調用MapperRegistry.getMapper得到Mapper的實現代理,如下所示:
public class MapperRegistry {
...
@SuppressWarnings("unchecked")
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);
}
}
}
MapperRegistry又將創建代理的任務委托給MapperProxyFactory,MapperProxyFactory首先為Mapper接口創建了一個實現了InvocationHandler方法調用處理器接口的代理類MapperProxy,並實現invoke接口(其中為mapper各方法執行sql的具體邏輯),最后才調用JDK的
java.lang.reflect.Proxy為Mapper接口創建動態代理類並返回。如下所示:
public class MapperProxyFactory<T> {
...
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
這樣當我們應用層執行List users = mapper.getUser2(293);的時候,JVM會首先調用MapperProxy.invoke,如下:
具體實現代碼如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
MapperMethod.execute實現如下:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH: // 主要用於BatchExecutor和CacheExecutor的場景,SimpleExecutor模式不適用
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
對非查詢類SQL,首先將請求參數轉換為mybatis內部的格式,然后調用sqlSession實例對應的方法,這就和第一種方式的SQL邏輯一樣的。
對於查詢類SQL,根據返回類型是void/many/map/one/cursor分別調用不同的實現入口,但主體邏輯都類似,除少數特別處理外,都是調用sqlSession.selectXXX,這里我們就不一一講解。
4.3 動態sql
准確的說,只要mybatis的的crud語句中包含了、等標簽或者${}之后,就已經算是動態sql了,所以只要在mybatis加載mapper文件期間被解析為非StaticSqlSource,就會被當做動態sql處理,在執行selectXXX或者update/insert/delete期間,就會調用對應的SqlNode接口和TextSqlNode.isDynamic()處理各自的標簽以及${},並最終將每個sql片段處理到StaticTextSqlNode並生成最終的參數化靜態SQL語句為止。所以,可以說,在絕大部分非PK查詢的情況下,我們都是在使用動態SQL。
4.4 存儲過程與函數調用實現
如果MappedStatement.StatementType類型為CALLABLE,在Executor.doQuery方法中創建語句處理器的時候,就會返回CallableStatementHandler實例,隨后在調用語句處理器的初始化語句和設置參數 方法時,調用jdbc對應存儲過程的prepareCall方法,如下:
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getResultSetType() != null) {
return connection.prepareCall(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareCall(sql);
}
}
@Override
public void parameterize(Statement statement) throws SQLException {
registerOutputParameters((CallableStatement) statement);
parameterHandler.setParameters((CallableStatement) statement);
}
private void registerOutputParameters(CallableStatement cs) throws SQLException {
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (int i = 0, n = parameterMappings.size(); i < n; i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() == ParameterMode.OUT || parameterMapping.getMode() == ParameterMode.INOUT) {
if (null == parameterMapping.getJdbcType()) {
throw new ExecutorException("The JDBC Type must be specified for output parameter. Parameter: " + parameterMapping.getProperty());
} else {
if (parameterMapping.getNumericScale() != null && (parameterMapping.getJdbcType() == JdbcType.NUMERIC || parameterMapping.getJdbcType() == JdbcType.DECIMAL)) {
cs.registerOutParameter(i + 1, parameterMapping.getJdbcType().TYPE_CODE, parameterMapping.getNumericScale());
} else {
if (parameterMapping.getJdbcTypeName() == null) {
cs.registerOutParameter(i + 1, parameterMapping.getJdbcType().TYPE_CODE);
} else {
cs.registerOutParameter(i + 1, parameterMapping.getJdbcType().TYPE_CODE, parameterMapping.getJdbcTypeName());
}
}
}
}
}
}
4.5 mybatis事務實現
mybatis的事務管理模式分為兩種,自動提交和手工提交,DefaultSqlSessionFactory的openSession中重載中,提供了一個參數用於控制是否自動提交事務,該參數最終被傳遞給 java.sql.Connection.setAutoCommit()方法用於控制是否自動提交事務(默認情況下,連接是自動提交的),如下所示:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}
如上所示,返回的事務傳遞給了執行器,因為執行器是在事務上下文中執行,所以對於自動提交模式,實際上mybatis不需要去關心。只有非自動管理模式,mybatis才需要關心事務。對於非自動提交模式,通過sqlSession.commit()或sqlSession.rollback()發起,在進行提交或者回滾的時候會調用isCommitOrRollbackRequired判斷是否應該提交或者回滾事務,如下所示:
private boolean isCommitOrRollbackRequired(boolean force) {
return (!autoCommit && dirty) || force;
}
只有非自動提交模式且執行過DML操作或者設置強制提交才會認為應該進行事務提交或者回滾操作。
對於不同的執行器,在提交和回滾執行的邏輯不一樣,因為每個執行器在一級、二級、語句緩存上的差異:
- 對於簡單執行器,除了清空一級緩存外,什么都不做;
- 對於REUSE執行器,關閉每個緩存的Statement以釋放服務器端語句處理器,然后清空緩存的語句;
- 對於批量處理器,則執行每個批處理語句的executeBatch()方法以便真正執行語句,然后關閉Statement;
上述邏輯執行完成后,會執行提交/回滾操作。對於緩存執行器,在提交/回滾完成之后,會將TransactionCache中的entriesMissedInCache和entriesToAddOnCommit列表分別移動到語句對應的二級緩存中或清空掉。
4.6 緩存
只要實現org.apache.ibatis.cache.Cache接口的任何類都可以當做緩存,Cache接口很簡單:
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
/**
* Optional. As of 3.2.6 this method is no longer called by the core.
*
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
ReadWriteLock getReadWriteLock();
}
mybatis提供了基本實現org.apache.ibatis.cache.impl.PerpetualCache,內部采用原始HashMap實現。第二個需要知道的方面是mybatis有一級緩存和二級緩存。一級緩存是SqlSession級別的緩存,不同SqlSession之間的緩存數據區域(HashMap)是互相不影響,MyBatis默認支持一級緩存,不需要任何的配置,默認情況下(一級緩存的有效范圍可通過參數localCacheScope參數修改,取值為SESSION或者STATEMENT),在一個SqlSession的查詢期間,只要沒有發生commit/rollback或者調用close()方法,那么mybatis就會先根據當前執行語句的CacheKey到一級緩存中查找,如果找到了就直接返回,不到數據庫中執行。其實現在代碼BaseExecutor.query()中,如下所示:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
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--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
// 如果設置了一級緩存是STATEMENT級別而非默認的SESSION級別,一級緩存就去掉了
clearLocalCache();
}
}
return list;
}
二級緩存是mapper級別的緩存,多個SqlSession去操作同一個mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是跨SqlSession。二級緩存默認不啟用,需要通過在Mapper中明確設置cache,它的實現在CachingExecutor的query()方法中,如下所示:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
// 如果二級緩存中找到了記錄就直接返回,否則到DB查詢后進行緩存
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); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在mybatis的緩存實現中,緩存鍵CacheKey的格式為:cacheKey=ID + offset + limit + sql + parameterValues + environmentId。對於本書例子中的語句,其CacheKey為:
-1445574094:212285810:org.mybatis.internal.example.mapper.UserMapper.getUser:0:2147483647:select lfPartyId,partyName from LfParty where partyName = ? AND partyName like ? and lfPartyId in ( ?, ?):p2:p2:1:2:development
- 對於一級緩存,commit/rollback都會清空一級緩存。
- 對於二級緩存,DML操作或者顯示設置語句層面的flushCache屬性都會使得二級緩存失效。
在二級緩存容器的具體回收策略實現上,有下列幾種:
- LRU – 最近最少使用的:移除最長時間不被使用的對象,也是默認的選項,其實現類是org.apache.ibatis.cache.decorators.LruCache。
- FIFO – 先進先出:按對象進入緩存的順序來移除它們,其實現類是org.apache.ibatis.cache.decorators.FifoCache。
- SOFT – 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象,其實現類是org.apache.ibatis.cache.decorators.SoftCache。
- WEAK – 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象,其實現類是org.apache.ibatis.cache.decorators.WeakCache。
在緩存的設計上,Mybatis的所有Cache算法都是基於裝飾器/Composite模式對PerpetualCache擴展增加功能。
對於模塊化微服務系統來說,應該來說mybatis的一二級緩存對業務數據都不適合,尤其是對於OLTP系統來說,CRM/BI這些不算,如果要求數據非常精確的話,也不是特別合適。對這些要求數據准確的系統來說,盡可能只使用mybatis的ORM特性比較靠譜。但是有一部分數據如果前期沒有很少的設計緩存的話,是很有價值的,比如說對於一些配置類數據比如數據字典、系統參數、業務配置項等很少變化的數據。
5 執行期主要類總結
mybatis在執行期間,主要有四大核心接口對象:
- 執行器Executor,執行器負責整個SQL執行過程的總體控制。
- 參數處理器ParameterHandler,參數處理器負責PreparedStatement入參的具體設置。
- 語句處理器StatementHandler,語句處理器負責和JDBC層具體交互,包括prepare語句,執行語句,以及調用ParameterHandler.parameterize()設置參數。
- 結果集處理器ResultSetHandler,結果處理器負責將JDBC查詢結果映射到java對象。
5.1 執行器Executor
什么是執行器?所有我們在應用層通過sqlSession執行的各類selectXXX和增刪改操作在做了動態sql和參數相關的封裝處理后,都被委托給具體的執行器去執行,包括一、二級緩存的管理,事務的具體管理,Statement和具體JDBC層面優化的實現等等。所以執行器比較像是sqlSession下的各個策略工廠實現,用戶通過配置決定使用哪個策略工廠。只不過執行器在一個mybatis配置下只有一個,這可能無法適應於所有的情況,尤其是哪些微服務做得不是特別好的中小型公司,因為這些系統通常混搭了OLTP和ETL功能。先來看下執行器接口的定義:
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
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;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
boolean isCached(MappedStatement ms, CacheKey key);
void clearLocalCache();
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
Transaction getTransaction();
void close(boolean forceRollback);
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
mybatis提供了下列類型的執行器:
從上述可以看出,mybatis提供了兩種類型的執行器,緩存執行器與非緩存執行器(使用哪個執行器是通過配置文件中settings下的屬性defaultExecutorType控制的,默認是SIMPLE),是否使用緩存執行器則是通過執行cacheEnabled控制的,默認是true。
緩存執行器不是真正功能上獨立的執行器,而是非緩存執行器的裝飾器模式。
我們先來看非緩存執行器。非緩存執行器又分為三種,這三種類型的執行器都基於基礎執行器BaseExecutor,基礎執行器完成了大部分的公共功能,如下所示:
package org.apache.ibatis.executor;
...
public abstract class BaseExecutor implements Executor {
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// mybatis的二級緩存 PerpetualCache實際上內部使用的是常規的Map
protected PerpetualCache localCache;
// 用於存儲過程出參
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
// transaction的底層連接是否已經釋放
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
@Override
public Transaction getTransaction() {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
return transaction;
}
// 關閉本執行器相關的transaction
@Override
public void close(boolean forceRollback) {
try {
try {
rollback(forceRollback);
} finally {
if (transaction != null) {
transaction.close();
}
}
} catch (SQLException e) {
// Ignore. There's nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
transaction = null;
deferredLoads = null;
localCache = null;
localOutputParameterCache = null;
closed = true;
}
}
@Override
public boolean isClosed() {
return closed;
}
// 更新操作
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
@Override
public List<BatchResult> flushStatements() throws SQLException {
return flushStatements(false);
}
public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
return doFlushStatements(isRollBack);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
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--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
return doQueryCursor(ms, parameter, rowBounds, boundSql);
}
@Override
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
DeferredLoad deferredLoad = new DeferredLoad(resultObject, property, key, localCache, configuration, targetType);
if (deferredLoad.canLoad()) {
deferredLoad.load();
} else {
deferredLoads.add(new DeferredLoad(resultObject, property, key, localCache, configuration, targetType));
}
}
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
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);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
return localCache.getObject(key) != null;
}
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
// 接下去的4個方法由子類進行實現
protected abstract int doUpdate(MappedStatement ms, Object parameter)
throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
throws SQLException;
protected void closeStatement(Statement statement) {
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
// ignore
}
}
}
/**
* Apply a transaction timeout.
* @param statement a current statement
* @throws SQLException if a database access error occurs, this method is called on a closed <code>Statement</code>
* @since 3.4.0
* @see StatementUtil#applyTransactionTimeout(Statement, Integer, Integer)
*/
protected void applyTransactionTimeout(Statement statement) throws SQLException {
StatementUtil.applyTransactionTimeout(statement, statement.getQueryTimeout(), transaction.getTimeout());
}
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
final Object cachedParameter = localOutputParameterCache.getObject(key);
if (cachedParameter != null && parameter != null) {
final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
final MetaObject metaParameter = configuration.newMetaObject(parameter);
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
final String parameterName = parameterMapping.getProperty();
final Object cachedValue = metaCachedParameter.getValue(parameterName);
metaParameter.setValue(parameterName, cachedValue);
}
}
}
}
}
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 {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
@Override
public void setExecutorWrapper(Executor wrapper) {
this.wrapper = wrapper;
}
private static class DeferredLoad {
private final MetaObject resultObject;
private final String property;
private final Class<?> targetType;
private final CacheKey key;
private final PerpetualCache localCache;
private final ObjectFactory objectFactory;
private final ResultExtractor resultExtractor;
// issue #781
public DeferredLoad(MetaObject resultObject,
String property,
CacheKey key,
PerpetualCache localCache,
Configuration configuration,
Class<?> targetType) {
this.resultObject = resultObject;
this.property = property;
this.key = key;
this.localCache = localCache;
this.objectFactory = configuration.getObjectFactory();
this.resultExtractor = new ResultExtractor(configuration, objectFactory);
this.targetType = targetType;
}
public boolean canLoad() {
return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
}
public void load() {
@SuppressWarnings( "unchecked" )
// we suppose we get back a List
List<Object> list = (List<Object>) localCache.getObject(key);
Object value = resultExtractor.extractObjectFromList(list, targetType);
resultObject.setValue(property, value);
}
}
}
我們先來看下BaseExecutor的屬性,從上述BaseExecutor的定義可以看出:
- 執行器在特定的事務上下文下執行;
- 具有本地緩存和本地出參緩存(任何時候,只要事務提交或者回滾或者執行update或者查詢時設定了刷新緩存,都會清空本地緩存和本地出參緩存);
- 具有延遲加載任務;
BaseExecutor實現了大部分通用功能本地緩存管理、事務提交、回滾、超時設置、延遲加載等,但是將下列4個方法留給了具體的子類實現:
protected abstract int doUpdate(MappedStatement ms, Object parameter)
throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
throws SQLException;
從功能上來說,這三種執行器的差別在於:
- ExecutorType.SIMPLE:這個執行器類型不做特殊的事情。它為每個語句的每次執行創建一個新的預處理語句。
- ExecutorType.REUSE:這個執行器類型會復用預處理語句。
- ExecutorType.BATCH:這個執行器會批量執行所有更新語句,也就是jdbc addBatch API的facade模式。
所以這三種類型的執行器可以說時應用於不同的負載場景下,除了SIMPLE類型外,另外兩種要求對系統有較好的架構設計,當然也提供了更多的回報。
5.4.1 SIMPLE執行器
我們先來看SIMPLE各個方法的實現,
public class SimpleExecutor extends BaseExecutor {
public SimpleExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
@Override
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();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
@Override
protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>queryCursor(stmt);
}
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
return Collections.emptyList();
}
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;
}
}
簡單執行器的實現非常的簡單,我們就不展開詳述了。下面倆看REUSE執行器。
5.4.2 REUSE執行器
我們來看下REUSE執行器中和SIMPLE執行器不同的地方:
public class ReuseExecutor extends BaseExecutor {
private final Map<String, Statement> statementMap = new HashMap<String, Statement>();
public ReuseExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
}
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
}
@Override
protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>queryCursor(stmt);
}
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
for (Statement stmt : statementMap.values()) {
closeStatement(stmt);
}
statementMap.clear();
return Collections.emptyList();
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
if (hasStatementFor(sql)) {
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
private boolean hasStatementFor(String sql) {
try {
return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed();
} catch (SQLException e) {
return false;
}
}
private Statement getStatement(String s) {
return statementMap.get(s);
}
private void putStatement(String sql, Statement stmt) {
statementMap.put(sql, stmt);
}
}
從實現可以看出,REUSE和SIMPLE在doUpdate/doQuery上有個差別,不再是每執行一個語句就close掉了,而是盡可能的根據SQL文本進行緩存並重用,但是由於數據庫服務器端通常對每個連接以及全局的語句(oracle稱為游標)handler的數量有限制,oracle中是open_cursors參數控制,mysql中是mysql_stmt_close參數控制,這就會導致如果sql都是靠if各種拼接出來,日積月累可能會導致數據庫資源耗盡。其是否有足夠價值,視創建Statement語句消耗的資源占整體資源的比例、以及一共有多少完全不同的Statement數量而定,一般來說,純粹的OLTP且非自動生成的sqlmap,它會比SIMPLE執行器更好。
5.4.3 BATCH執行器
BATCH執行器的實現代碼如下:
public class BatchExecutor extends BaseExecutor {
public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
// 存儲在一個事務中的批量DML的語句列表
private final List<Statement> statementList = new ArrayList<Statement>();
// 存放DML語句對應的參數對象,包括自動/手工生成的key
private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
// 最新提交執行的SQL語句
private String currentSql;
// 最新提交執行的語句
private MappedStatement currentStatement;
public BatchExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
final Configuration configuration = ms.getConfiguration();
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
final Statement stmt;
// 如果最新執行的一條語句和前面一條語句相同,就不創建新的語句了,直接用緩存的語句,只是把參數對象添加到該語句對應的BatchResult中
// 否則的話,無論是否在未提交之前,還有pending的語句,都新插入一條語句到list中
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
handler.parameterize(stmt);//fix Issues 322
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); //fix Issues 322
currentSql = sql;
currentStatement = ms;
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// handler.parameterize(stmt);
// 調用jdbc的addBatch方法
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {
Statement stmt = null;
try {
flushStatements();
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
@Override
protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException {
flushStatements();
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
Connection connection = getConnection(ms.getStatementLog());
Statement stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return handler.<E>queryCursor(stmt);
}
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<BatchResult>();
if (isRollback) {
return Collections.emptyList();
}
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
// Close statement to close cursor #1109
closeStatement(stmt);
} catch (BatchUpdateException e) {
StringBuilder message = new StringBuilder();
message.append(batchResult.getMappedStatement().getId())
.append(" (batch index #")
.append(i + 1)
.append(")")
.append(" failed.");
if (i > 0) {
message.append(" ")
.append(i)
.append(" prior sub executor(s) completed successfully, but will be rolled back.");
}
throw new BatchExecutorException(message.toString(), e, results, batchResult);
}
results.add(batchResult);
}
return results;
} finally {
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
}
批量執行器是JDBC Statement.addBatch的實現,對於批量insert而言比如導入大量數據的ETL,驅動器如果支持的話,能夠大幅度的提高DML語句的性能(首先最重要的是,網絡交互就大幅度減少了),比如對於mysql而言,在5.1.13以上版本的驅動,在連接字符串上rewriteBatchedStatements參數也就是jdbc:mysql://192.168.1.100:3306/test?rewriteBatchedStatements=true后,性能可以提高幾十倍,參見 https://www.cnblogs.com/kxdblog/p/4056010.html 以及 http://blog.sina.com.cn/s/blog_68b4c68f01013yog.html 。因為BatchExecutor對於每個statementList中的語句,都執行executeBatch()方法,因此最差的極端情況是交叉執行不同的DML SQL語句,這種情況退化為原始的方式。比如下列形式就是最差的情況:
for(int i=0;i<100;i++) {
session.update("insertUser", userReq);
session.update("insertUserProfile", userReq);
}
5.4.4 緩存執行器CachingExecutor的實現
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
@Override
public Transaction getTransaction() {
return delegate.getTransaction();
}
@Override
public void close(boolean forceRollback) {
try {
//issues #499, #524 and #573
if (forceRollback) {
tcm.rollback();
} else {
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
@Override
public boolean isClosed() {
return delegate.isClosed();
}
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
@Override
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);
}
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
flushCacheIfRequired(ms);
return delegate.queryCursor(ms, parameter, rowBounds);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
// 首先判斷是否啟用了二級緩存
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 然后判斷緩存中是否有對應的緩存條目(正常情況下,執行DML操作會清空緩存,也可以語句層面明確明確設置),有的話則返回,這樣就不用二次查詢了
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); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public List<BatchResult> flushStatements() throws SQLException {
return delegate.flushStatements();
}
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
@Override
public void rollback(boolean required) throws SQLException {
try {
delegate.rollback(required);
} finally {
if (required) {
tcm.rollback();
}
}
}
// 存儲過程不支持二級緩存
private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement.");
}
}
}
}
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
return delegate.isCached(ms, key);
}
@Override
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
delegate.deferLoad(ms, resultObject, property, key, targetType);
}
@Override
public void clearLocalCache() {
delegate.clearLocalCache();
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
@Override
public void setExecutorWrapper(Executor executor) {
throw new UnsupportedOperationException("This method should not be called");
}
}
緩存執行器相對於其他執行器的差別在於,首先是在query()方法中判斷是否使用二級緩存(也就是mapper級別的緩存)。雖然mybatis默認啟用了CachingExecutor,但是如果在mapper層面沒有明確設置二級緩存的話,就退化為SimpleExecutor了。二級緩存的維護由TransactionalCache(事務化緩存)負責,當在TransactionalCacheManager(事務化緩存管理器)中調用putObject和removeObject方法的時候並不是馬上就把對象存放到緩存或者從緩存中刪除,而是先把這個對象放到entriesToAddOnCommit和entriesToRemoveOnCommit這兩個HashMap之中的一個里,然后當執行commit/rollback方法時再真正地把對象存放到緩存或者從緩存中刪除,具體可以參見TransactionalCache.commit/rollback方法。
還有一個差別是使用了TransactionalCacheManager管理事務,其他邏輯就一樣了。
5.2 參數處理器ParameterHandler
ParameterHandler的接口定義如下:
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps)
throws SQLException;
}
ParameterHandler只有一個默認實現DefaultParameterHandler,它的代碼如下:
public class DefaultParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;
public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
this.mappedStatement = mappedStatement;
this.configuration = mappedStatement.getConfiguration();
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.parameterObject = parameterObject;
this.boundSql = boundSql;
}
@Override
public Object getParameterObject() {
return parameterObject;
}
// 設置PreparedStatement的入參
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
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;
String propertyName = parameterMapping.getProperty();
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);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
}
ParameterHandler的實現很簡單,上面在執行語句的時候詳細解釋了每個步驟,這里就不重復了。
5.3 語句處理器StatementHandler
先來看下StatementHandler的定義:
public interface StatementHandler {
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
void parameterize(Statement statement)
throws SQLException;
void batch(Statement statement)
throws SQLException;
int update(Statement statement)
throws SQLException;
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
從接口可以看出,StatementHandler主要包括prepare語句、給語句設置參數、執行語句獲取要執行的SQL語句本身。mybatis包含了三種類型的StatementHandler實現:
分別用於JDBC對應的PrepareStatement,Statement以及CallableStatement。BaseStatementHandler是這三種類型語句處理器的抽象父類,封裝了一些實現細節比如設置超時時間、結果集每次提取大小等操作,代碼如下:
public abstract class BaseStatementHandler implements StatementHandler {
protected final Configuration configuration;
protected final ObjectFactory objectFactory;
protected final TypeHandlerRegistry typeHandlerRegistry;
protected final ResultSetHandler resultSetHandler;
protected final ParameterHandler parameterHandler;
protected final Executor executor;
protected final MappedStatement mappedStatement;
protected final RowBounds rowBounds;
protected BoundSql boundSql;
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.objectFactory = configuration.getObjectFactory();
if (boundSql == null) { // issue #435, get the key before calculating the statement
// 首先執行SelectKey對應的SQL語句把ID生成
generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}
this.boundSql = boundSql;
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}
@Override
public BoundSql getBoundSql() {
return boundSql;
}
@Override
public ParameterHandler getParameterHandler() {
return parameterHandler;
}
// prepare SQL語句
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
// 創建Statement
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
// 不同類型語句的初始化過程不同,比如Statement語句直接調用JDBC java.sql.Connection.createStatement,而PrepareStatement則是調用java.sql.Connection.prepareStatement
protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
// 設置JDBC語句超時時間,注:數據庫服務器端也可以設置語句超時時間。mysql通過參數max_statement_time設置,oracle截止12.2c不支持
protected void setStatementTimeout(Statement stmt, Integer transactionTimeout) throws SQLException {
Integer queryTimeout = null;
if (mappedStatement.getTimeout() != null) {
queryTimeout = mappedStatement.getTimeout();
} else if (configuration.getDefaultStatementTimeout() != null) {
queryTimeout = configuration.getDefaultStatementTimeout();
}
if (queryTimeout != null) {
stmt.setQueryTimeout(queryTimeout);
}
StatementUtil.applyTransactionTimeout(stmt, queryTimeout, transactionTimeout);
}
// fetchSize設置每次從服務器端提取的行數,默認不同數據庫實現不同,mysql一次性提取全部,oracle默認10。正確設置fetchSize可以避免OOM並且對性能有一定的影響,尤其是在網絡延時較大的情況下
protected void setFetchSize(Statement stmt) throws SQLException {
Integer fetchSize = mappedStatement.getFetchSize();
if (fetchSize != null) {
stmt.setFetchSize(fetchSize);
return;
}
Integer defaultFetchSize = configuration.getDefaultFetchSize();
if (defaultFetchSize != null) {
stmt.setFetchSize(defaultFetchSize);
}
}
protected void closeStatement(Statement statement) {
try {
if (statement != null) {
statement.close();
}
} catch (SQLException e) {
//ignore
}
}
protected void generateKeys(Object parameter) {
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
ErrorContext.instance().store();
keyGenerator.processBefore(executor, mappedStatement, null, parameter);
ErrorContext.instance().recall();
}
}
5.4 結果集處理器ResultSetHandler
結果集處理器,顧名知義,就是用了對查詢結果集進行處理的,目標是將JDBC結果集映射為業務對象。其接口定義如下:
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
接口中定義的三個接口分別用於處理常規查詢的結果集,游標查詢的結果集以及存儲過程調用的出參設置。和參數處理器一樣,結果集處理器也只有一個默認實現DefaultResultSetHandler。結果集處理器的功能包括對象的實例化、屬性自動匹配計算、常規屬性賦值、嵌套ResultMap的處理、嵌套查詢的處理、鑒別器結果集的處理等,每個功能我們在分析SQL語句執行selectXXX的時候都詳細的講解過了,具體可以參見selectXXX部分。
6 插件
插件幾乎是所有主流框架提供的一種擴展方式之一,插件可以用於記錄日志,統計運行時性能,為核心功能提供額外的輔助支持。在mybatis中,插件是在內部是通過攔截器實現的。要開發自定義自定義插件,只要實現org.apache.ibatis.plugin.Interceptor接口即可,Interceptor接口定義如下:
public interface Interceptor {
//執行代理類方法
Object intercept(Invocation invocation) throws Throwable;
// 用於創建代理對象
Object plugin(Object target);
// 插件自定義屬性
void setProperties(Properties properties);
}
mybatis提供了一個示例插件ExamplePlugin,如下所示:
@Intercepts({})
public class ExamplePlugin implements Interceptor {
private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
public Properties getProperties() {
return properties;
}
}
不過這個例子一點也不完整,沒有體現出插件/攔截器的強大之處,mybatis提供了為插件配置提供了兩個注解:org.apache.ibatis.plugin.Signature和org.apache.ibatis.plugin.Intercepts。
Intercepts注解用來指示當前類是一個攔截器,它的定義如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
它有一個類型為Signature數組的value屬性,如果沒有指定,它會攔截StatementHandler、ResultSetHandler、ParameterHandler和Executor這四個核心接口對象中的所有方法。如需改變默認行為,可以通過明確設置value的值,Signature的定義如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
Class<?> type();
String method();
Class<?>[] args();
}
比如:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class}),
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
在實際使用中,使用最頻繁的mybatis插件應該算是分頁查詢插件了,最流行的應該是com.github.pagehelper.PageHelper了。下面我們就來看下PageHelper的詳細實現。
6.1 分頁插件PageHelper詳解
6.2 自定義監控插件StatHelper實現
看過了PageHelper的實現,現在我們來實現一個簡單的統計各個sql語句執行時間的插件StatHelper。
7 與spring集成
generated by haroopad