mybatis 3.x源碼深度解析與最佳實踐php
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下載。 css
MyBatis是當前最流行的java持久層框架之一,其經過XML配置的方式消除了絕大部分JDBC重複代碼以及參數的設置,結果集的映射。雖然mybatis是最爲流行的持久層框架之一,可是相比其餘開源框架好比spring/netty的源碼來講,其註釋相對而言顯得比較少。爲了更好地學習和理解mybatis背後的設計思路,做爲高級開發人員,有必要深刻研究瞭解優秀框架的源碼,以便更好的借鑑其思想。同時,框架做爲設計模式的主要應用場景,經過研究優秀框架的源碼,能夠更好的領會設計模式的精髓。學習框架源碼和學習框架自己不一樣,咱們不只要首先比較細緻完整的熟悉框架提供的每一個特性,還要理解框架自己的初始化過程等,除此以外,更重要的是,咱們不能泛泛的快速瀏覽哪一個功能是經過哪一個類或者接口實現和封裝的,對於核心特性和初始化過程的實現,咱們應該達到下列目標:html
首先,和從使用層面相同,咱們在理解實現細節的時候,也會涉及到學習A的時候,引用到B中的部分概念,B又引用了C的部分概念,C反過來又依賴於D接口和A的相關接口操做,D又有不少不一樣的具體實現。在使用層面,咱們一般不須要深刻去理解B的具體實現,只要知道B的概念和能夠提供什麼特性就能夠了。在實現層面,咱們必須去所有掌握這全部依賴的實現,直到最底層JDK原生操做或者某些咱們熟悉的類庫爲止。這實際上涉及到橫向流程和縱向切面的交叉引用,以及兩個概念的接口和多種實現之間的交叉調用。因此,咱們在整個系列中會先按照業務流程橫向的方式進行總體分析,並附上必要的流程圖,在整個大流程完成以後,在一個專門的章節安排對關鍵的類進行詳細分析,它們的適用場景,這樣就能夠交叉引用,又不會在主流程中夾雜太多的干擾內容。
其次,由於咱們免不了會對源碼的主要部分作必要的註釋,因此,對源碼的講解和註釋會穿插進行,對於某些部分經過註釋後已經徹底可以知道其含義的實現,就不會在畫蛇添足的重述。前端
從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。java
要理解mybatis的內部原理,最好的方式是直接從頭開始,而不是從和spring的集成開始。每個MyBatis的應用程序都以一個SqlSessionFactory對象的實例爲核心。SqlSessionFactory對象的實例能夠又經過SqlSessionFactoryBuilder對象來得到。SqlSessionFactoryBuilder對象能夠從XML配置文件加載配置信息,而後建立SqlSessionFactory。先看下面的例子:
主程序:node
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(); } } }
配置文件:mysql
<?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文件jquery
<?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接口nginx
package org.mybatis.internal.example.mapper; import org.mybatis.internal.example.pojo.User; public interface UserMapper { public User getUser(int lfPartyId); }
運行上述程序,不出意外的話,將輸出:git
1,匯金超級管理員
從上述代碼能夠看出,SqlSessionFactoryBuilder是一個關鍵的入口類,其中承擔了mybatis配置文件的加載,解析,內部構建等職責。下一章,咱們將重點分析mybatis配置文件的加載,配置類的構建等一系列細節。
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; } }
首先判斷有沒有解析過配置文件,只有沒有解析過才容許解析。其中調用了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構造器執行期間初始化的。下面咱們來每塊分析。
解析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。此時開始全部的屬性就能夠在隨後的整個配置文件中使用了。
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中的默認值不必定是真正的默認值。
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
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的效果是同樣的。
幾乎全部優秀的框架都會預留插件體系以便擴展,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章詳細講解自定義插件的實現。
什麼是對象工廠?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>
對象包裝器工廠主要用來包裝返回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); } }
由於加載配置文件中的各類插件類等等,爲了提供更好的靈活性,mybatis支持用戶自定義反射工廠,不過整體來講,用的很少,要實現反射工廠,只要實現ReflectorFactory接口便可。默認的反射工廠是DefaultReflectorFactory。通常來講,使用默認的反射工廠就能夠了。
環境能夠說是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()); } } } }
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 } } } } }
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 類型,不過這種行爲能夠經過兩種方法改變:
還能夠建立一個泛型類型處理器,它能夠處理多於一個類。爲達到此目的, 須要增長一個接收該類做爲參數的構造器,這樣在構造一個類型處理器的時候 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"/>
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文件的解析加載與初始化。
前面說過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(); }
總體流程爲:
一、首先加載mapper接口對應的xml文件並解析。loadXmlResource和經過resource、url解析相同,都是解析mapper文件中的定義,他們的入口都是XMLMapperBuilder.parse(),咱們稍等會兒專門專門分析,這一節先來看經過註解方式配置的mapper的解析(注:對於一個mapper接口,不能同時使用註解方式和xml方式,任什麼時候候只能之一,可是不一樣的mapper接口能夠混合使用這兩種方式)。
二、解析緩存註解;
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中的是一一對應的。緩存的解析很簡單,這裏不展開細細講解。
三、解析緩存參照註解。緩存參考的解析也很簡單,這裏不展開細細講解。
四、解析非橋接方法。在正式開始以前,咱們先來看下什麼是橋接方法。橋接方法是 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; }
五、二次解析pending的方法。
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); } }
其主要過程是:
一、解析緩存參照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中。
二、解析緩存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下的緩存。
三、解析參數映射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 >
整體來講,目前已經不推薦使用參數映射,而是直接使用內聯參數。因此咱們這裏就不展開細講了。若有必要,咱們後續會補上。
四、解析結果集映射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。
鑑別器很是容易理解,它的表現很像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的實現確實是很複雜。
五、解析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的解析與內部表示。
六、解析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節點總的來講比較簡單:
一、加載相關屬性;
二、使用語言驅動器建立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。
三、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裏面進行了防重複處理。
四、最後爲SelectKey對應的sql語句建立並維護一個KeyGenerator。
這一部分到mybatis語言驅動器一節一塊兒講解。
到此爲止,crud部分的解析和加載就完成了。如今咱們來看看這個語言驅動器。
雖然官方名稱叫作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的實現以下:
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); } }
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); } }
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); } }
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; } }
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); } }
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的一種特殊實現。
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); } }
和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的建立,這是兩方面緣由:1、靜態SQL是動態SQL的一部分;2、靜態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的建立。動態SQL主要在運行時由上下文調用SqlSource.getBoundSql()接口獲取。它在處理了動態標籤以及${}以後,調用靜態SQL構建器返回PreparedStatement,也就是靜態SQL形式。
到此爲止,整個mapper文件的第一輪解析就完成了。
在第一次解析期間,有可能由於加載順序或者其餘緣由,致使部分依賴的resultMap或者其餘沒有徹底完成解析,因此針對pending的結果映射、緩存參考、CRUD語句進行遞歸從新解析直到完成。
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的具體實現,以下所示:
SqlNode接口主要用來處理CRUD節點下的各種動態標籤好比、,對每一個動態標籤,mybatis都提供了對應的SqlNode實現,這些動態標籤能夠相互嵌套且實現上採用單向鏈表進行應用,這樣後面若是須要增長其餘動態標籤,就只須要新增對應的SqlNode實現就能支持。mybatis使用OGNL表達式語言。對sqlNode的調用在SQL執行期間的DynamicSqlSource.getBoundSql()方法中,SQL執行過程咱們後面會講解。
當前版本的SqlNode有下列實現:
其中MixedSqlNode表明了全部具體SqlNode的集合,其餘分別表明了一種類型的SqlNode。下面對每一個SqlNode的實現作簡單的分析:
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; } }
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(); } } }
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."); } }
靜態文本節點不作任何處理,直接將本文本節點的內容追加到已經解析了的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主要是用來將${}轉換爲實際的參數值,並返回拼接後的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; } } }
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); }
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直接委託給TrimSqlNode處理。參見TrimSqlNode。
WhereSqlNode直接委託給TrimSqlNode處理。參見TrimSqlNode。
從整個設計角度來講,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接口,要麼就用組合代替繼承。
額外參數主要是維護一些在加載時沒法肯定的參數,好比標籤中的參數在加載時就沒法盡最大努力肯定,必須經過運行時執行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。
當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進行代理出入參的設置和獲取。
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。
MetaObject是一個對象包裝器,其性質上有點相似ASF提供的commons類庫,其中包裝了對象的元數據信息,對象自己,對象反射工廠,對象包裝器工廠等。使得根據OGNL表達式設置或者獲取對象的屬性更爲便利,也能夠更加方便的判斷對象中是否包含指定屬性、指定屬性是否具備getter、setter等。主要的功能是經過其ObjectWrapper類型的屬性完成的,它包裝了操做對象元數據以及對象自己的主要接口,操做標準對象的實現是BeanWrapper。BeanWrapper類型有個MetaClass類型的屬性,MetaClass中有個Reflector屬性,其中包含了可讀、可寫的屬性、方法以及構造器信息。
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.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,可參見:
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的數據結構表示外,自己並無提供額外的功能。
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; ... }
每一個鑑別器節點都表示爲一個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); } }
在原生jdbc中,咱們要執行一個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(); } } }
一樣的,在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調用的第三、四、五、6步。SqlSession接口的定義以下:
可知,絕大部分的方法都是泛型方法,也能夠說採用了模板方法實現。
獲取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接口的一個實現,而後調用具體的方法。除非早期,如今實際開發中,咱們通常採用這種方式。
咱們先來看第一種形式的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默認提供了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重載,這三個重載完成的功能從最裏面到最外面分別是: 一、使用構造器建立目標對象類型; 先判斷是否有目標對象類型的處理器,有的話,直接調用類型處理器(這裏爲何必定是原生類型)建立目標對象 若是沒有,判斷是否有構造器,有的話,使用指定的構造器建立目標對象(構造器裏面若是嵌套了查詢或者ResultMap,則進行處理) 若是結果類型是接口或者具備默認構造器,則使用ObjectFactory建立默認目標對象 最後判斷是否能夠應用自動映射,默認是對非嵌套查詢,只要沒有明確設置AutoMappingBehavior.NONE就能夠,對於嵌套查詢,AutoMappingBehavior.FULL就能夠。自動映射的邏輯是先找目標對象上具備@AutomapConstructor註解的構造器,而後根據ResultSet返回的字段清單找匹配的構造器,若是找不到,就報錯 二、若是此時建立的對象不爲空,且不須要應用結果對象處理器,判斷有沒有延遲加載且具備嵌套查詢的屬性,若是有的話,則爲對象建立一個代理,額外存儲後面fetch的時候進行延遲加載所需的信息。返回對象。 三、若是此時建立的對象不爲空,且不須要應用結果對象處理器,若是對象須要自動映射,則先進行自動映射(建立自動映射列表的過程爲:先找到在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類型的屬性,主要是:一、維護記錄cacheKey和父屬性/記錄對Map的關聯關係,便於在處理嵌套ResultMap時很快能夠找到全部須要處理嵌套結果集的父屬性;二、維護父屬性和對應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; }
看完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>格式。
看完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的不一樣之處在於:
經過分析delete/insert,咱們會發現他們內部都委託給update實現了,因此咱們就不作重複的分析了。
經過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,這裏咱們就不一一講解。
準確的說,只要mybatis的的crud語句中包含了、等標籤或者${}以後,就已經算是動態sql了,因此只要在mybatis加載mapper文件期間被解析爲非StaticSqlSource,就會被當作動態sql處理,在執行selectXXX或者update/insert/delete期間,就會調用對應的SqlNode接口和TextSqlNode.isDynamic()處理各自的標籤以及${},並最終將每一個sql片斷處理到StaticTextSqlNode並生成最終的參數化靜態SQL語句爲止。因此,能夠說,在絕大部分非PK查詢的狀況下,咱們都是在使用動態SQL。
若是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()); } } } } } }
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操做或者設置強制提交纔會認爲應該進行事務提交或者回滾操做。
對於不一樣的執行器,在提交和回滾執行的邏輯不同,由於每一個執行器在一級、二級、語句緩存上的差別:
上述邏輯執行完成後,會執行提交/回滾操做。對於緩存執行器,在提交/回滾完成以後,會將TransactionCache中的entriesMissedInCache和entriesToAddOnCommit列表分別移動到語句對應的二級緩存中或清空掉。
只要實現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
在二級緩存容器的具體回收策略實現上,有下列幾種:
在緩存的設計上,Mybatis的全部Cache算法都是基於裝飾器/Composite模式對PerpetualCache擴展增長功能。
對於模塊化微服務系統來講,應該來講mybatis的一二級緩存對業務數據都不適合,尤爲是對於OLTP系統來講,CRM/BI這些不算,若是要求數據很是精確的話,也不是特別合適。對這些要求數據準確的系統來講,儘量只使用mybatis的ORM特性比較靠譜。可是有一部分數據若是前期沒有不多的設計緩存的話,是頗有價值的,好比說對於一些配置類數據好比數據字典、系統參數、業務配置項等不多變化的數據。
mybatis在執行期間,主要有四大核心接口對象:
什麼是執行器?全部咱們在應用層經過sqlSession執行的各種selectXXX和增刪改操做在作了動態sql和參數相關的封裝處理後,都被委託給具體的執行器去執行,包括1、二級緩存的管理,事務的具體管理,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的定義能夠看出:
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;
從功能上來講,這三種執行器的差異在於:
咱們先來看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執行器。
咱們來看下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執行器更好。
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後,性能能夠提升幾十倍,參見 http://www.javashuo.com/article/p-mkzfsstq-bg.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); }
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管理事務,其餘邏輯就同樣了。
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的實現很簡單,上面在執行語句的時候詳細解釋了每一個步驟,這裏就不重複了。
先來看下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(); } }
結果集處理器,顧名知義,就是用了對查詢結果集進行處理的,目標是將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部分。
插件幾乎是全部主流框架提供的一種擴展方式之一,插件能夠用於記錄日誌,統計運行時性能,爲核心功能提供額外的輔助支持。在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的詳細實現。
看過了PageHelper的實現,如今咱們來實現一個簡單的統計各個sql語句執行時間的插件StatHelper。
generated by haroopad