上一篇我们曾约定 mybatis-config.xml
文件为配置文件,SQL 语句配置文件为映射文件,本文我们将沿用上一篇中的示例程序,一起探究一下 MyBatis 加载和解析配置文件(即 mybatis-config.xml
)的过程。
配置文件的加载过程
在示例程序中,执行配置文件(包括后面要介绍的映射文件)加载与解析的过程位于第一行代码中(如下)。其中,Resources 是一个简单的基于类路径或其它位置获取数据流的工具类,借助该工具类可以获取配置文件的 InputStream 流对象,然后将其传递给 SqlSessionFactoryBuilder#build
方法以构造 SqlSessionFactory 对象。
1 2 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream("mybatis-config.xml" ));
SqlSessionFactoryBuilder 由名字可知它是一个构造器,用于构造 SqlSessionFactory 对象。按照 MyBatis 的官方文档来说,SqlSessionFactoryBuilder 一旦构造完 SqlSessionFactory 对象便完成了其使命。其实现也比较简单,只定义了 SqlSessionFactoryBuilder#build
这一个方法及其重载版本,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public SqlSessionFactory build (InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return this .build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession." , e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { } } } public SqlSessionFactory build (Configuration config) { return new DefaultSqlSessionFactory(config); }
上述实现的核心在于如下两行:
1 2 1. XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);2. return this .build(parser.parse());
第一行用来构造 XMLConfigBuilder 对象,XMLConfigBuilder 可以看作是 mybatis-config.xml
配置文件的解析器;第二行则调用该对象的 XMLConfigBuilder#parse
方法对配置文件进行解析,并记录相关配置项到 Configuration 对象中,然后基于该配置对象创建 SqlSessionFactory 对象返回。Configuration 可以看作是 MyBatis 框架内部全局唯一的配置类,用于记录几乎所有的配置和映射,以及运行过程中的中间值。后面我们会经常遇到这个类,现在可以将其理解为 MyBatis 框架的配置中心。
我们来看一下 XMLConfigBuilder 对象的构造过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public XMLConfigBuilder (InputStream inputStream, String environment, Properties props) { this ( new XPathParser(inputStream, true , props, new XMLMapperEntityResolver()), environment, props); } private XMLConfigBuilder (XPathParser parser, // XPath 解析器 String environment, // 当前使用的配置文件组 ID Properties props) { super (new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration" ); this .configuration.setVariables(props); this .parsed = false ; this .environment = environment; this .parser = parser; }
构造方法各参数的释义见代码注释。这里针对一些比较不太直观的参数作进一步说明,首先看一下 XPathParser 类型的构造参数。我们需要知道的一点是,MyBatis 基于 DOM 树对 XML 配置文件进行解析,而操作 DOM 树的方式则是基于 XPath(XML Path Language) 。它是一种能够极大简化 XML 操作的路径语言,优点在于简单、直观,并且好用,没有接触过的同学可以针对性的学习一下。XPathParser 基于 XPath 语法对 XML 进行解析,其实现比较简单,这里不展开说明。
接着看一下 environment 参数。基于配置的框架一般都允许配置多套环境,以应对开发、测试、灰度,以及生产环境。除了后面会讲到的 <environment/>
配置,MyBatis 也允许我们通过参数指定实际生效的配置环境,我们在调用 SqlSessionFactoryBuilder#build
方法时,可以以参数形式指定当前使用的配置环境。
配置文件的解析过程
完成了 XMLConfigBuilder 对象的构造,下一步会调用其 XMLConfigBuilder#parse
方法执行对配置文件的解析操作。在具体分析配置文件的解析过程之前,先简单介绍一下后续过程依赖的一些基础组件。
上面用到的 XMLConfigBuilder 类派生自 BaseBuilder 抽象类,包括后面会介绍的 XMLMapperBuilder、XMLStatementBuilder,以及 SqlSourceBuilder 等都继承自该抽象类。先来看一下 BaseBuilder 的字段定义:
1 2 3 4 5 6 protected final Configuration configuration;protected final TypeAliasRegistry typeAliasRegistry;protected final TypeHandlerRegistry typeHandlerRegistry;
BaseBuilder 仅定义了三个属性,各属性的作用见代码注释。XMLConfigBuilder 构造方法调用了父类 BaseBuilder 的构造方法以实现对这三个属性的初始化,前面我们提及到的封装全局配置的 Configuration 对象就记录在这里。接下来分析一下属性 BaseBuilder#typeAliasRegistry
和 BaseBuilder#typeHandlerRegistry
分别对应的 TypeAliasRegistry 类和 TypeHandlerRegistry 类的功能和实现。
我们都知道在编写 SQL 语句时可以为表名或列名定义别名(alias),以减少书写量,而 TypeAliasRegistry 是对别名这一机制的延伸,借助于此,我们可以为任意类型定义别名。
TypeAliasRegistry 中仅定义了一个 Map 类型的属性 TypeAliasRegistry#typeAliases
充当内存数据库,记录着别名与具体类型之间的映射关系。TypeAliasRegistry 持有一个无参数的构造方法,其中只做一件事,即调用 TypeAliasRegistry#registerAlias
方法为常用类型注册对应的别名。该方法的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public void registerAlias (String alias, Class<?> value) { if (alias == null ) { throw new TypeException("The parameter alias cannot be null" ); } 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); }
整个方法的执行过程本质上就是将 (alias, value)
键值对写入 Map 集合中,只是在插入之前需要保证 alias 不为 null,且不允许相同的别名和类型重复注册。除了这里的单个注册,TypeAliasRegistry 还提供了 TypeAliasRegistry#registerAliases
方法,允许扫描注册指定 package 下面的所有类或指定类型及其子类型。在批量扫描注册时,我们可以利用 @Alias
注解为类指定别名,否则 MyBatis 将会以当前类的 simple name 作为类型别名。
当然,能够注册就能够获取,方法 TypeAliasRegistry#resolveAlias
提供了获取指定别名对应类型的能力。实现比较简单,无非就是从 Map 集合中获取指定 key 对应的 value。
再来看一下 TypeHandlerRegistry 类,在开始分析之前我们必须对 TypeHandler 接口有一个了解。我们都知道 JDBC 定义的类型(枚举类 JdbcType 对已有 JDBC 类型进行了封装)与 java 定义的类型并不是完全匹配的,所以就需要在这中间执行一些转换操作,而 TypeHandler 的职责就在于此。TypeHandler 是一个接口,其中定义了 4 个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 接口的实现类用于处理特定类型,具体可以参考 官方文档 。
对 TypeHandler 有一个基本认识之后,继续来看 TypeHandlerRegistry。顾名思义,这是一个 TypeHandler 的注册中心。TypeHandlerRegistry 中定义了多个 final 类型 Map 类型属性,以记录类型及其类型处理器 TypeHandler 之间的映射关系,其中最核心的两个属性定义如下:
1 2 3 4 5 6 7 8 9 10 11 private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
在构造 TypeHandlerRegistry 对象时,会调用 TypeHandlerRegistry#register
方法注册类型及其对应的类型处理器,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 private void register (Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null ) { Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType); if (map == null || map == NULL_TYPE_HANDLER_MAP) { map = new HashMap<>(); } map.put(jdbcType, handler); typeHandlerMap.put(javaType, map); } allTypeHandlersMap.put(handler.getClass(), handler); }
上述方法的核心逻辑在于往 TypeHandlerRegistry#typeHandlerMap
属性中注册 java 类型及其类型处理器。MyBatis 基于该方法封装了多层重载版本,其中大部分实现都比较简单,下面就基于注解 @MappedJdbcTypes
和注解 @MappedTypes
指定对应类型的版本进一步说明。
注解 @MappedJdbcTypes
用于指定类型处理器 TypeHandler 关联的 JDBC 类型列表,对应的解析实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private <T> void register (Type javaType, TypeHandler<? extends T> typeHandler) { MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class); if (mappedJdbcTypes != null ) { for (JdbcType handledJdbcType : mappedJdbcTypes.value()) { this .register(javaType, handledJdbcType, typeHandler); } if (mappedJdbcTypes.includeNullJdbcType()) { this .register(javaType, null , typeHandler); } } else { this .register(javaType, null , typeHandler); } }
上述方法首先获取注解 @MappedJdbcTypes
配置的 JDBC 类型列表,然后遍历挨个注册。注解 @MappedJdbcTypes
定义如下:
1 2 3 4 5 6 7 8 9 10 11 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MappedJdbcTypes { JdbcType[] value(); boolean includeNullJdbcType () default false ; }
该注解还允许通过 MappedJdbcTypes#includeNullJdbcType
属性指定是否允许当前类型处理器处理 null 值。
能够指定 JDBC 类型,当然也就能够指定 JAVA 类型。注解 @MappedTypes
用于指定与类型处理器 TypeHandler 关联的 java 类型,对应的解析实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public <T> void register (TypeHandler<T> typeHandler) { boolean mappedTypeFound = false ; MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class); if (mappedTypes != null ) { for (Class<?> handledType : mappedTypes.value()) { this .register(handledType, typeHandler); mappedTypeFound = true ; } } if (!mappedTypeFound && typeHandler instanceof TypeReference) { try { TypeReference<T> typeReference = (TypeReference<T>) typeHandler; this .register(typeReference.getRawType(), typeHandler); mappedTypeFound = true ; } catch (Throwable t) { } } if (!mappedTypeFound) { this .register((Class<T>) null , typeHandler); } }
上述方法首先获取 @MappedTypes
注解配置,并针对关联的 java 类型逐一注册。如果未指定 @MappedTypes
注解配置,则 MyBatis 会尝试自动发现并注册 TypeHandler 能够处理的 java 类型。
能够注册也就能够获取,TypeHandlerRegistry 中提供了 TypeHandlerRegistry#getTypeHandler
方法的多种重载实现,比较简单,不再展开。
回过头再来看一下 BaseBuilder 抽象类的实现,其中定义了许多方法,但是只要了解上面介绍的 TypeAliasRegistry 和 TypeHandlerRegistry 类,那么这些方法的作用在理解上应该非常容易,这里就不多做撰述,有兴趣的同学可以参考上面的分析去阅读一下源码。
下面正式进入主题,回到 XMLConfigBuilder#parse
方法分析配置文件的解析过程,实现如下:
1 2 3 4 5 6 7 8 9 10 public Configuration parse () { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once." ); } parsed = true ; this .parseConfiguration(parser.evalNode("/configuration" )); return configuration; }
配置文件 mybatis-config.xml
以 <configuration/>
标签作为配置文件根节点,上述方法的核心在于触发调用 XMLConfigBuilder#parseConfiguration
方法对配置文件的各个元素进行解析,并封装解析结果到 Configuration 对象中,最终返回该配置对象。方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private void parseConfiguration (XNode root) { try { this .propertiesElement(root.evalNode("properties" )); Properties settings = this .settingsAsProperties(root.evalNode("settings" )); this .loadCustomVfs(settings); this .loadCustomLogImpl(settings); this .typeAliasesElement(root.evalNode("typeAliases" )); this .pluginElement(root.evalNode("plugins" )); this .objectFactoryElement(root.evalNode("objectFactory" )); this .objectWrapperFactoryElement(root.evalNode("objectWrapperFactory" )); this .reflectorFactoryElement(root.evalNode("reflectorFactory" )); this .settingsElement(settings); this .environmentsElement(root.evalNode("environments" )); this .databaseIdProviderElement(root.evalNode("databaseIdProvider" )); this .typeHandlerElement(root.evalNode("typeHandlers" )); this .mapperElement(root.evalNode("mappers" )); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
上述方法在实现上比较直观,各配置项的解析都采用专门的方法进行封装,接下来会逐一进行分析。其中 <plugins/>
标签用于配置自定义插件,以拦截 SQL 语句的执行过程,相应的解析过程暂时先不展开,留到后面专门介绍插件的实现机制的文章中一并分析。
解析 properties 标签
先来看一下 <properties/>
标签怎么玩,其中的配置项可以在整个配置文件中用来动态替换占位符。配置项可以从外部 properties 文件读取,也可以通过 <property/>
子标签指定。假设我们希望通过该标签指定数据源配置,如下:
1 2 3 4 5 <properties resource ="datasource.properties" > <property name ="driver" value ="com.mysql.jdbc.Driver" /> <property name ="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value ="true" /> </properties >
文件 datasource.properties
内容:
1 2 3 url =jdbc:mysql://localhost:3306/test username =root password =123456
然后可以基于 OGNL 表达式在其它配置项中引用这些配置值,如下:
1 2 3 4 5 6 <dataSource type ="POOLED" > <property name ="driver" value ="${driver}" /> <property name ="url" value ="${url}" /> <property name ="username" value ="${username:zhenchao}" /> <property name ="password" value ="${password}" /> </dataSource >
其中,除了 driver 属性值来自 <property/>
子标签,其余属性值均是从 datasource.properties
配置文件中获取的。
MyBatis 针对配置的读取顺序约定如下:
在 <properties/>
标签体内指定的属性首先被读取;
然后,根据 <properties/>
标签中 resource 属性读取类路径下配置文件,或根据 url 属性指定的路径读取指向的配置文件,并覆盖已读取的同名配置项;
最后,读取方法参数传递的配置项,并覆盖已读取的同名配置项。
下面分析一下 <properties/>
标签的解析过程,由 XMLConfigBuilder#propertiesElement
方法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private void propertiesElement (XNode context) throws Exception { if (context != null ) { Properties defaults = context.getChildrenAsProperties(); 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)); } Properties vars = configuration.getVariables(); if (vars != null ) { defaults.putAll(vars); } parser.setVariables(defaults); configuration.setVariables(defaults); } }
由 MyBatis 的官方文档可知,标签 <properties/>
支持以 resource 属性或 url 属性指定配置文件所在的路径,由上述实现也可以看出这两个属性配置是互斥的。在将对应的配置加载成为 Properties 对象之后,上述方法会合并 Configuration 对象中已有的配置项,并将结果再次填充到 XPathParser 和 Configuration 对象中,以备后用。
解析 settings 标签
MyBatis 通过 <settings/>
标签提供一些全局性的配置,这些配置会影响 MyBatis 的运行行为。官方文档 对这些配置项进行了详细的说明,下面的配置摘自官方文档,其中各项的含义可以参考文档说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <settings > <setting name ="cacheEnabled" value ="true" /> <setting name ="lazyLoadingEnabled" value ="true" /> <setting name ="multipleResultSetsEnabled" value ="true" /> <setting name ="useColumnLabel" value ="true" /> <setting name ="useGeneratedKeys" value ="false" /> <setting name ="autoMappingBehavior" value ="PARTIAL" /> <setting name ="autoMappingUnknownColumnBehavior" value ="WARNING" /> <setting name ="defaultExecutorType" value ="SIMPLE" /> <setting name ="defaultStatementTimeout" value ="25" /> <setting name ="defaultFetchSize" value ="100" /> <setting name ="safeRowBoundsEnabled" value ="false" /> <setting name ="mapUnderscoreToCamelCase" value ="false" /> <setting name ="localCacheScope" value ="SESSION" /> <setting name ="jdbcTypeForNull" value ="OTHER" /> <setting name ="lazyLoadTriggerMethods" value ="equals,clone,hashCode,toString" /> </settings >
MyBatis 对于该标签的解析实现十分简单,首先调用 XMLConfigBuilder#settingsAsProperties
方法获取配置项对应的 Properties 对象,同时会检查配置项是否是可识别的,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private Properties settingsAsProperties (XNode context) { if (context == null ) { return new Properties(); } Properties props = context.getChildrenAsProperties(); 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; }
接下来调用 XMLConfigBuilder#loadCustomVfs
方法和 XMLConfigBuilder#loadCustomLogImpl
方法分别解析 vfsImpl
和 logImpl
配置项,其中 vfsImpl
配置项用于设置自定义 VFS 的实现类全限定名,以逗号分隔。所有的 <settings/>
配置项最后都会通过 XMLConfigBuilder#settingsElement
方法记录到 Configuration 对象对应的属性中。
解析 typeAliases 和 typeHandlers 标签
前面介绍了 TypeAliasRegistry 和 TypeHandlerRegistry 两个类的功能和实现,本小节介绍的这两个标签分别对应这两个类的相关配置,前者用于配置类型及其别名的映射关系,后者用于配置类型及其类型处理器 TypeHandler 之间的映射关系。二者在实现上基本相同,这里以 <typeAliases/>
标签的解析过程为例进行分析(由 XMLConfigBuilder#typeAliasesElement
方法实现),有兴趣的读者可以自己阅读 <typeHandlers/>
标签的相关实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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); } } } } }
标签 <typeAliases/>
具备两种配置方式,单一注册与批量扫描,具体使用可以参考 官方文档 。对应的实现也需要区分这两种情况,如果是批量扫描,即子标签是 <package/>
,则会调用 TypeAliasRegistry#registerAliases
方法进行扫描注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void registerAliases (String packageName) { this .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) { if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { this .registerAlias(type); } } }
如果子标签是 <typeAlias alias="" type=""/>
这种配置形式,则会获取 alias 和 type 属性值,然后基于一定规则进行注册,具体过程如代码注释。
解析 objectFactory 标签
在具体分析 <objectFactory/>
标签的解析实现之前,我们必须先了解与之密切相关的 ObjectFactory 接口。由名字我们可以猜测这是一个工厂类,并且是创建对象的工厂,定义如下:
1 2 3 4 5 6 7 8 9 10 public interface ObjectFactory { default void setProperties (Properties properties) { } <T> T create (Class<T> type) ; <T> T create (Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) ; <T> boolean isCollection (Class<T> type) ; }
各方法的作用如代码注释,DefaultObjectFactory 类是该接口的默认实现。下面重点看一下基于指定构造参数(类型)选择对应的构造方法创建目标对象的实现细节:
1 2 3 4 5 6 public <T> T create (Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { Class<?> classToCreate = this .resolveInterface(type); return (T) this .instantiateClass(classToCreate, constructorArgTypes, constructorArgs); }
方法首先会判断当前指定的类型是否是接口类型,因为接口类型无法实例化,所以需要选择相应的实现类代替。例如当我们传递的是一个 List 接口类型会返回相应的 ArrayList 实现类型。再来看一下 DefaultObjectFactory#instantiateClass
方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private <T> T instantiateClass (Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { try { Constructor<T> constructor; if (constructorArgTypes == null || constructorArgs == null ) { constructor = type.getDeclaredConstructor(); try { return constructor.newInstance(); } catch (IllegalAccessException e) { if (Reflector.canControlMemberAccessible()) { constructor.setAccessible(true ); return constructor.newInstance(); } else { throw e; } } } constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0 ])); try { return constructor.newInstance(constructorArgs.toArray(new Object[0 ])); } catch (IllegalAccessException e) { if (Reflector.canControlMemberAccessible()) { constructor.setAccessible(true ); return constructor.newInstance(constructorArgs.toArray(new Object[0 ])); } else { throw e; } } } catch (Exception e) { } }
上述方法主要基于传递的参数以决策具体创建对象的构造方法版本,并基于反射机制创建对象。
所以说 ObjectFactory 接口的作用主要是对我们传递的类型进行实例化,默认的实现版本比较简单。如果默认实现不能满足需求,则可以扩展 ObjectFactory 接口,并将相应的自定义实现通过 <objectFactory/>
标签进行注册,具体的使用方式参见 官方文档 。我们继续分析针对该标签的解析过程,由 XMLConfigBuilder#objectFactoryElement
方法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void objectFactoryElement (XNode context) throws Exception { if (context != null ) { String type = context.getStringAttribute("type" ); Properties properties = context.getChildrenAsProperties(); ObjectFactory factory = (ObjectFactory) this .resolveClass(type).getDeclaredConstructor().newInstance(); factory.setProperties(properties); configuration.setObjectFactory(factory); } }
解析 <objectFactory/>
标签的基本流程就是获取我们在标签中通过 type 属性指定的自定义 ObjectFactory 实现类的全限定名和相应属性配置;然后构造自定义 ObjectFactory 实现类对象,并将获取到的配置项列表记录到对象中;最后将自定义 ObjectFactory 对象填充到 Configuration 对象中。
解析 reflectorFactory 标签
标签 <reflectorFactory/>
用于注册自定义 ReflectorFactory 实现,该标签的解析过程与 <objectFactory/>
标签基本相同,不再重复撰述,本小节重点分析一下该标签涉及到相关类的功能与实现。
ReflectorFactory 顾名思义是一个 Reflector 工厂,接口定义如下:
1 2 3 4 5 6 7 8 public interface ReflectorFactory { boolean isClassCacheEnabled () ; void setClassCacheEnabled (boolean classCacheEnabled) ; Reflector findForClass (Class<?> type) ; }
默认实现类 DefaultReflectorFactory 通过一个 boolean 变量 DefaultReflectorFactory#classCacheEnabled
记录是否启用缓存,并通过一个线程安全的 Map 集合 DefaultReflectorFactory#reflectorMap
记录缓存的 Reflector 对象,相应的方法实现都十分简单,不再展开。
ReflectorFactory 本质上是用来创建和管理 Reflector 对象,那么 Reflector 又是什么呢?我们先来看一下 Reflector 的属性和构造方法定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class Reflector { private final Class<?> type; private final String[] readablePropertyNames; private final String[] writablePropertyNames; private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>(); private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>(); private final Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>(); private final Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>(); private Constructor<?> defaultConstructor; private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>(); public Reflector (Class<?> clazz) { type = clazz; this .addDefaultConstructor(clazz); this .addGetMethods(clazz); this .addSetMethods(clazz); this .addFields(clazz); readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]); writablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]); for (String propName : readablePropertyNames) { caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); } for (String propName : writablePropertyNames) { caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); } } }
可以看到 Reflector 是对指定 Class 对象的封装,记录了对应的 Class 类型、属性、getter 和 setter 方法列表等信息,是反射操作的基础,其中的方法实现虽然较长,但是逻辑都比较简单,读者可以自行阅读源码。
解析 objectWrapperFactory 标签
标签 <objectWrapperFactory/>
用于注册自定义 ObjectWrapperFactory 实现,该标签的解析过程与 <objectFactory/>
标签基本相同,同样不再重复撰述,本小节重点分析该标签涉及到相关类的功能与实现。
ObjectWrapperFactory 顾名思义是一个 ObjectWrapper 工厂,其默认实现 DefaultObjectWrapperFactory 并没有实现有用的逻辑,所以可以忽略。然而,借助 <reflectorFactory/>
标签,我们可以注册自定义的 ObjectWrapperFactory 实现。
被 ObjectWrapperFactory 创建和管理的 ObjectWrapper 是一个接口,用于包装和处理对象,其中声明了多个操作对象的方法,包括获取、更新对象属性等,接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 的主要作用在于简化调用方对于对象的操作。
解析 environments 标签
标签 <environments/>
用于配置多套数据库环境,典型的应用场景就是在开发、测试、灰度,以及生产等环境通过该标签分别指定相应的配置。当应用需要同时操作多套数据源时,也可以基于该标签分别配置,具体的使用请参阅 官方文档 。MyBatis 解析该标签的过程由 XMLConfigBuilder#environmentsElement
方法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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" ); if (this .isSpecifiedEnvironment(id)) { TransactionFactory txFactory = this .transactionManagerElement(child.evalNode("transactionManager" )); DataSourceFactory dsFactory = this .dataSourceElement(child.evalNode("dataSource" )); DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } }
上述方法首先会判断是否通过参数指定了 environment 配置,如果没有则尝试获取 <environments/>
标签的 default 属性,说明参数指定相对于 default 属性配置优先级更高。然后开始遍历寻找并解析指定激活的 <environment/>
配置。整个解析过程主要是对 <transactionManager/>
和 <dataSource/>
两个子标签进行解析,前者用于指定 MyBatis 的事务管理器,后者用于配置数据源。
数据源的配置解析比较直观,下面主要看一下事务管理器配置的解析过程,由 XMLConfigBuilder#transactionManagerElement
方法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 private TransactionFactory transactionManagerElement (XNode context) throws Exception { if (context != null ) { String type = context.getStringAttribute("type" ); Properties props = context.getChildrenAsProperties(); TransactionFactory factory = (TransactionFactory) this .resolveClass(type).getDeclaredConstructor().newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a TransactionFactory." ); }
MyBatis 允许我们配置两种类型的事务管理器,即 JDBC 类型和 MANAGED 类型,引用官方文档的话来理解二者的区别:
在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"
):
JDBC:这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
MANAGED:这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。例如:
1 2 3 <transactionManager type ="MANAGED" > <property name ="closeConnection" value ="false" /> </transactionManager >
提示:如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。
Transaction 接口定义了事务,并为 JDBC 类型和 MANAGED 类型提供了相应的实现,即 JdbcTransaction 和 ManagedTransaction。正如上面引用的官方文档所说的那样,MyBatis 的事务操作实现的比较简单,考虑实际应用中更多是依赖于 Spring 的事务管理器,这里也就不再深究。
解析 databaseIdProvider 标签
生产环境中可能会存在同时操作多套不同类型数据库的场景,而 <databaseIdProvider/>
标签则用于配置数据库厂商标识。我们知道 SQL 不能完全做到数据库无关,且 MyBatis 暂时也还不能做到对上层完全屏蔽底层数据库的实现细节,所以在这种情况下执行 SQL 语句时,我们需要通过 databaseId 指定 SQL 应用的具体数据库类型。
该标签的解析过程由 XMLConfigBuilder#databaseIdProviderElement
方法实现,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private void databaseIdProviderElement (XNode context) throws Exception { DatabaseIdProvider databaseIdProvider = null ; if (context != null ) { String type = context.getStringAttribute("type" ); if ("VENDOR" .equals(type)) { type = "DB_VENDOR" ; } Properties properties = context.getChildrenAsProperties(); databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); databaseIdProvider.setProperties(properties); } Environment environment = configuration.getEnvironment(); if (environment != null && databaseIdProvider != null ) { String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); configuration.setDatabaseId(databaseId); } }
解析过程如上述代码注释,关于该标签的使用可以参考 官方文档 。
解析 mappers 标签
标签 <mappers/>
用于指定映射文件列表,这是一个我们非常熟悉的标签。MyBatis 广受欢迎的一个很重要的原因是支持自己定义 SQL 语句,这样就可以保证 SQL 的优化可控。抛去注解配置 SQL 的形式(注解对于复杂 SQL 的支持较弱,一般仅用于编写简单的 SQL),对于框架自动生成的 SQL 和用户自定义的 SQL 都记录在映射 XML 文件中,标签 <mappers/>
用于指定映射文件所在的路径。
我们可以通过 <mapper resource="">
或 <mapper url="">
子标签指定映射 XML 文件所在的位置,也可以通过 <mapper class="">
子标签指定一个或多个具体的 Mapper 接口,甚至可以通过 <package name=""/>
子标签指定映射文件所在的包名,扫描注册。
该标签的解析过程由 XMLConfigBuilder#mapperElement
方法实现,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 private void mapperElement (XNode parent) throws Exception { if (parent != null ) { for (XNode child : parent.getChildren()) { 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." ); } } } } }
上述方法首先会判断当前是否是 package 配置,如果是则会获取配置的 package 名称,然后执行扫描注册逻辑。如果是 resource 或 url 配置,则先获取指定路径映射文件的输入流,然后构造 XMLMapperBuilder 对象对映射文件进行解析。对于 class 配置而言,则会构建接口限定名对应的 Class 对象,并调用 MapperRegistry#addMapper
方法执行注册。
整个方法的运行逻辑还是比较直观的,其中涉及到对映射文件的解析注册过程,即 XMLMapperBuilder 相关类实现,将留到下一篇介绍映射文件加载与解析时专门介绍。
下面来重点分析一下 MapperRegistry 类及其周边类的功能和实现。我们在使用 MyBatis 框架时需要实现数据表对应的 Mapper 接口(以后统称为 Mapper 接口),其中声明了一系列数据库操作方法。我们可以通过注解的方式在方法上编写 SQL 语句,也可以通过映射 XML 文件的方式编写和关联对应的 SQL 语句。上面解析 <mappers/>
标签实现时我们看到方法通过调用 MapperRegistry#addMapper
方法注册相应的 Mapper 接口,包括以 package 配置的方式在扫描获取到相应的 Mapper 接口之后,也需要通过调用该方法进行注册。MapperRegistry 类中定义了两个属性:
1 2 3 4 private final Configuration config;private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
上面调用的 MapperRegistry#addMapper
方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public <T> void addMapper (Class<T> type) { if (type.isInterface()) { if (this .hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry." ); } boolean loadCompleted = false ; try { knownMappers.put(type, new MapperProxyFactory<>(type)); MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true ; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
Mapper 方法必须是一个接口才会被注册,这主要是为了配合 JDK 内置的动态代理机制。上一篇介绍 MyBatis 的基本运行原理时我们曾说过,MyBatis 通过为 Mapper 接口创建相应的动态代理类以执行具体的数据库操作,这一部分的详细过程将留到后面介绍 SQL 语句执行机制时再细讲,这里先知道有这样一个概念即可。如果当前 Mapper 接口还没有被注册,则会创建对应的 MapperProxyFactory 对象并记录到 MapperRegistry#knownMappers
属性中,然后解析 Mapper 接口中注解的 SQL 配置,这一过程留到下一篇分析映射文件解析过程时再一并介绍。
总结
到此,我们完成了对配置文件 mybatis-config.xml
加载和解析过程的分析。总的来说,对于配置文件的解析实际上就是将静态的 XML 配置解析成内存中的 Configuration 对象的过程。Configuration 可以看作是 MyBatis 全局的配置中心,后续对于映射文件的解析,以及 SQL 语句的执行都依赖于其中的配置项。下一篇,我们将一起来探究映射文件的加载和解析过程。
参考
MyBatis 官方文档
MyBatis 技术内幕