重拾-MyBatis-配置文件解析
前言
我们知道在使用 Mybatis
时,我们需要通过 SqlSessionFactoryBuild
去创建 SqlSessionFactory
实例,譬如:
// resource 为 mybatis 的配置文件 InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
那么我们看下 build
方法的具体实现
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
。
properties 解析
配置示例
<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
方法传参譬如:
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
指定的属性。
typeAliases 解析
类型别名是为 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,比如:
<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
两种配置方式进行了不同的解析。 下面我们先看下通过包名的配置方式
通过包名解析
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;若有注解,则别名为其注解值。
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 集合中。
配置环境 environments 解析
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 解析
配置示例
<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); } } } } }
typeHandler 解析
指定 javaType 和 jdbcType
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
。
指定 javaType 未指定 jdbcType
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 注册。
未指定 javaType 和 jdbcType
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
并注册。
package 解析
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
实例中包括以下内容:
- variables : Properties 类型,存储属性变量
- typeAliasRegistry : 别名注册中心,通过一个 Map 集合变量
typeAliases
存储别名与类的映射关系 - environment : 配置环境,绑定事务管理器和当前数据源
- typeHandlerRegistry : 类型转换器注册中心,存储
javaType
与jdbcType
,typeHandler
的映射关系,内置jdbcType
与typeHandler
的映射关系