前言
筆者大概是從今年的5月份開始喜歡上源碼閱讀的,起初是閱讀徐郡明前輩的《Mybatis技術内幕》入的坑,不得不說大佬就是大佬,書中講得東西很細很全。半年過去了,筆者對mybatis略知一二,也開始在為公司搭架構,并且基于Mybatis寫了一套架構,但是盡管如此還是感覺自己對于源碼的了解稍微有點淺。好比是初高中學數學吧,光看例題不做題是記不住的,是以産生了為mybatis寫注釋的想法,想要通過寫注釋的過程,加強對mybatis的了解。雖然現在網上已經有了較全的mybatis中文注釋,但是感覺還是經過自己手敲更能加強記憶,是以便挖下了這個大坑。筆者也希望可以在一年内把這個坑填完,後續關于其他技術的文章可能就比較少,大多數應該就都是mybatis源碼閱讀犀利了
在這裡,附上我的碼雲位址(别問我為什麼是碼雲而不是github,下半天代碼下不動急死人)
mybatis中文注釋
同時,我也很歡迎更多的國中級開發者投入到閱讀源碼的行列,并且很樂意大家在我的倉庫上建立自己的分支,希望可以和大家一同進步。
入口
Mybatis
初始化入口檔案是SqlSessionFactoryBuilder。該類通過調用XMLConfigBuilder.parse方法初始化配置檔案。
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.
}
}
}
在XMLConfigBuilder.parse方法中,會先校驗配置檔案是否已經解析過了,如果重複解析就抛出異常
public Configuration parse() {
if (parsed) {
// 已經解析過就不再解析。這裡隻解析一次
throw new BuilderException("每個 XMLConfigBuilder 隻能使用一次.");
}
parsed = true;
// 擷取configuration節點進行解析
// mybatis解析配置檔案使用的是XPathParser,這裡的evalNode方法就是解析xml的代碼
// 這裡對XPathParser不進行注釋,這不屬于mybatis的範疇(其實是懶。)
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
parseConfiguration方法中,傳入configuration節點配置,對mybatis-config.xml檔案中的該節點進行解析。解析結果會set到Configuration類中。今天隻注釋完了properties和settings兩個節點的解析
/**
* 解析mybatis-config.xml檔案
*
* @param root
*/
private void parseConfiguration(XNode root) {
try {
// 解析properties節點。該節點用來引入外部的資源檔案,如db.properties
propertiesElement(root.evalNode("properties"));
// 解析settings節點,校驗配置中的配置項是否合法。該節點用來設定一些mybatis的配置項
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加載使用者自己配置的虛拟檔案系統
loadCustomVfs(settings);
// 加載日志
loadCustomLogImpl(settings);
// TODO 解析typeAliases節點,下次繼續
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);
}
}
先看propertiesElement方法,該方法用于解析properties節點。
/**
* 解析mybatis-config.xml的properties節點
* 将節點中所有的配置set到Configuration中
*
* @param context
* @throws Exception
*/
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 解析拿到節點下的所有子節點配置
Properties defaults = context.getChildrenAsProperties();
// 擷取properties節點的resource屬性
String resource = context.getStringAttribute("resource");
// 擷取properties節點的url屬性。
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
// resource和url屬性隻能同時存在一個。
throw new BuilderException("properties節點不能同時指定resource和url兩個屬性.");
}
// url和resource屬性隻能同時存在一個
// 讀取引入的資源檔案所有屬性,put到properties節點之下
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
// 如果configuration之前已經有了配置,也put進去
// put這些設定是為了能夠保證後面set回configuration時可以set所有的配置
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
// 将Properties節點下所有的配置set到configuration
configuration.setVariables(defaults);
}
}
接着就是解析settings節點,該節點用于配置一些mybatis配置項
/**
* 解析settings節點
*
* @param context
* @return
*/
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 擷取settings節點下所有的setting節點
Properties props = context.getChildrenAsProperties();
// 通過Configuration擷取metaClass,用于友善對Configuration進行操作
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
// 周遊setting配置
// 如果Configuration沒有這個set方法,說明該配置是無效的
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("配置 " + key + " 無效. 請檢查拼寫是否正确.");
}
}
// 校驗完settings之後傳回
return props;
}
解析完settings節點後,程式會加載使用者配置的虛拟檔案系統和日志。
/**
* 加載使用者自己設定的虛拟檔案系統
*
* @param props
* @throws ClassNotFoundException
*/
private void loadCustomVfs(Properties props) throws ClassNotFoundException {
// 從settings中拿到name是vfsImpl的配置節點
String value = props.getProperty("vfsImpl");
if (value != null) {
String[] clazzes = value.split(",");
for (String clazz : clazzes) {
if (!clazz.isEmpty()) {
@SuppressWarnings("unchecked")
Class<? extends VFS> vfsImpl = (Class<? extends VFS>) Resources.classForName(clazz);
// 加載檔案系統,set到Configuration中
configuration.setVfsImpl(vfsImpl);
}
}
}
}
/**
* 加載日志。代碼比較簡單
* 就是從settings中拿到name為logImpl的配置項
* 然後set到Configuration中去
*
* @param props
*/
private void loadCustomLogImpl(Properties props) {
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
引申
上面的代碼中使用到了MetaClass類和Configuration類。下面對這兩個類進行解釋。
首先是Configuration類。該類通過名稱可以很明顯的知道這是mybatis的配置類,對應的是mybatis-config.xml檔案的配置。其中今天将properties和settings節點對應的字段進行注釋。
public class Configuration {
/**
* mybatis-config.xml屬性
* settings節點
* 允許嵌套語句中使用分頁
*/
protected boolean safeRowBoundsEnabled;
/**
* mybatis-config.xml屬性
* settings節點
* 是否開啟自動駝峰命名規則映射
* 即從經典資料庫列名 a_column 到經典 Java 屬性名 aColumn 的類似映射。
*/
protected boolean mapUnderscoreToCamelCase;
/**
* mybatis-config.xml檔案下
* settings節點
* 當啟用時,對任意延遲屬性的調用會使帶有延遲加載屬性的對象完整加載;
* 反之,每種屬性将會按需加載。
*/
protected boolean aggressiveLazyLoading;
/**
* mybatis-config.xml檔案下
* settings節點
* 是否允許單一語句傳回多條結果集
*/
protected boolean multipleResultSetsEnabled = true;
/**
* mybatis-config.xml檔案
* settings節點
* 允許 JDBC 支援自動生成主鍵
*/
protected boolean useGeneratedKeys;
/**
* mybatis-config.xml檔案
* settings節點
* 使用列标簽代替列名
*/
protected boolean useColumnLabel = true;
/**
* mybatis-config.xml檔案
* settings節點
* 該配置影響的所有映射器中配置的緩存的全局開關
*/
protected boolean cacheEnabled = true;
/**
* mybatis-config.xml檔案
* settings節點
* 指定當結果集中值為null的時候是否調用映射對象的set方法
*/
protected boolean callSettersOnNulls;
/**
* mybatis-config.xml檔案
* settings節點
* 指定MyBatis增加到日志名稱的字首
*/
protected String logPrefix;
/**
* mybatis-config.xml檔案
* settings節點
* 指定MyBatis所用日志的具體實作
*/
protected Class<? extends Log> logImpl;
/**
* mybatis-config.xml檔案
* settings節點
* VFS含義是虛拟檔案系統;
* 主要是通過程式能夠友善讀取本地檔案系統、FTP檔案系統等系統中的檔案資源。
* Mybatis中提供了VFS這個配置。
* 主要是通過該配置可以加載自定義的虛拟檔案系統應用程式
* 多個檔案系統使用逗号隔開
*/
protected Class<? extends VFS> vfsImpl;
/**
* mybatis-config.xml檔案
* settings節點
* mybatis利用本地緩存機制防止循環引用的加速重複嵌套查詢。
* 預設是SESSION,這種情況會緩存一個會話中執行的所有查詢
* 如果是STATEMENT,本地會話僅用在語句執行上
* 對相同的SqlSession的不同調用将不會共享資料
*/
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
/**
* mybatis-config.xml檔案
* settings節點
* 當沒有為菜蔬提供特定的JDBC類型時
* 為空值制定JDBC類型
*/
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
/**
* mybatis-config.xml
* settings節點
* 指定哪個對象的方法觸發一次延遲加載
*/
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
/**
* mybatis-config.xml檔案
* settings節點
* 設定逾時時間
*/
protected Integer defaultStatementTimeout;
/**
* mybatis-config.xml檔案
* settings節點
* 為驅動程式設定提示以控制傳回結果的擷取大小
*/
protected Integer defaultFetchSize;
/**
* mybatis-config.xml檔案
* settings節點
* 配置預設的執行器。
* SIMPLE 就是普通的執行器;
* REUSE 執行器會重用預處理語句(PreparedStatements);
* BATCH 執行器将重用語句并執行批量更新。
*/
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
/**
* mybatis-config.xml檔案
* settings節點
* 指定 MyBatis 應如何自動映射列到字段或屬性。
* NONE 表示取消自動映射;
* PARTIAL 隻會自動映射沒有定義嵌套結果集映射的結果集。
* FULL 會自動映射任意複雜的結果集
*/
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
/**
* mybatis-config.xml檔案下
* properties節點的所有配置
* 以及該節點對應的resource和url的所有配置
* 在XMLConfigBuilder.propertiesElement方法中進行初始化
*/
protected Properties variables = new Properties();
/**
* mybatis-config.xml檔案
* settings節點屬性
* 延遲加載的全局開關。
* 當開啟時,所有關聯對象都會延遲加載。
* 特定關聯關系中可通過設定fetchType屬性來覆寫該項的開關狀态
*/
protected boolean lazyLoadingEnabled = false;
/**
* mybatis-config.xml檔案
* settings節點
* 指定Mybatis建立具有延遲加載能力對象所用到的代理工廠
*/
protected ProxyFactory proxyFactory = new JavassistProxyFactory();
/**
* 将資料庫類型轉換成Java類型
*/
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
/**
* 存儲掃包得到的别名
*/
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
}
而MetaClass是反射工具箱裡的一個類。Reflector是Mybatis中反射子產品的基礎,每個Reflector對象都對應一個類,在該對象中緩存了反射操作需要使用的元資訊,如:可讀屬性、可寫屬性、get、set方法等。ReflectorFactory接口主要實作了對Reflector對象的建立和緩存。而MetaClass則是對Reflector和reflectorFactory的封裝,使其更友善通過反射去操作一個類。
這裡就不帖MetaClass的代碼了,感興趣可以自行閱讀。
結語
今天因為時間充裕是以寫的部落格比較清晰,後面可能會因為加班是以部落格僅僅是對代碼注釋的複制粘貼,還希望讀者可以諒解。這個坑我會繼續填下去的。
最後需要提一下java裡的一個容易被忽視的規範,也是面試、大學考試經常喜歡問的内容。
類中定義的成員變量也稱之為“字段”,而屬性則是指get和set方法。屬性隻與方法有關而與字段無關。如一個類中存在getName()和setName(String name)方法,不管該類中有沒有name字段,我們都認為它有name這個屬性。反之如果隻有字段name而沒有對應的get/set方法,則該類僅僅是有name這個字段而沒有name屬性。後面對于get/set方法我不會稱之為屬性,但是有必要厘清楚這兩個概念。