咱們知道在使用 Mybatis
時,咱們須要經過 SqlSessionFactoryBuild
去建立 SqlSessionFactory
實例,譬如:java
// resource 爲 mybatis 的配置文件 InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
那麼咱們看下 build
方法的具體實現sql
public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { // 建立 XMLConfigBuilder 實例並執行解析 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) { } } } public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
Mybatis
主要經過 XMLConfigBuilder
執行對配置文件的解析,具體實現以下文:數據庫
private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 解析 properties 標籤 propertiesElement(root.evalNode("properties")); // 解析 settings 標籤 Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); // 解析 typeAliases 別名標籤 typeAliasesElement(root.evalNode("typeAliases")); // 解析 plugins 插件標籤 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 // 解析 environments 標籤 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 解析 typeHandlers 標籤 typeHandlerElement(root.evalNode("typeHandlers")); // 解析 mappers 標籤 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
從 XMLConfigBuilder
的方法 parseConfiguration
實現咱們知道,MyBatis
會依次解析配置文件中的相應標籤,本文將針對開發中經常使用的配置進行分析;主要包括 properties
, typeAliases
, enviroments
, typeHandlers
, mappers
。apache
<configuration> <!-- 能夠指定 resource 屬性,也能夠指定 url 屬性 --> <properties resource="org/mybatis/example/config.properties"> <property name="username" value="dev_user"/> <property name="password" value="F2Fa3!33TYyg"/> </properties> </configuration>
從配置示例能夠看出 properties
屬性變量的來源能夠是外部的配置文件,也能夠是配置文件中自定義的,也能夠是 SqlSessionFactoryBuilder
的 build
方法傳參譬如:session
public SqlSessionFactory build(InputStream inputStream, Properties properties) { return build(inputStream, null, properties); }
那麼當存在同名的屬性時,將採用哪一種方式的屬性值呢?
private void propertiesElement(XNode context) throws Exception { if (context != null) { // 獲取 properties 標籤下的全部 property 子標籤 Properties defaults = context.getChildrenAsProperties(); // 獲取 resource,url 屬性 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) { // 加載 resource 指定的配置文件 defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { // 加載 url 指定的配置文件 defaults.putAll(Resources.getUrlAsProperties(url)); } /** * 獲取傳參的 properties * 構建 sqlSessionFactory 時能夠傳參 properties * * @see SqlSessionFactoryBuilder.build(InputStream inputStream, Properties properties) */ Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } parser.setVariables(defaults); // 將 properties 賦值 configuration 中的 variables 變量 configuration.setVariables(defaults); } }
public Properties getChildrenAsProperties() { Properties properties = new Properties(); // 遍歷 properties 標籤下的 propertry 子標籤 for (XNode child : getChildren()) { // 獲取 propertry 的 name value 屬性 String name = child.getStringAttribute("name"); String value = child.getStringAttribute("value"); if (name != null && value != null) { properties.setProperty(name, value); } } return properties; }
從 properties
標籤解析的實現來看,MyBatis
加載 properties
屬性的過程以下:數據結構
properties
標籤內全部子標籤的 property
properties
標籤屬性 resource
或 url
指定的外部屬性配置SqlSessionFactoryBuilder
的方法 build
傳參的屬性配置所以,經過方法參數傳遞的properties
具備最高優先級,resource/url 屬性中指定的配置文件次之,最低優先級的是properties
標籤內的子標籤property
指定的屬性。
類型別名是爲 Java 類型設置一個短的名字。它只和 XML 配置有關,存在的意義僅在於用來減小類徹底限定名的冗餘
<typeAliases> <typeAlias alias="Author" type="domain.blog.Author"/> <typeAlias alias="Blog" type="domain.blog.Blog"/> <typeAlias alias="Comment" type="domain.blog.Comment"/> </typeAliases>
也能夠指定一個包名,MyBatis
會在包名下面搜索須要的 Java Bean,好比:mybatis
<typeAliases> <package name="domain.blog"/> </typeAliases>
private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { // 若是是 package 標籤,對整個包下的 java bean 進行別名處理 // 若 java bean 沒有配置註解的話,使用 bean 的首字母小寫類名做爲別名 // 若 java bean 配置了註解,使用註解值做爲別名 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); } } } } }
typeAliasesElement
在對 typeAliases
標籤解析時,針對採用 package
和 typeAlias
兩種配置方式進行了不一樣的解析。 下面咱們先看下經過包名的配置方式app
public void registerAliases(String packageName) { registerAliases(packageName, Object.class); } public void registerAliases(String packageName, Class<?> superType) { // 獲取包下全部的類 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses(); for (Class<?> type : typeSet) { // Ignore inner classes and interfaces (including package-info.java) // Skip also inner classes. See issue #6 // 忽略內部類 接口 if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { registerAlias(type); } } } public void registerAlias(Class<?> type) { // 別名爲類名 String alias = type.getSimpleName(); // 是否配置了別名註解,若配置了則別名取註解值 Alias aliasAnnotation = type.getAnnotation(Alias.class); if (aliasAnnotation != null) { alias = aliasAnnotation.value(); } registerAlias(alias, type); }
當經過 package
指定包名時,MyBatis
會掃描包下全部的類(忽略內部類,接口),若類沒有采用 @Alias
註解的狀況下,會使用 Bean 的首字母小寫的非限定類名來做爲它的別名, 好比 domain.blog.Author
的別名爲 author;如有註解,則別名爲其註解值。dom
public void registerAlias(String alias, Class<?> value) { if (alias == null) { throw new TypeException("The parameter alias cannot be null"); } // issue #748 // 別名小寫處理 String key = alias.toLowerCase(Locale.ENGLISH); if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'."); } // 別名與類映射 typeAliases.put(key, value); }
在完成別名的解析以後會將其註冊到 typeAliasRegistry
的變量 typeAliases
Map 集合中。ide
environments
用於事務管理器及數據源相關配置
<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> <environment id="test"> <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>
從environments
的配置來看MyBatis
是支持多數據源的,但每一個SqlSessionFactory
實例只能選擇其中一個; 若須要鏈接多個數據庫,就得須要建立多個SqlSessinFactory
實例。
private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { /** * @see org.apache.ibatis.session.SqlSessionFactoryBuilder.build 時未指定 enviorment, 則取默認的 */ 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 environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); // 設置配置環境 configuration.setEnvironment(environmentBuilder.build()); } } } }
private boolean isSpecifiedEnvironment(String id) { if (environment == null) { // 若 environment 爲空說明未指定當前 SqlSessionFactory 實例所需的配置環境;同時 environments 標籤未配置 default 屬性 throw new BuilderException("No environment specified."); } else if (id == null) { // environment 標籤須要配置 id 屬性 throw new BuilderException("Environment requires an id attribute."); } else if (environment.equals(id)) { // environment == id 說明當前匹配配置環境 return true; } return false; }
因 environments
支持多數據源的配置,因此在解析時會先查找匹配當前 SqlSessionFactory
的 environment
; 而後在解析當前配置環境所需的事務管理器和數據源。
private TransactionFactory transactionManagerElement(XNode context) throws Exception { if (context != null) { // 獲取配置事務管理器的類別,也就是別名 String type = context.getStringAttribute("type"); // 獲取事務屬性配置 Properties props = context.getChildrenAsProperties(); // 經過別名查找對應的事務管理器類並實例化 TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a TransactionFactory."); }
事務管理器解析時會經過配置中指定的 type
別名去查找對應的 TransactionFactory
並實例化。
那麼
MyBatis
內部內置了哪些事務管理器呢?
public Configuration() { typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); // 省略 }
從 Configuration
的構造能夠看出,其構造時會經過 typeAliasRegistry
註冊了別名爲 JDBC
,MANAGED
的兩種事務管理器。
private DataSourceFactory dataSourceElement(XNode context) throws Exception { if (context != null) { // 獲取配置數據源的類別,也就是別名 String type = context.getStringAttribute("type"); // 獲取數據源屬性配置 Properties props = context.getChildrenAsProperties(); // 經過別名查找數據源並實例化 DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a DataSourceFactory."); }
同事務管理器同樣,數據源解析時也會經過指定的別名查找對應的數據源實現類一樣其在 Configuration
構造時向 typeAliasRegistry
註冊了三種數據源
public Configuration() { // 省略 typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); // 省略 }
<typeHandlers> <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/> </typeHandlers>
<typeHandlers> <package name="org.mybatis.example"/> </typeHandlers>
private void typeHandlerElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { String typeHandlerPackage = child.getStringAttribute("name"); typeHandlerRegistry.register(typeHandlerPackage); } else { // 映射 java 對象類型 String javaTypeName = child.getStringAttribute("javaType"); // 映射 jdbc 類型 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) { // 指定了 java type,未指定 jdbc type typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { // 指定了 java type,指定了 jdbc type typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { // 未指定 java type 按 typeHandlerClass 註冊 typeHandlerRegistry.register(typeHandlerClass); } } } } }
public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) { register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass)); }
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null) { // 一個 java type 可能會映射多個 jdbc type Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType); if (map == null || map == NULL_TYPE_HANDLER_MAP) { map = new HashMap<>(); typeHandlerMap.put(javaType, map); } map.put(jdbcType, handler); } // 存儲 typeHandler allTypeHandlersMap.put(handler.getClass(), handler); }
當指定了javaType
和jdbcType
最終會將兩者及typeHandler
映射並註冊到typeHandlerMap
中,從typeHandlerMap
的數據結構來看,javaType
可能會與多個jdbcType
映射。 譬如String
->CHAR
,VARCHAR
。
public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) { // 將 type handler 實例化 register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass)); }
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) { // 獲取 MappedJdbcTypes 註解 // 該註解用於設置類型轉換器匹配的 jdbcType MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class); if (mappedJdbcTypes != null) { // 遍歷匹配的 jdbcType 並註冊 for (JdbcType handledJdbcType : mappedJdbcTypes.value()) { register(javaType, handledJdbcType, typeHandler); } if (mappedJdbcTypes.includeNullJdbcType()) { register(javaType, null, typeHandler); } } else { // 未指定 jdbcType 時按 null 處理 register(javaType, null, typeHandler); } }
當類型轉換器配置了javaType
未配置jdbcType
時,會判斷類型轉換器是否配置了@MappedJdbcTypes
註解; 若配置了則使用註解值做爲jdbcType
並註冊,若未配置則按 null 註冊。
public void register(Class<?> typeHandlerClass) { boolean mappedTypeFound = false; // 獲取 MappedTypes 註解 // 該註解用於設置類型轉換器匹配的 javaType MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class<?> javaTypeClass : mappedTypes.value()) { // 執行註冊 register(javaTypeClass, typeHandlerClass); mappedTypeFound = true; } } if (!mappedTypeFound) { register(getInstance(null, typeHandlerClass)); } }
當javaType
,jdbcType
均爲指定時,會判斷類型轉換器是否配置了@MappedTypes
註解; 若配置了則使用註解值做爲javaType
並註冊。
public void register(String packageName) { // 掃描指定包下的全部類 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); 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
配置文件中標籤 properties
,typeAliases
,enviroments
,typeHandlers
的解析過程,因爲 mappers
的解析比較複雜後續在進行分析;經過本文的分析咱們瞭解到 Configuration
實例中包括如下內容:
typeAliases
存儲別名與類的映射關係javaType
與 jdbcType
,typeHandler
的映射關係,內置 jdbcType
與 typeHandler
的映射關係