一盐数、解析配置文件
上一篇文章說到边琉,SqlSessionFactory的配置都是委托給XMLConfigBuilder的parse方法完成的抑进,本篇就來看看解析工程都做了哪些事情戈泼。
//解析配置
public Configuration parse() {
//如果已經(jīng)解析過了骂倘,報錯
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//根節(jié)點是configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
首先檢查下是否已經(jīng)解析過了眼滤,如果直接拋異常,在這里貼一份常見的mybatis配置文件历涝,便于理解:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
Mybatis解析xml用的是自己封裝的一個名為XPathParser的工具類(使用的全是JDK的類包)诅需,至于怎么具體解析的,這里不做展開荧库。如果是第一次解析堰塌,則從“/configuation”節(jié)點開始解析,代碼如下:
private void parseConfiguration(XNode root) {
try {
//分步驟解析
//1.properties
propertiesElement(root.evalNode("properties"));
//2.類型別名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.對象工廠
objectFactoryElement(root.evalNode("objectFactory"));
//5.對象包裝工廠
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.設(shè)置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.環(huán)境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.類型處理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
二分衫、十個解析項
上邊的代碼就是解析配置的核心场刑,總共10個步驟,我們一一來看看:
2.1丐箩、properties元素
在properties下配置如下屬性:
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
可以在該配置文件上全局使用摇邦,用于替換其它配置的變量,比如:
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
另外屎勘,配置的Properties還可以傳遞到SqlSessionFactoryBuilder.build() 方法中使用施籍,如下
SqlSessionFactory factory =
sqlSessionFactoryBuilder.build(reader, props);
// ... or ...
SqlSessionFactory factory =
new SqlSessionFactoryBuilder.build(reader, environment, props);
如果相同的配置項存在不同的位置,加載是怎樣的概漱?我們從代碼里一看究竟丑慎,代碼如下:
if (context != null) {
//properties元素下的屬性會被首先加載
Properties defaults = context.getChildrenAsProperties();
//第二順序被加載的是類路徑下的resource或者url,如果與之前指定的屬性重復(fù),會覆蓋掉之前加載的屬性
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("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));
}
//通過方法參數(shù)被傳遞進來的最后被加載竿裂,它會覆蓋掉前面兩步所有與之重復(fù)的屬性
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
看代碼邏輯玉吁,屬性加載的順序如下:
- properties元素下的屬性會被首先加載
- 第二順序被加載的是類路徑下的resource或者url,如果與之前指定的屬性重復(fù)腻异,會覆蓋掉之前加載的屬性进副。
- 通過方法參數(shù)被傳遞進來的最后被加載,它會覆蓋掉前面兩步所有與之重復(fù)的屬性悔常。
2.2影斑、typeAliases元素
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>
別名是為了簡化java類型的實體全路徑名過長的問題,如上机打,通過配置矫户,Blog可以在任何地方代替domain.blog.Blog使用.
同理,也可以給J整個包起一個別名残邀,方便使用皆辽,如果沒有特殊說明使用全小寫的類名作為別名:
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
另外,也支持通過注解的形式指定別名
@Alias("author")
public class Author {
...
}
看一看解析別名的源碼
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//如果是package
String typeAliasPackage = child.getStringAttribute("name");
//(一)調(diào)用TypeAliasRegistry.registerAliases芥挣,去包下找所有類,然后注冊別名(有@Alias注解則用驱闷,沒有則取類的simpleName)
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//如果是typeAlias
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
//根據(jù)Class名字來注冊類型別名
//(二)調(diào)用TypeAliasRegistry.registerAlias
if (alias == null) {
//alias可以省略
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
...
}
}
}
}
}
可以看出在解析別名過程中會從上至下遍歷所有別名,登記到別名的map中九秀。
如果我在配置別名時寫成:
<typeAliases>
<typeAlias alias="goods_alias" type="org.apache.ibatis.wyjdemo.MbGoods"/>
<package name="org.apache.ibatis.wyjdemo"/>
</typeAliases>
即單獨指定和通過包名同時配置遗嗽,注冊時會變成怎么樣呢?打斷點看下
可以看到兩種方式都生效了鼓蜒,兩個別名指向了一個bean.
2.3痹换、pluginElement元素
Mybatis支持拓展插件實現(xiàn)豐富的功能,它允許你在映射語句執(zhí)行過程匯中的某一點進行攔截調(diào)用都弹,通常來說娇豫,Mybatis允許插件在以下幾個方法調(diào)用攔截:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
這些類方法的詳細介紹可以在發(fā)行的源碼中方法簽名查看更多細節(jié)。想要編寫插件也非常簡單畅厢,只需要實現(xiàn)Interceptor接口冯痢,并指定要攔截的方法簽名即可。
我們自己實現(xiàn)一個非常簡單的插件來感受下框杜,插件的作用浦楣,首先要實現(xiàn)Interceptor接口,
@Intercepts({@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class }
)})
public class MySimplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 執(zhí)行目標(biāo)方法的前置處理
Object[] args = invocation.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
Object returnObject = invocation.proceed();
// 執(zhí)行目標(biāo)方法的后置處理
return returnObject;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
簽名這里我配置的是Excutor,并且攔截query方法多一些文章咪辱,實現(xiàn)比較簡單就是打印出查詢參數(shù)振劳,然后再配置文件中加上,插件相關(guān)配置:
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.apache.ibatis.wyjdemo.MySimplePlugin">
</plugin>
</plugins>
執(zhí)行輸出如下
org.apache.ibatis.mapping.MappedStatement@38082d64
{id=12, param1=12}
org.apache.ibatis.session.RowBounds@dfd3711
null
這個四個值依次對應(yīng)油狂,@Signature注解中args屬性配置的值历恐。
說回正題寸癌,我們看下源碼中是如何解析插件的配置的
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 interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
//調(diào)用InterceptorChain.addInterceptor
configuration.addInterceptor(interceptorInstance);
}
}
}
源碼很簡單,總體來說就是從配置文件中解拿到插件配置的攔截器名稱弱贼,然后實例化一個攔截器對象放到攔截器鏈中等待處理蒸苇。核心就是調(diào)用resovleClass方法根據(jù)配置的攔截名獲取一個該攔截的實例化對象。它又會調(diào)用resolveAlias實際去處理吮旅,resolveAlias源碼如下:
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
//原理就很簡單了溪烤,從HashMap里找對應(yīng)的鍵值,找到則返回類型別名對應(yīng)的Class
if (TYPE_ALIASES.containsKey(key)) {
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
//找不到庇勃,再試著將String直接轉(zhuǎn)成Class(這樣怪不得我們也可以直接用java.lang.Integer的方式定義氛什,也可以就int這么定義)
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
內(nèi)容就是先從上一步維護的別名映射map中找尋是否已經(jīng)擁有該實例(也就是攔截器中的配置也是支持別名的),沒有找到的話用反射得到一個實例返回匪凉。
2.4、objectFactoryElement元素
對象工廠主要的作用就是生產(chǎn)封裝結(jié)果集所需要的對象捺檬,Mybatis默認的對象工廠再层,不會對結(jié)果集做特殊的處理,僅是實例化一個目標(biāo)對象堡纬,如果需要對結(jié)果對象進行定制化的處理聂受,則要繼承DefaultObjectFactory,重寫父類方法烤镐,源碼也非常簡單蛋济,如下
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties properties = context.getChildrenAsProperties();
ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
factory.setProperties(properties);
configuration.setObjectFactory(factory);
}
}
2.5、objectFactoryElement元素
對象包裝工程炮叶,官網(wǎng)文檔上沒有介紹碗旅,我自己也沒有搞明白,先略過镜悉。祟辟。。
2.6侣肄、settingsElement元素
顧名思義是對Mybatis進行設(shè)置旧困,這會影響Mybatis的運行方式,可以理解為是Mybatis的一些不同工能的開關(guān)稼锅,源碼非常簡單:
private void settingsElement(XNode context) throws Exception {
if (context != null) {
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
//檢查下是否在Configuration類里都有相應(yīng)的setter方法(沒有拼寫錯誤)
MetaClass metaConfig = MetaClass.forClass(Configuration.class);
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).");
}
}
//下面非常簡單吼具,一個個設(shè)置屬性
//如何自動映射列到字段/ 屬性
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
...
}
}
源碼中可以看到,設(shè)置前先檢查了setting節(jié)點中配置的屬性key值的有效性矩距,如果不是Configuration.class中的屬性則在啟動時就拋出異常拗盒,之后就是逐個讀配置設(shè)置到configuration中了。(另:這個過程中用到的Reflector類剩晴,可以學(xué)習(xí)下)
2.7锣咒、environmentsElement元素
Mybatis支持多個環(huán)境配置侵状,這允許我們寫的SQL可以映射到多個數(shù)據(jù)庫,例如毅整,我們的開發(fā)趣兄、測試、生產(chǎn)會有不同環(huán)境等悼嫉,在很多場景下都會有不同環(huán)境配置的需求艇潭,通過這個配置可以滿足。
但是要注意戏蔑,盡管我們可以配置多個環(huán)境蹋凝,但是每個環(huán)境只能選擇一個sqlSeesionFactory實例。如果我們需要連接兩個數(shù)據(jù)庫总棵,就需要創(chuàng)建兩個sqlSessionFactory,以此類推鳍寂。很容易記著:每個數(shù)據(jù)都需要一個sqlSessionFactroy與之對應(yīng)
看下配置文件:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
源碼部分:
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");
//循環(huán)比較id是否就是指定的environment
if (isSpecifiedEnvironment(id)) {
//7.1事務(wù)管理器
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//7.2數(shù)據(jù)源
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
首先判斷是否指定了環(huán)境,未指定就使用默認環(huán)境情龄,然后遍歷environment節(jié)點進行配置迄汛,由上邊代碼可以看到配置的主要內(nèi)容:加載事務(wù)管理器、數(shù)據(jù)源分別到TransactionFactory和DataSourceFactory骤视,然后賦值到configuration中鞍爱。
2.8、databaseIdProviderElement元素
Mybaits還支持根據(jù)不同數(shù)據(jù)庫廠商執(zhí)行不同的SQL語句专酗,其實不是很實用睹逃,很少生產(chǎn)項目會用不同廠商的數(shù)據(jù)庫,這里也不多做介紹祷肯。
2.9沉填、typeHandlerElement元素
類型處理器,Mybatis無論是在設(shè)置參數(shù)或者從結(jié)果集獲取數(shù)據(jù)都會用到類型處理器佑笋,它主要的功能是
獲取到的值轉(zhuǎn)換成對應(yīng)的Java類型數(shù)據(jù)拜轨。
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
<typeHandlers>
<package name="org.mybatis.example"/>
</typeHandlers>
看配置文件就可以知道,這個元素的方式也是同時支持單個處理器配置以及掃描包下的處理器兩種方式的允青,源碼如下:
private void typeHandlerElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果是package
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
//(一)調(diào)用TypeHandlerRegistry.register橄碾,去包下找所有類
typeHandlerRegistry.register(typeHandlerPackage);
} else {
//如果是typeHandler
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);
//(二)調(diào)用TypeHandlerRegistry.register(以下是3種不同的參數(shù)形式)
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
循環(huán)遍歷typeHandlers節(jié)點下元素,優(yōu)先掃描包下處理器颠锉,不存在則去單配總獲取法牲。可自行實現(xiàn)定制化的typeHandler,參考 自定義typeHandler
2.10琼掠、mapperElement元素
既然上邊幾個步驟我們配置了這么多的元素拒垃,最后我們要配置SQL映射了,但是怎么找到配置的映射關(guān)系呢瓷蛙?
Mybatis提供四種方法悼瓮,從配置文件可以看出
<!-- Using classpath relative resources -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- Using url fully qualified paths -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- Using mapper interface classes -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- Register all interfaces in a package as mappers -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
結(jié)合源碼看:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//10.4自動掃描包下所有映射器
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) {
//10.1使用類路徑
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比較復(fù)雜戈毒,調(diào)用XMLMapperBuilder
//注意在for循環(huán)里每個mapper都重新new一個XMLMapperBuilder,來解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//10.2使用絕對url路徑
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//映射器比較復(fù)雜横堡,調(diào)用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//10.3使用java類名
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.");
}
}
}
}
}
- 首先看是否用掃描包的方式配置埋市。
- 如果是非包掃描方式,則會從resource命贴、url道宅、classs三種方式中按照順序加載一種方式。
- 上述方式都沒有采用的話胸蛛,則會拋出異常污茵。
總結(jié)
至此,XMLConfigbuilder初始化的過程我們已經(jīng)過了一遍葬项,這些步驟其實就是一個讀Mybatis配置文件的過程泞当,為后續(xù)的運行進行環(huán)境配置以及參數(shù)設(shè)置,到這一步民珍,Configuration已經(jīng)被“填充”完畢零蓉。