# mybatis-3 **Repository Path**: topanda/mybatis-3 ## Basic Information - **Project Name**: mybatis-3 - **Description**: mybatis源码中文翻译,并包含学习时新增的单元测试 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 1 - **Created**: 2019-01-14 - **Last Updated**: 2024-05-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 菜鸟进阶记之MyBatis源码解析 ## 找到学习Mybatis源码的切入点 如果我们采用硬编码的形式简单的使用Mybatis,代码大概会类似下面这种结构: ``` // 定义Mybatis 主要配置文件的地址 String resource = "resources/config-custom-s1.xml"; // 获取Mybatis配置文件的输入流 InputStream inputStream = Test.class.getClassLoader().getResourceAsStream(resource); // 通过输入流构建SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 通过SqlSessionFactory获取一个SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 执行操作 // userDao 的方法将会被代理 UserDao userDao = sqlSession.getMapper(UserDao.class); User teacher = userDao.getById(1); System.out.println(userDao.insert(2, "panda")); sqlSession.rollback(); // 关闭会话 sqlSession.close(); ``` 我们首先通过Mybatis的主要配置文件来初始化一个SqlSessionFactory的实例,之后通过SqlSessionFactory来获取一个SqlSession,然后在这个SqlSession中获取我们想要执行Dao操作的接口实例,进而执行DAO操作。 我们先忽略具体实现细节,先来了解这三个主要的类: - SqlSessionFactoryBuilder - SqlSessionFactory - SqlSession 在我们常用的23种设计模式中,有一种设计模式叫做建造者模式,他通常用于构建复杂对象,在这里SqlSessionFactoryBuilder就是建造者模式的一种简单实现,他的作用是创建一个SqlSessionFactory对象实例。 事实上在Mybatis中,SqlSessionFactory的初始化是一个很复杂的过程,为了简化理解,我们暂且忽略掉其具体的初始化过程,仅需记住SqlSessionFactoryBuilder就是用来简化我们构造SqlSessionFactory对象复杂度一个工具类就可以了。 我们可以很容易的通过SqlSessionFactory这个名称,来简单的断定其是一个用于构造SqlSession的工厂类,那么说到工厂类就要提到设计模式中的工厂模式,简单的说工厂模式可以让我们使用工厂方法来代替常规new方法,其实也可以间接的简化一些构造过程,当然这里对设计模式只是稍微提及,并不会进行太深入的探讨,毕竟,学习Mybatis的源码才是我们的主要任务。 ## SqlSession SqlSessionFactory用来创建SqlSession对象,这个SqlSession对象就是我们需要了解的第一个真正的对象了。 我们都知道,如果我们要进行数据库操作,首先是要获取数据库链接,之后才能对数据库进行操作。 在Mybatis框架中,针对JDBC的操作进行了封装,统一了JDBC操作的APi,我们可以将这个API视为Mybatis框架中的Connection对象,这个API的具体表现形式就是SqlSession对象。 不过,区别于`Connection`,SqlSession对象除了封装了JDBC操作之外,还提供了其他好玩的东西,比如上文中调用的`SqlSession#getMapper()`方法,这个方法就很有意思,你传给他一个Dao操作接口的类型,他还给你一个对应的实体对象,之后你调用获取到的对象的方法的时候,就自带了JDBC效果。 那么这个看起来灰常六的功能是如何实现的呢?这就涉及到了Mybatis的核心原理,不过简单来讲,其实现依托了设计模式中的代理模式,代理模式也是常用的设计模式之一,它能够取代目标对象,代替目标对象执行操作流程,上文中自带的JDBC效果对于定义的DAO操作结果本质上不存在,但是因为代理对象的存在他假装成了一个DAO操作接口的实例,并完成了具有JDBC属性的操作。 但是Mybatis是在什么时候对Dao操作接口进行代理的呢?带着这个疑问,我们回到`SqlSessionFactoryBuilder`对象中,且看且分析。 ## SqlSessionFactoryBuilder `SqlSessionFactoryBuilder`看起来是一个很简单的接口,它定义了一个`build`方法,并为其提供了多种重载. ``` SqlSessionFactory build(Reader reader); SqlSessionFactory build(Reader reader, String environment); SqlSessionFactory build(Reader reader, Properties properties); SqlSessionFactory build(Reader reader, String environment, Properties properties); SqlSessionFactory build(InputStream inputStream); SqlSessionFactory build(InputStream inputStream, String environment); SqlSessionFactory build(InputStream inputStream, Properties properties); SqlSessionFactory build(InputStream inputStream, String environment, Properties properties); SqlSessionFactory build(Configuration config); ``` 在看源码是我们往往要看的是其参数最复杂的一个方法,因为通常情况下方法的调用会落在这个最复杂的方法上。 SqlSessionFactory的重载方法最复杂的有两个。 - 一个是`SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);` - 另一个是`SqlSessionFactory build(Reader reader, String environment, Properties properties);` 这两个方法其实很像,区别只是在于文件流的具体类型一个是Reader,一个是InputStream。 其余并无太大区别,所以这里我们只看InputStream对应的build方法,顺带一提的是,其实我们可以通过`Reader reader=new InputStreamReader(inputStream);`来将Inputstream转换为reader。 ``` /** * 创建一个SqlSessionFactory * * @param inputStream Mybatis配置文件对应的字节流 * @param environment 默认的环境ID,具体的environment的配置可以参考Mybatis配置文件内的Environments节点,默认为default * @param properties 指定的配置 */ public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 构建一个Mybatis 主要配置文件的解析器 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 使用默认的JDBC会话工厂 return build( parser.parse()/*解析Mybatis配置*/ ); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } ``` 在build方法中,获取了一个XmlConfigBuilder的对象,这个对象实质上是抽象类`BaseBuilder`的一个具体实现,主要用来解析Mybatis的主要配置文件。 在定义上看`BaseBuilder`定义的目的应该是为了统一解析构建Mybatis组件的入口,但是其在具体实现中并没有遵循这一设计,而且个人不是很喜欢`*Builder`这个命名,因为这个命名听起来更像是用于构造某一复杂对象的,但是实际上他虽然负责构造了一些复杂对象,但是本质上更多的是解析XML的配置,所以个人还是比较喜欢`*Parser`这种命名方式,仅仅用来解析,具体的构建操作委托给`*Register`来实现,当然这只是个人喜好而已。 继续回到代码上来,当我们调用`XMLConfigBuilder#parse()`方法的时候,他会返回一个`Configuration`对象,并将`Configuration`对象传给方法`SqlSessionFactory build(Configuration config)`继续处理,好吧,打脸了,其最终实现竟然是调用了一个单参数方法。 ``` /** * 创建一个SqlSessionFactory * * @param config 指定的Mybatis配置类 */ public SqlSessionFactory build(Configuration config) { // 构建默认的SqlSessionFactory实例 return new DefaultSqlSessionFactory(config); } ``` 最终调用的这个方法很简单,只是通过`Configuration`构建了一个`DefaultSqlSessionFactory`实例而已。 我们先忽略`SqlSessionFactory build(Configuration config)`方法,将目光重新投向`XmlConfigBuilder`: ``` // 构建一个Mybatis 主要配置文件的解析器 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 使用默认的JDBC会话工厂 return build( parser.parse()/*解析Mybatis配置*/ ); ``` 这里调用了`XmlConfigBuilder`的`XMLConfigBuilder(InputStream inputStream, String environment, Properties props)`构造器来初始化一个`XmlConfigBuilder`实例对象,接着使用了`#parse`方法生成了一个`Configuration`对象用于配置Mybatis的`DefaultSqlSessionFactory`实例。 显而易见在`XmlConfigBuilder#parse()`中执行了一些不为我们所知的方法,它用来解析Mybatis的XMl配置文件的同时生成了一个Mybatis Configuration对象。 --- 我们跟踪代码进入`XMLConfigBuilder# XMLConfigBuilder(InputStream inputStream, String environment, Properties props)`构造器中: ``` public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { this(new XPathParser( inputStream,/*XML文件输入流*/ true, /*开启验证*/ props, /*Property参*/ new XMLMapperEntityResolver() /*XML实体解析器*/ ) /*新建一个XML解析器的实例*/ , environment/*环境对象*/ , props /*Property参数*/ ); } ``` 在这里,Mybatis依据XML文件的输入流构建了一个`XpathParser`对象,`XpathParser`的作用很简单,它使用`SAX`解析出输入流对应的XML的DOM树,并存放到`XpathParser`对象中,部分`XpathParser`的代码如下: ``` public class XPathParser { /** * XML文本内容 */ private final Document document; /** * 是否验证DTD */ private boolean validation; /** * 实体解析器 */ private EntityResolver entityResolver; /** * 属性集合 */ private Properties variables; /** * XML地址访问器 */ private XPath xpath; } ``` 我们刚才调用的构造器如下: ``` public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) { commonConstructor(validation, variables, entityResolver); this.document = createDocument(new InputSource(reader)); } ``` 在这个构造器中有两个部分,一个是调用`commonConstructor`初始化当前的属性,另一个是调用`createDocument`来解析XMl文件对应的DOM树. 其中`commonConstructor`相对比较简单,代码如下: ``` /** * 公共构造参数,主要是用来配置DocumentBuilderFactory * * @param validation 是否进行DTD校验 * @param variables 属性配置 * @param entityResolver 实体解析器 */ private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) { // 是否进行DTD校验 this.validation = validation; // 配置XML实体解析器 this.entityResolver = entityResolver; // Mybatis Properties this.variables = variables; // 初始化XPath XPathFactory factory = XPathFactory.newInstance(); this.xpath = factory.newXPath(); } ``` `createDocument`方法则主要是根据当前的配置来进行解析Xml文件获取DOM对象: ``` /** * 根据输入源,创建一个文本对象 * * @param inputSource 输入源 * @return 文本对象 */ private Document createDocument(InputSource inputSource) { // important: this must only be called AFTER common constructor try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // 是否验证被解析的文档 factory.setValidating(validation); // 是否提供对XML命名空间的支持 factory.setNamespaceAware(false); //忽略注释 factory.setIgnoringComments(true); //忽略空白符 factory.setIgnoringElementContentWhitespace(false); //是否解析CDATA节点转换为TEXT节点 factory.setCoalescing(false); //是否展开实体引用节点 factory.setExpandEntityReferences(true); DocumentBuilder builder = factory.newDocumentBuilder(); // 配置XML文档的解析器,现在主要是XMLMapperEntityResolver builder.setEntityResolver(entityResolver); // 配置错误处理器 builder.setErrorHandler(new ErrorHandler() { @Override public void error(SAXParseException exception) throws SAXException { throw exception; } @Override public void fatalError(SAXParseException exception) throws SAXException { throw exception; } @Override public void warning(SAXParseException exception) throws SAXException { } }); // 解析出DOM树 return builder.parse(inputSource); } catch (Exception e) { throw new BuilderException("Error creating document instance. Cause: " + e, e); } } ``` 在执行完这两个方法之后,`XPathParse`的构造过程就已经结束了,此时的他不仅持有`Document`对象还持有一个`Xpath`访问器,接下来很多操作都会使用`Xpath`来读取`Document`对象的内容。 我们继续回到`XMLConfigBuilder`的构造方法中,我们之前说过`XMLConfigBuilder`对象是`BaseBuilder`的子类,在`BaseBuilder`里有三个`final`修饰的属性: ``` public abstract class BaseBuilder { /** * Mybatis配置 */ protected final Configuration configuration; /** * 类型别名注册表 */ protected final TypeAliasRegistry typeAliasRegistry; /** * 类型转换处理器注册表 */ protected final TypeHandlerRegistry typeHandlerRegistry; ``` - 其中`Configuration`对象是Mybaits的配置类,他里面封装了很多配置属性便于Mybatis在之后的处理过程中使用,我们暂且不提,等待具体使用时再去介绍。 - `TypeAliasRegistry`是Mybatis的类型别名注册表,其目的在于简化我们在使用Mybatis过程中的编码量,比如在我们在定义resultMap的result时,可以使用``,而不必非得使用类的全限定名称`java.lang.Integer`. `TypeAliasRegistry`本身的实现是很简单的,他维护了一个叫做`Map> TYPE_ALIASES`的MAP集合,用来存储类别名和类定义的映射关系,并在其构造方法中默认注册了我们常用的一些类的别名。 - `TypeHandlerRegistry`属性维护了Mybatis的类型转换处理器注册表,类型转换处理器(`TypeHandler`)的作用是处理java类型和jdbc类型之间的互相转换工作,同样他也在构造时初始化注册了一些常用的类型转换器. `XMLConfigBuilder`本身也定义了一些属性,其作用大致如下: ``` /*解析标志,防止重复解析*/ private boolean parsed; /*XML地址解析器*/ private final XPathParser parser; /*指定Mybatis当前运行使用的环境*/ private String environment; ``` 这里`environment`属性的作用,在之后的解析过程中,我们会讲到。 回到刚才`XmlConfigBuilder`的构造器中,在完成初始化一个`XPathParse`对象之后, 会继续调用其重载的构造方法`XMLConfigBuilder(XPathParser parser, String environment, Properties props) `: ``` private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { // 初始化Configuration对象的同时注册部分别名以及语言驱动 super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); // 设置Mybatis变量 this.configuration.setVariables(props); // 初始化解析标签 this.parsed = false; // 初始化环境容器 this.environment = environment; // 初始化Xml地址解析器 this.parser = parser; } ``` 需要注意的是,在这个构造器中,调用父类的构造器: ``` public BaseBuilder(Configuration configuration) { this.configuration = configuration; // 从Mybatis配置中同步过来类型别名注册表 this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); // 从Mybatis配置中同步过来类型处理器注册表 this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); } ``` 通过代码我们可以看到`BaseBuilder#typeAliasRegistry`和`BaseBuilder#typeHandlerRegistry`都是引用的Mybatis `Configuration`中的属性,因为这里敲黑板!!! > BaseBuilder#typeAliasRegistry和Configuration#typeAliasRegistry是同一个实例。 > BastBuilder#typeHandlerRegistry和Configuration#typeHandlerRegistry是同一个实例。 至此,整个`XMLConfigBuilder`的初始化过程也已经全部完成。 接下来,Mybvatis在`SqlSessionFactoryBuilder#build()`>`return build(parser.parse()/*解析Mybatis配置*/);`位置调用了`XmlConfigBuidler#parse()`方法触发了整个Mybatis的初始化解析过程。 ``` public Configuration parse() { if (parsed) { // 第二次调用XMLConfigBuilder throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 重置XMLConfigBuilder的解析标志,防止重复解析 parsed = true; // 此处开始进行Mybatis配置文件的解析流程 // 解析 configuration 配置文件,读取【configuration】节点下的内容 parseConfiguration(parser.evalNode("/configuration")); // 返回Mybatis的配置实例 return configuration; } ``` 首先调用了方法` #parseConfiguration()`来解析Mybatis 主配置文件的 `configuration`节点,而`configuration`节点的数据获取使用了`parser.evalNode("/configuration")`, 该方法最终落点于方: ``` private Object evaluate(String expression, Object root, QName returnType) { try { // 在指定的上下文中计算XPath表达式,并将结果作为指定的类型返回。 return xpath.evaluate(expression, root, returnType); } catch (Exception e) { throw new BuilderException("Error evaluating XPath. Cause: " + e, e); } } ``` 之后Mybatis将该对象通过方法`return new XNode(this, node, variables);`包装成一个XNode对象. XNode节点的主要定义如下: ``` public class XNode { /** * DOM 节点 */ private final Node node; /** * 当前节点的名称 */ private final String name; /** * 节点内容 */ private final String body; /** * 节点属性 */ private final Properties attributes; /** * Mybatis对应的配置 */ private final Properties variables; /** * XPathParser解析器 */ private final XPathParser xpathParser; } ``` 其主要作用就是简化访问DOM资源的操作。 在将`configuration`节点的内容包装为XNode对象之后,就开始了真正的解析过程. ## 解析Mybatis的主配置文件,并配置Mybatis允许所需要的基础环境 `configuration`是Mybaits主配置文件的根节点,我们通常这样使用: ``` ... ``` 参考Mybatis的DTD定义文件,可以发现在`configuration`节点下允许出现11种类型的子节点,且都是可选的,这也意味着Mybatis针对这些属性都有默认的处理: ``` ``` 这些子节点的具体用途我们在接下来的解析过程中会一一说到。 还是继续回到`parseConfiguration`方法上: ``` /** * 解析Configuration节点 */ private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 加载资源配置文件,并覆盖对应的属性[properties节点] propertiesElement(root.evalNode("properties")); // 将settings标签内的内容转换为Property,并校验。 Properties settings = settingsAsProperties(root.evalNode("settings")); // 根据settings的配置确定访问资源文件的方式 loadCustomVfs(settings); // 根据settings的配置确定日志处理的方式 loadCustomLogImpl(settings); // 别名解析 typeAliasesElement(root.evalNode("typeAliases")); // 插件配置 pluginElement(root.evalNode("plugins")); // 配置对象创建工厂 objectFactoryElement(root.evalNode("objectFactory")); // 配置对象包装工厂 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 配置反射工厂 reflectorFactoryElement(root.evalNode("reflectorFactory")); // 通过settings配置初始化全局配置 settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 // 加载多环境源配置,寻找当前环境(默认为default)对应的事务管理器和数据源 environmentsElement(root.evalNode("environments")); // 数据库类型标志创建类,Mybatis会加载不带databaseId以及当前数据库的databaseId属性的所有语句,有databaseId的 // 语句优先级大于没有databaseId的语句 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 注册类型转换器 typeHandlerElement(root.evalNode("typeHandlers")); // !!注册解析Dao对应的MapperXml文件 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } ``` ### 解析配置Mybatis的属性资源(properties节点) 在解析的过程中,首先是加载`configuration -> properties`节点,并覆盖Mybatis Configuration中的variables属性。 properties属性的定义很简单: ``` ``` 它允许有多个`property`子项,同时也可以配置 `resource` 和`url` 属性,但是`resource`和`url`属性不能同时存在, 其中`property`子项的定义如下: ``` ``` `property`是由`name`和`value`构成的,因此其可以被解析成Properties。 在具体的解析过程中,我们也可以看到Mybatis针对这三种不同的配置方式都给出了对应的解析操作。 ``` /** * 解析Properties标签 * * @param context Properties节点 * @throws Exception 异常 */ private void propertiesElement(XNode context) throws Exception { if (context != null) { // 获取Properties下得Property节点的name 和value值,并转换Properties属性 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) { // 加载*.Properties资源配置文件 defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { // 加载*.Properties资源配置文件 defaults.putAll(Resources.getUrlAsProperties(url)); } Properties vars = configuration.getVariables(); if (vars != null) { // 加载配置内容 defaults.putAll(vars); } // 刷新配置内容,并将全局默认配置同步到解析器内,用于接下来的解析 parser.setVariables(defaults); // 同步到Mybatis设置中 configuration.setVariables(defaults); } } ``` Mybatis首先获取`propeties`下的所有`proerty`子节点,并据此生成一个`Properties`实例,其中`context.getChildrenAsProperties();`的实现代码如下: ``` /** * 获取settings 节点下的所有setting,setting>name作为Properties的key, * setting>value 作为properties的value。 */ public Properties getChildrenAsProperties() { Properties properties = new Properties(); for (XNode child : getChildren()) { String name = child.getStringAttribute("name"); String value = child.getStringAttribute("value"); if (name != null && value != null) { properties.setProperty(name, value); } } return properties; ``` 之后会分别获取`propertie`s的`resource`和`url`两个属性,校验两者不得同时存在,之后根据`resource`/`url`加载对应的资源文件 并保存到生成的Properties对象中,之后会获取Mybatis 配置类(Configuration)中的所有变量,然后保存到Properties对象中, 最后使用聚合出的`Properties`对象刷新`XmlPathparser`中的Mybatis配置,然后刷新Mybatis 配置类(Configuration)的属性配置。 通过上诉的加载顺序我们不难理解针对Mybatis的属性配置,优先级由高到低依次为: 启动时设置的参数->Mybatis 主配置文件中properties的url/resource属性指向的配置 -> Mybatis 主配置文件中properties的property配置。 ### 解析配置Mybatis的环境设置(settings节点) 在解析完`propeties`节点之后,继续解析`settings`节点。 ``` // 将settings标签内的内容转换为Property,并校验。 Properties settings = settingsAsProperties(root.evalNode("settings")); ``` `settings`节点的定义如下: ``` ``` 他规定了在`settings`节点下必须有一个或者多个`setting`子节点,`setting`子节点必须有`name`和`value`两个参数 ,且`setting`下不能继续有其他子节点。 从这个DTD上也不难看出,其实`settings`节点也会被转换为`Properties`对象。 解析`settings`的代码: ``` /** * 解析settings节点 * * @param context settings节点 */ private Properties settingsAsProperties(XNode context) { if (context == null) { // 如果没有配置settings节点,返回个空配置 return new Properties(); } Properties props = context.getChildrenAsProperties(); // 检查Settings节点下的配置是否被支持 // Check that all settings are known to the configuration class // 获取Configuration的反射缓存类 MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); for (Object key : props.keySet()) { if (!metaConfig.hasSetter(String.valueOf(key))) { // 校验setting 配置的属性是否支持 throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); } } return props; } ``` 其实在解析`settings`的过程中做的事情比较少,第一步就是将整个`settings`节点转换为一个`Properties`对象,如果没有配置 `settings`节点,那么就返回一个空的properties对象,如果有的话,就检查现有的`settings`配置的属性是否全部被支持。 这里面就涉及到了一个新的对象,叫做`MetaClass`,`MetaClass`是Mybatis用于保存对象的类型的元数据定义的一个类。 它主要有两个属性,一个是反射工厂,一个是对象属性描述元数据类。 ``` public class MetaClass { /** * 反射工厂 */ private final ReflectorFactory reflectorFactory; /** * 指定类的元数据 */ private final Reflector reflector; } ``` 其中`ReflectorFactory`用于读取`class`并生成`Reflector`的辅助性工厂类,`Reflector`则缓存了一个类定义的基本信息,包括 类的类型,可读可写属性名称,以及对应的getter/setter方法,构造函数等。 在这里`XmlConfigBuilder`使用`DefaultReflectorFactory`来获取了`Configuration`的基本属性,并判断`settings`配置的属性 在`Configuration`中是否有对应的setter方法。 至此,整个`settings`的解析已经完成。 接下来的操作,则是根据`settings`的配置,确定Mybatis访问jar内部资源的方式,在Mybatis中定义了一个`VFS`抽象类,这个 抽象类定义了访问容器资源的API,其默认有两个实现`JBoss6VFS.class, DefaultVFS.class`。 ``` // 根据settings的配置确定访问资源文件的方式 loadCustomVfs(settings); ``` ``` /** * 加载访问系统虚拟文件系统的实现类 * * @param props 系统全局配置 * @throws ClassNotFoundException 未找到实现类 */ private void loadCustomVfs(Properties props) throws ClassNotFoundException { // 获取配置的vfsImpl属性,如果存在覆盖默认的虚拟文件系统访问实现类 String value = props.getProperty("vfsImpl"); if (value != null) { String[] clazzes = value.split(","); for (String clazz : clazzes) { if (!clazz.isEmpty()) { @SuppressWarnings("unchecked") Class vfsImpl = (Class) Resources.classForName(clazz); // 更新Mybatis访问虚拟文件系统的实现类 configuration.setVfsImpl(vfsImpl); } } } } ``` 在上述的代码中`configuration.setVfsImpl(vfsImpl)`并不是简单的设值而已,在方法中它调用了`VFS.addImplClass(this.vfsImpl);`: ``` /** * 新增一个VFS实例 * @param vfsImpl VFS实例 */ public void setVfsImpl(Class vfsImpl) { if (vfsImpl != null) { this.vfsImpl = vfsImpl; // 添加一个新的VFS实例 VFS.addImplClass(this.vfsImpl); } } ``` `VFS.addImplClass(this.vfsImpl);`的作用是往`VFS#USER_IMPLEMENTATIONS`中添加一个`VFS`实现类,在获取VFS实例时会优先读取。 在完成更新Mybatis访问虚拟文件系统的实现类之后,会继续根据`settings`来完成Mybatis日志实现类的配置工作。 ``` // 根据settings的配置确定日志处理的方式 loadCustomLogImpl(settings); ``` ``` /** * 加载自定义日志实现类 * * @param props 全局配置 */ private void loadCustomLogImpl(Properties props) { Class logImpl = resolveClass(props.getProperty("logImpl")); configuration.setLogImpl(logImpl); } ``` 到这里,整个`settings`的处理才算是暂时告一段落。 ### 解析使用Mybatis的别名机制(typeAliases节点) 接下来则是处理Mybatis的的类型别名注册的工作,关于类型别名,在文章前面提到过,它主要是用做简化我们使用Mybatis时的代码量的一个优化性操作。 在Mybatis中配置自己的别名,需要使用的元素是`typeAliases`,`typealiases`的定义如下: ``` ``` 在`typealiases`下允许有零个或多个`typeAlias`/`package`标签,且`typeAlias`和`package`均不允许再包含其他子元素。 其中: - `typeAlias`下有两个可填参数,其中`type`指向一个java类型的权限定名称,且必填,`alias`属性表示该java对象的别名,非必填,默认是使用java类的`#getSimpleName()`. - `package`下只有一个必填的参数`name`,该参数指向一个java包,包下的所有符合规则(默认是Object.class的子类)的类均会被注册。 ``` /** * 解析配置typeAliases节点 * * @param parent typeAliases节点 */ private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { // 根据 package 来批量解析别名,默认为实体类的SimpleName String typeAliasPackage = child.getStringAttribute("name"); // 注册别名映射关系到别名注册表 configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { // 处理typeAlias配置,并将typeAlias>alias和typeAlias>type作为别名和类型注册到别名注册表 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); } } } } } ``` Mybatis对于`typeAliases`的节点也是根据DTD定义的一样,分为了两种处理方式:一种是处理`package`子元素,另一种是处理`typeAlias`子元素。 其中针对`package`的处理,是首先获取`package`的`name`属性指向的基础包名,然后调用`configuration#getTypeAliasRegistry()#registerAliases()`方法来执行别名注册的操作。 > 这里有一点要注意,前文已经提到本质上在XmlConfigBuilder中的typeAliasRegistry和configuration中的typeAliasRegistry是同一实例,因此,此处调用`typeAliasRegistry#registerAliases()`和`configuration#getTypeAliasRegistry()#registerAliases()`对于最终结果来说并无实质区别。 然后看一下`TypeAliasRegistry#registerAliases(String)`方法,这个方法提供了批量注册类别名的功能。 ``` /** * 注册指定包下所有的java类 * * @param packageName 指定包 */ public void registerAliases(String packageName) { registerAliases(packageName, Object.class); } ``` 调用其重载方法,并传入限制的类型,此处默认传入的是`Object.class`,因此其想要注册别名的类是该包下`Object.class`的子类集合。 ``` /** * 注册指定包下指定类型及其子实现的别名映射关系 * * @param packageName 指定包名称 * @param superType 指定类型 */ public void registerAliases(String packageName, Class superType) { ResolverUtil> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); // 返回当前已经找到的类 Set>> 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); } } } ``` 在这个方法中,他通过`ResolverUtils#find();`来获取初步符合预期的类集合,之后排除掉匿名类、成员类和接口,对剩下的类执行别名注册的操作。 > `ResolverUtils#find()`方法用于递归扫描指定的包及其子包中的类,并对所有找到的类执行Test测试,只有满足测试的类才会保留。 其代码如下: ```/** * 递归扫描指定的包及其子包中的类,并对所有找到的类执行Test测试,只有满足测试的类才会保留。 * * @param test 用于过滤类的测试对象 * @param packageName 被扫描的基础报名 */ public ResolverUtil find(Test test, String packageName) { String path = getPackagePath(packageName); try { // 获取指定路径下的所有文件 List children = VFS.getInstance().list(path); for (String child : children) { if (child.endsWith(".class")) { // 处理下面所有的类编译文件 addIfMatching(test, child); } } } catch (IOException ioe) { log.error("Could not read package: " + packageName, ioe); } return this; } ``` 在上述方法中有一行代码`List children = VFS.getInstance().list(path);`,这里回顾到解析`settings`标签并配置`VFS`实例的时候说过,用户自定义的VFS实例就是在此处生效的. ``` /** * 单例实例持有者 * Singleton instance holder. */ private static class VFSHolder { static final VFS INSTANCE = createVFS(); @SuppressWarnings("unchecked") static VFS createVFS() { // Try the teacher implementations first, then the built-ins List> impls = new ArrayList<>(); // 优先使用用户自己加载的 impls.addAll(USER_IMPLEMENTATIONS); // 使用系统默认的 impls.addAll(Arrays.asList((Class[]) IMPLEMENTATIONS)); // Try each implementation class until a valid one is found VFS vfs = null; for (int i = 0; vfs == null || !vfs.isValid(); i++) { // 当获取不到vfs对象时或者找到有效的vfs对象时结束. // 获取vfs实例类型 Class impl = impls.get(i); try { // 实例化vfs vfs = impl.newInstance(); if (vfs == null || !vfs.isValid()) { if (log.isDebugEnabled()) { log.debug("VFS implementation " + impl.getName() + " is not valid in this environment."); } } } catch (InstantiationException e) { log.error("Failed to instantiate " + impl, e); return null; } catch (IllegalAccessException e) { log.error("Failed to instantiate " + impl, e); return null; } } if (log.isDebugEnabled()) { log.debug("Using VFS adapter " + vfs.getClass().getName()); } return vfs; } } ``` 对于`VFS`实例实例化的过程,其实也涉及到一种常用的设计模式 —— 单例模式,此处稍作了解即可。 继续回到别名解析的方法上来,刚才说到获取指定包下所有的需要被注册别名的类已经找到之后,Mybatis会继续处理这些类,将这些类的别名注册交给`registerAlias(Class)`方法来完成。 ``` /** * 注册指定类型的别名到别名注册表中 * 在没有注解的场景下,会使用的Bean实例的简短名称的首字母小写的名称作为别名 * * @param 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); } ``` 在这个方法里,主要是获取需要注册别名类的别名,别名的默认值是别名类通过`#getSimpleName()`方法获取到的简短名称,但是如果再别名类上找到注解`Alias`,`Alias`注解的别名将会覆盖默认别名。 在获取到别名之后,继续调用方法`registerAlias(String, Class)`来真正执行注册别名的操作。 ``` /** * 将指定类型和别名注册到别名注册表 * * @param alias 别名 * @param value 指定类型 */ 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 (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) { throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'."); } // 添加到别名映射表 TYPE_ALIASES.put(key, value); } ``` 注册别名的时候,首先将别名转换为全小写字符串,之后校验当前是否已经注册了同名称的别名或者同类型的别名,如果都没有的话才会将别名添加到别名注册表中,即`TypeAliasRegistry#TYPE_ALIASES`中。 到这儿,对于`typeAliases`中的`package`子标签的解析就完成了,接下来说的是另一个标签`typeAlias`,之前说过,`typeAlias`有两个属性,其中`type`属性是必填的,他使用类的全限定名称来指向一个java类,`alias`属性是非必填的,它用来指定java类的别名。 其实针对`typeAlias`的解析已经包含在了刚才对`package`解析的过程中了,在解析`typeAlias`标签时,首先会判断`type`属性指向的java类是否存在,如果不存在则会抛出异常,存在的话才会继续处理`alias`的值。 如果`typeAlias`指定了`alias`的值,他会调用`registerAlias(String, Class)`方法来注册别名,如果没有指定`alias`的值,那么他会调用`registerAlias(Class)`方法在生成类别名之后,再通过`registerAlias(String, Class)`方法注册别名. 到此,`typeAliases`节点也已经解析完成,接下来是解析Mybatis插件的过程。 ### 解析使用Mybatis的插件机制(plugins节点) Mybtis的插件是一个很强大的功能,它允许我们在Mybatis运行期间切入到Mybatis内部中,执行我们想要做的一些事情,比如比较火的分页插件`page-helper`其实就是基于Mybatis的插件功能实现的。 Mybatis的插件配置DTD定义如下: ``` ``` `plugins`标签下必须定义一个或多个`plugin`标签,`plugin`有一个必填的属性`interceptor `,该值指向一个实现了`Interceptor`的类,同时在`plugin`标签下面运行出现零个或多个`property`标签,用来指定配置该插件的属性. 关于接口`Interceptor`的定义如下: ``` /** * Mybatis 拦截器(插件)接口定义 * @author Clinton Begin */ public interface Interceptor { /** * 拦截代理类执行操作 * @param invocation 代理 */ Object intercept(Invocation invocation) throws Throwable; /** * 拦截代理类的构建过程 */ Object plugin(Object target); /** * 设置拦截器属性 */ void setProperties(Properties properties); } ``` 然后我们继续看Mybatis插件配置的解析: ``` // 插件配置 pluginElement(root.evalNode("plugins")); ``` ``` /** * 解析plugins节点 * * @param parent plugins节点 */ private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 处理每一个interceptor String interceptor = child.getStringAttribute("interceptor"); // 获取name=>value映射 Properties properties = child.getChildrenAsProperties(); // 获取插件的实现 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); // 初始化插件配置 interceptorInstance.setProperties(properties); // 注册插件,在插件职责链中注册一个新的插件实例 configuration.addInterceptor(interceptorInstance); } } } ``` 整个插件处理的过程中有两个地方需要注意,一个是`(Interceptor) resolveClass(interceptor).newInstance();`,另一个是`configuration.addInterceptor(interceptorInstance);`。 其中`resolveClass(String alias)`是在`BaseBuilder`中定义的一个方法,用于通过别名解析出一个具体的java类实例,因此这也意味着,我们可以通过别名的方式注册Mybatis插件. `resolveClass(String alias)`方法的具体实现是委托给了`TypeAliasRegistry`的`resolveAlias(String)`来完成的. ``` public Class resolveAlias(String string) { try { if (string == null) { return null; } // issue #748 String key = string.toLowerCase(Locale.ENGLISH); Class value; if (TYPE_ALIASES.containsKey(key)) { // 优先从别名注册表加载 value = (Class) TYPE_ALIASES.get(key); } else { // 反射获取 value = (Class) Resources.classForName(string); } return value; } catch (ClassNotFoundException e) { throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e); } } ``` `configuration.addInterceptor(interceptorInstance);`的实现也不是简单的赋值而是在现有的`Configuration#interceptorChain#interceptors`中添加一个新的`interceptor`,其中`Configuration#interceptorChain#interceptors`是一个列表。 `InterceptorChain`的定义如下: ``` /** * Mybatis插件职责链 * * @author Clinton Begin */ public class InterceptorChain { /** * 所有的插件 */ private final List interceptors = new ArrayList<>(); /** * 调用所有插件的{@link Interceptor#plugin(Object)}方法 */ public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } /** * 添加一个新的{@link Interceptor#}实例 */ public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } /** * 获取所有的Interceptor */ public List getInterceptors() { return Collections.unmodifiableList(interceptors); } } ``` 因此,对于拦截器的解析过程是首先解析出interceptor的类名(全限定名称或者别名),之后通过反射获取拦截器的对象实例,并将在配置文件中配置的`property`解析成`Properties`,调用拦截器的`setProperties(Properties)`方法完成赋值,最后注册到`Configuration#interceptorChain#interceptors`中。 至此,Mybatis的插件也已经完成解析,接下来要解析的就是对象创建工厂。 ### 解析并配置Mybatis的对象创建工厂(objectFactory) 在Mybatis中其实有很多通过反射来实例化对象的操作,比如在JDBC操作完成之后将返回结果转换成具体的实体类的时候,对象的创建采用的就是反射来实现的,对于这种创建对象的操作,Mybatis提供了一个对象创建工厂的接口API,叫做`ObjectFactory`,其默认实现类是`DefaultObjectFactory`. `ObjectFactory`类定义如下: ``` /** * Mybatis用于所有对象的实例的工厂 * * @author Clinton Begin */ public interface ObjectFactory { /** * 设置配置参数 * * @param properties 配置参数 */ void setProperties(Properties properties); /** * 使用默认的构造方法创建一个指定类型的实例 * * @param type 指定类型 */ T create(Class type); /** * 通过构造方法和构造参数创建一个指定类型的实例 * Creates a new object with the specified constructor and params. * * @param type Object type 指定对象类型 * @param constructorArgTypes 构造参数类型集合 * @param constructorArgs 构造参数集合 */ T create(Class type, List> constructorArgTypes, List constructorArgs); /** * 如果此对象可以包含一组其他对象则返回true,该方法的目的是为了兼容非Collection的集合。 * @param type 指定对象类型 * @since 3.1.0 */ boolean isCollection(Class type); } ``` `configuration -> objectFactory`的作用就是用来配置这个对象工厂实现类的。 对于`objectFactory`的DTO定义如下: ``` ``` `objectFactory`有一个必填的`type`属性,该属性用于指向一个`ObjectFactory`的实例,这里可以使用别名,同时在`objectFactory`下可以配置一个或多个`property`子元素,用来设置`ObjectFactory`实例需要的参数。 ``` // 配置对象创建工厂 objectFactoryElement(root.evalNode("objectFactory")); ``` ``` /** * 解析objectFactory节点 * * @param context objectFactory节点 */ private void objectFactoryElement(XNode context) throws Exception { if (context != null) { // 获取objectFactory的type属性 String type = context.getStringAttribute("type"); Properties properties = context.getChildrenAsProperties(); // 获取factory的实例 ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance(); factory.setProperties(properties); // 配置对象创建工厂 configuration.setObjectFactory(factory); } } ``` 这个方法的实现也很简单,通过反射获取ObjectFactory的实例,之后将由`property`解析而成的Properties设值到实例中,最后赋值给`Configuration#objectFactory`的属性。 到此,`objectFactory`标签的解析也已经完成。 ### 解析并配置Mybatis的对象包装工厂(objectWrapperFactory节点) `ObjectWrapperFactory`是一个对象包装器工厂,在设计模式中有一个叫做装饰器的模式,他在不影响目标对象原有的执行结果的前提下,为对象的执行添加一些额外的功能,这里的ObjectWrapperFactory的作用就是为一个指定的对象生成一个包装类或者叫装饰类。 `ObjectWrapperFactory`的接口定义如下: ``` /** * 对象包装器工厂,主要是用来包装返回的result对象,比如可以用来对返回的数据结果进行统一的操作,比如脱敏,加密等操作。 * * @author Clinton Begin * @see org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(ResultSetWrapper, ResultMap, String) */ public interface ObjectWrapperFactory { /** * 是否拥有指定对象的装饰对象 * @param object 指定该对象 */ boolean hasWrapperFor(Object object); /** * 通过对象元数据获取指定对象的包装对象 * @param metaObject 对象元数据 * @param object 指定对象 */ ObjectWrapper getWrapperFor(MetaObject metaObject, Object object); } ``` 在Mybatis中对于`objectWrapperFactory`的DTO定义如下: ``` ``` `objectWrapperFactory` 必须有一个type属性,该属性指向一个`ObjectWrapperFactory`的实例,可以使用别名。 ``` // 配置对象包装工厂 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); ``` ``` /** * 解析objectWrapperFactory节点 * * @param context objectWrapperFactory节点 */ private void objectWrapperFactoryElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type"); ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance(); configuration.setObjectWrapperFactory(factory); } } ``` 其实现方法是通过反射获取`ObjectWrapperFactory`的实例,然后赋值给`Configuration#objectWrapperFactory`的属性。 ### 解析并配置Mybatis的反射工厂(reflectorFactory节点) 我们在上文讲到`MetaClass`时提到过`ReflectorFactory`,他是一个用于根据class生成`Reflector`的辅助性工厂类, 他定义了三个方法: ``` /** * 反射工厂 */ public interface ReflectorFactory { /** * 是否开启了缓存 */ boolean isClassCacheEnabled(); /** * 设置缓存 */ void setClassCacheEnabled(boolean classCacheEnabled); /** * 获取指定类的缓存信息 */ Reflector findForClass(Class type); } ``` 他创建的`Reflector`对象用来缓存一个类定义的基本信息,包括类的类型,可读可写属性名称,以及对应的getter/setter方法,构造函数等。 `ReflectorFactory`的默认实现是`DefaultReflectorFactory`,他提供了缓存`Reflector`对象的功能,缓存功能依托于一个`ConcurrentMap, Reflector>`类型的`reflectorMap`属性。 对于`ReflectorFactory`的DTO定义和`objectWrapperFactory`相似: ``` ``` `reflectorFactory` 必须有一个type属性,该属性指向一个`ReflectorFactory`的实例,可以使用别名。 其解析方法也和`objectWrapperFactory`相似: ``` // 配置反射工厂 reflectorFactoryElement(root.evalNode("reflectorFactory")); ``` ``` /** * 解析reflectorFactory节点 * * @param context reflectorFactory节点 */ private void reflectorFactoryElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type"); ReflectorFactory factory = (ReflectorFactory) resolveClass(type).newInstance(); configuration.setReflectorFactory(factory); } } ``` 其实现方法是通过反射获取`ReflectorFactory`的实例,然后赋值给`Configuration#reflectorFactory`的属性。 ### 通过settings初始化Mybatis全局配置(Configuration) 讲到了通过settings初始化Mybatis全局配置,我们就很难继续绕开Mybatis的`Configuration`对象。 `Configuration`对象无疑是Mybatis的核心对象之一,他定义了很多属性和方法,在解析的过程中,我们可能会遇到一些我们不了解其具体作用的属性或者方法,但是这都不重要,在后续的解析过程中,我们会慢慢填充对于`Configuration`对象的了解。 这里我们先大致贴出来`Configuration`对象的属性,这里先大致看一下,有一个印象,在后期解析的过程中,会逐渐讲到每个属性的作用。 #### 代码总览(可以跳过) ``` /** * Mybatis配置类 * 提供了Mybatis所有的配置以及Mapper文件的元数据容器 * * @author Clinton Begin */ public class Configuration { /** * 环境信息,包括事务和数据源等数据 */ protected Environment environment; /** * 是否允许在嵌套语句中使用{@link RowBounds}执行分页操作 */ protected boolean safeRowBoundsEnabled; /** * 是否允许在嵌套语句中使用{@link ResultHandler}执行分页操作 */ protected boolean safeResultHandlerEnabled = true; /** * 将下划线转换为驼峰 */ protected boolean mapUnderscoreToCamelCase; /** * 是否允许在在方法调用时直接加载该对象的所有属性 */ protected boolean aggressiveLazyLoading; /** * 是否允许单条语句返回多个结果集 */ protected boolean multipleResultSetsEnabled = true; /** * 是否允许JDBC自动生成主键 */ protected boolean useGeneratedKeys; /** * 使用列标签代替列名 */ protected boolean useColumnLabel = true; /** * 是否启用缓存 */ protected boolean cacheEnabled = true; /** * 当内容为null时,是否依然设值 */ protected boolean callSettersOnNulls; /** * 是否使用实际的参数名称,一定程度上可以减少{@link org.apache.ibatis.annotations.Param}的代码 */ protected boolean useActualParamName = true; /** * 在获取不到内容的时候,是否允许返回一个空实例 * 在常规情况下如果查询结果返回的内容是null,MyBatis默认返回null,如果开启该功能,其将会返回一个空实例 */ protected boolean returnInstanceForEmptyRow; /** * Mybatis日志前缀 */ protected String logPrefix; /** * 日志实现类,如果未实现则自动查找 */ protected Class logImpl; /** * 虚拟文件系统,提供了一个访问系统文件资源的简单API */ protected Class vfsImpl; /** * 缓存的生命周期 * MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 * 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询. * 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 */ protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION; /** * 字段在数据库中的类型 * 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 * 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 */ protected JdbcType jdbcTypeForNull = JdbcType.OTHER; /** * 懒加载的触发方法 */ protected Set lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString")); /** * {@link java.sql.Statement}的默认超时时间,他决定了驱动等待数据库响应的秒数,默认不超时 */ protected Integer defaultStatementTimeout; /** * 获取数据的默认大小 * 为驱动的结果集设置默认获取数量。 */ protected Integer defaultFetchSize; /** * 执行预期时使用的执行器的行为类型 * 其中: * {@link ExecutorType#SIMPLE} 会为每个语句的执行都创建一个新的预处理语句({@link java.sql.PreparedStatement}) * {@link ExecutorType#REUSE} 会复用预处理语句 * {@link ExecutorType#BATCH} 会复用预处理语句并执行批量操作 */ protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE; /** * 自动映射行为定义 * {@link AutoMappingBehavior#NONE} 禁用自动映射 * {@link AutoMappingBehavior#PARTIAL} 局部自动映射,只映射没有定义嵌套结果集映射的结果集 * {@link AutoMappingBehavior#FULL} 完整自动映射,会自动映射任意复杂的结果集(无论是否嵌套) */ protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL; /** * 当自动映射遇到无法识别的字段的时候的处理行为 * {@link AutoMappingUnknownColumnBehavior#NONE} 不做任何操作 * {@link AutoMappingUnknownColumnBehavior#WARNING} 输出警告日志 * {@link AutoMappingUnknownColumnBehavior#FAILING} 终止自动映射并抛出异常 */ protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE; /** * 属性配置 configuration->settings下的属性 */ protected Properties variables = new Properties(); /** * 配置反射工厂,简化操作属性和构造器 */ protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); /** * 配置对象创建工厂 */ protected ObjectFactory objectFactory = new DefaultObjectFactory(); /** * 配置对象包装工厂,主要用于创建非原生对象 */ protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory(); /** * 是否启用懒加载。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 */ protected boolean lazyLoadingEnabled = false; /** * 代理工厂,指定Mybatis创建懒加载对象使用的代理工具 */ protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL /** * 数据库类型唯一标志,Mybatis根据能够根据不能的数据库厂商执行不同的语句,实现此功能就是依赖于该标志。 */ protected String databaseId; /** * 配置工厂 * Configuration factory class. * Used to create Configuration for loading deserialized unread properties. * * @see Issue 300 (google code) */ protected Class configurationFactory; /** * Dao操作对象注册表 */ protected final MapperRegistry mapperRegistry = new MapperRegistry(this); /** * 拦截器职责链(Mybatis插件) */ protected final InterceptorChain interceptorChain = new InterceptorChain(); /** * 类型处理器注册表 */ protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(); /** * 类型别名注册表,主要用在执行SQL语句的出入参以及一些类的简写 */ protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry(); /** * 语言支持驱动注册表 */ protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry(); /** * 声明语句映射表 */ protected final Map mappedStatements = new StrictMap("Mapped Statements collection") .conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and " + targetValue.getResource()); /** * 缓存映射表 */ protected final Map caches = new StrictMap<>("Caches collection"); /** * ResultMap映射表 */ protected final Map resultMaps = new StrictMap<>("Result Maps collection"); /** * 参数映射表 */ protected final Map parameterMaps = new StrictMap<>("Parameter Maps collection"); /** * 主键生成器映射表 */ protected final Map keyGenerators = new StrictMap<>("Key Generators collection"); /** * 已加载过得资源集合,可以用来防止重复加载文件 */ protected final Set loadedResources = new HashSet<>(); /** * 代码块映射集合 */ protected final Map sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers"); /** * 未完成处理的声明语句 */ protected final Collection incompleteStatements = new LinkedList<>(); /** * 未完成处理的缓存引用 */ protected final Collection incompleteCacheRefs = new LinkedList<>(); /** * 未完成处理的返回结果映射 */ protected final Collection incompleteResultMaps = new LinkedList<>(); /** * 未完成处理的方法集合 */ protected final Collection incompleteMethods = new LinkedList<>(); /* * 缓存引用映射关系 * 缓存引用方=》缓存 */ protected final Map cacheRefMap = new HashMap<>(); public Configuration(Environment environment) { this(); this.environment = environment; } public Configuration() { // 注册别名 // 注册JDBC别名 typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); // 注册事务管理别名 typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); // 注册JNDI别名 typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); // 注册池化数据源别名 typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); // 注册为池化的数据源 typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); // 注册永久缓存 typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); // 注册先入先出的缓存 typeAliasRegistry.registerAlias("FIFO", FifoCache.class); // 注册最近最少使用缓存 typeAliasRegistry.registerAlias("LRU", LruCache.class); // 注册软缓存 typeAliasRegistry.registerAlias("SOFT", SoftCache.class); // 注册弱缓存 typeAliasRegistry.registerAlias("WEAK", WeakCache.class); // 注册处理数据库ID的提供者 typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); // 注册基于XML的语言驱动 typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); //注册静态语言驱动(通常无需使用) typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); // 注册Sl4j日志 typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); //注册Commons日志 typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); //注册log4j日志 typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); //注册log4j2日志 typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); //注册jdk log日志 typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); //注册标准输出日志 typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); //注册无日志 typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); // 注册CGLIB typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); // 注册JAVASSIST typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); // 默认使用XML语言驱动 languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); // 支持原始语言驱动 languageRegistry.register(RawLanguageDriver.class); } ``` Emmmm。。。我有看了一遍还是觉得属性太多了,不管了,待会再一个个的了解吧。 #### 通过settings的配置初始化Mybatis的全局配置 还是回到通过settings初始化全局配置上来。 ``` // 通过settings配置初始化全局配置 settingsElement(settings); ``` ``` /** * 通过用户自定义配置覆盖系统默认配置 * * @param props 用户自定义配置 * @see http://www.mybatis.org/mybatis-3/zh/configuration.html#settings */ private void settingsElement(Properties props) { // 定义Mybatis如何自动将JDBC列转换为字段或者属性,它对应AutoMappingBehavior枚举类,其中 // NONE:表示禁用自动映射 // PARTIAL: 局部自动映射,只映射没有定义嵌套结果集映射的结果集 // FULL: 完整自动映射,会自动映射任意复杂的结果集(无论是否嵌套) configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL"))); // 定义当发现使用自动映射功能无法处理(识别)的列或者字段时的行为.它对应AutoMappingUnknownColumnBehavior枚举类,其中 // NONE:表示无任何操作 // WARNING:表示输出警告日志 // FAILING:映射失败并抛出SqlSessionException异常 configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE"))); // 全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存。 configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); // 指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认使用JavassistProxyFactory. configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory"))); // 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false)); // 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载(参考lazyLoadTriggerMethods). configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false)); // 是否允许单一语句返回多结果集(需要兼容驱动)。 configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true)); // 是否使用列标签代替列名称。 configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true)); // 是否允许JDBC自动生成主键,需要驱动兼容。 // 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false)); // 配置默认的执行器。对应ExecutorType枚举类 // SIMPLE 就是普通的执行器; // REUSE 执行器会重用预处理语句(prepared statements); // BATCH 执行器将重用语句并执行批量更新。 configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE"))); // 设置超时时间,它决定驱动等待数据库响应的秒数。 configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null)); // 为驱动的结果集获取数量(fetchSize)设置一个提示值。此参数只可以在查询设置中被覆盖。 configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null)); // 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射 configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false)); // 允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为false,看好了,允许使用是False。 configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false)); //MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 // 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 // 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION"))); // 在没有为参数指定JDBC类型时,在该参数为null的时候,默认使用的JDBC类型,通常使用NULL,VARCHAR,OTHER. configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER"))); // 懒加载的触发方法,指定哪个对象的哪些方法触发一次延迟加载。 configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString")); // 允许在嵌套语句中使用分页(ResultHandler)。如果允许使用则设置为false。 configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true)); // 指定动态 SQL 生成的默认语言,目前默认是XMLLanguageDriver. configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage"))); // 指定 Enum 使用的默认 TypeHandler 。 configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler"))); // 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法, // 这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。 // 注意基本类型(int、boolean等)是不能设置成 null 的。 configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false)); // 是否使用允许使用方法签名中的名称作为语句参数名称,一定程度上可以减少{@link org.apache.ibatis.annotations.Param}的代码 configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true)); // 当返回行的所有列都是空时,MyBatis默认返回null。 当开启这个设置时,MyBatis会返回一个空实例。 // 需要注意的是,他也适用于嵌套的结果集(collection和association). configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false)); // 指定 MyBatis 增加到日志名称的前缀。 configuration.setLogPrefix(props.getProperty("logPrefix")); // 指定一个提供Configuration实例的类。 // 这个被返回的Configuration实例用来加载被反序列化对象的懒加载属性值。 // 这个类必须包含一个签名方法static Configuration getConfiguration(). configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory"))); } ``` 不得不说`settingsElement(Properties)`方法中的代码也挺多的,我们先耐着性质一个一个看下去。 ##### Configuration#autoMappingBehavior(配置Mybatis的自动映射行为) ``` configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL"))); ``` AutoMappingBehavior是mybatis的定义自动映射列和字段的策略定义枚举,他有三个参数,`NONE`,`PARTIAL`,`FULL`. 其中: - NONE,表示禁用自动映射的功能。 - PARTIAL,表示局部自动映射,意思就是只会自动映射没有定义嵌套结果集的结果集。 - FULL,表示完整的自动映射功能,他会自动映射任意复杂的结果集,包括嵌套结果集的结果集。 当我们在ResultMap的返回参数中定义了`collection`或者`association`属性的时候,其查询操作并不是一个简单的SQL,他的具体结果实际上是一个一对多的复杂集合,这里这个一对多的复杂集合就是嵌套结果集。 比如:我们定义了如下Mapper: ``` ``` 在这里ClassInfo对应的查询结果里面包括了一个Student集合,这里Student集合就是嵌套的结果集。 这样,只有在我们的`Configuration#autoMappingBehavior`值为`AutoMappingBehavior#FULL`的时候,才会自动将Student的返回结果的ID和Name值赋给Student对象的相应字段。 ##### Configuration#autoMappingUnknownColumnBehavior(配置Mybatis在自动映射时遇到无法识别的字段的时候的处理行为) ``` configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE"))); ``` `AutoMappingUnknownColumnBehavior`是Mybatis定义在自动映射时发现无法处理(识别)的列或字段的时候的行为枚举类,他提供了一个叫做`doAction`的方法`来实现具体的操作. ``` /** * 检测到自动映射目标的未知列(或未知属性类型)时执行操作。 * * @param mappedStatement 映射声明语句 * @param columnName 字段名称 * @param propertyName 属性名称 * @param propertyType 字段类型,如果该字段不为null,则表示未注册TypeHandler. */ public abstract void doAction(MappedStatement mappedStatement, String columnName, String propertyName, Class propertyType); ``` `AutoMappingUnknownColumnBehavior`有三种模式,其中默认使用的是`NONE`. - NONE,不执行任何操作 - WARNING,输出警告日志. - FAILING,映射失败并抛出{@link SqlSessionException}. #### Configuration#cacheEnabled(是否启用缓存) ``` configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); ``` cacheEnabled是Mybatis用于配置是否启用所有缓存的开关,默认为`true`。 ##### Configuration#proxyFactory(Mybatis懒加载对象的代理工厂) ``` configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory"))); ``` proxyFactory指定了Mybatis创建具有延迟加载能力的对象所用到的代理工厂,默认使用JavassistProxyFactory. ##### Configuration#lazyLoadingEnabled(是否启用懒加载的能力) ``` configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false)); ``` lazyLoadingEnabled是MYbatis是否启用懒加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 ##### Configuration#aggressiveLazyLoading(是否允许在在方法调用时直接加载该对象的所有属性) ``` configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false)); ``` 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载,默认为`false`(参考lazyLoadTriggerMethods). ##### Configuration#multipleResultSetsEnabled(是否允许单一语句返回多结果集) ``` configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true)); ``` 默认为`true`。 ##### Configuration#useColumnLabel(是否使用列标签代替列名称) ``` configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true)); ``` 默认为`true`。 ##### Configuration#useGeneratedKeys(是否允许JDBC自动生成主键) ``` configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false)); ``` 该参数用于设置是否自动生成主键,默认为`false`,如果这个参数为`true`,那么,将强制自动生成主键,尽管一些驱动不能兼容但仍可正常工作。 ##### Configuration#defaultExecutorType(配置Mybatis的默认Sql执行器) ``` configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE"))); ``` defaultExecutorType对应了ExecutorType枚举类,该枚举类定义了Mybatis中Sql执行器的三种类型: - SIMPLE 就是普通的执行器,会为每个语句的执行都创建一个新的预处理语句({@link java.sql.PreparedStatement}); - REUSE 执行器会重用预处理语句(prepared statements); - BATCH 执行器将重用语句并执行批量更新。 默认使用的是`SIMPLE`。 ##### Configuration#defaultStatementTimeout(配置Mybatis请求超时时间) ``` configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null)); ``` 该值决定了Mybatis等待数据库响应的秒数,默认永不超时。 ##### Configuration#defaultFetchSize(配置Mybatis请求结果集的数量限制) ``` configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null)); ``` 为驱动的结果集获取数量(fetchSize)设置一个提示值。此参数只可以在查询设置中被覆盖。 ##### Configuration#mapUnderscoreToCamelCase(开启自动将下划线转换为驼峰的功能) ``` configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false)); ``` 开启该功能后,Mybatis将会自动将JDBC的类名称由典型的下划线命名法转换为Java属性的驼峰命名法,默认值为`false`。 ##### Configuration#safeRowBoundsEnabled(是否允许在嵌套语句中使用分页(RowBounds)) ``` configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false)); ``` 是否允许在嵌套结果集中使用RowBouds,默认为`false`,注意!! `false`为允许. > RowBounds是Mybatis的分页类,具体的使用会在后面给出。 ##### Configuration#localCacheScope(设置Mybatis本地缓存的生命周期) ``` configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION"))); ``` `LocalCacheScope`是Mybatis本地缓存的生命周期定义枚举,他有两个属性,分别对应本地缓存的不同生命周期。 - SESSIO,,这种情况下会缓存一个会话中执行的所有查询 - STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据. 默认使用的`SESSION`. ##### Configuration#jdbcTypeForNull(配置在没有为参数指定JDBC类型时,且该参数为NULL时默认使用的JDBC类型) ``` configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER"))); ``` 在没有为参数指定JDBC类型时,且参数为null的时候,默认使用的JDBC类型,通常是NULL,VARCHAR,OTHER三者之一,这里就不列举`JdbcType`枚举的实例了,里面的内容挺多的,这里默认为`OTHER`。 ##### Configuration#lazyLoadTriggerMethods(配置Mybatis懒加载的触发方法) ``` configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString")); ``` 该参数在`aggressiveLazyLoading`中提到过,两者通常配合使用,他制定了懒加载的触发方法,用来指定哪些对象的哪些方法触发一次懒加载,默认值为:`equals,clone,hashCode,toString`。 ##### Configuration#safeResultHandlerEnabled(允许在嵌套语句中使用分页(ResultHandler)) ``` configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true)); ``` 设置Mybaits是否允许在嵌套语句中使用ResultHandler,注意!!如果允许的话,请设置为`false`,默认值为`false`. ##### Configuration#languageRegistry(MybatisSql脚本语言处理器注册表) ``` configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage"))); ``` ``` /** * 设置Mybatis的Sql默认脚本语言处理器 * * @param driver 脚本语言处理器 */ public void setDefaultScriptingLanguage(Class driver) { if (driver == null) { driver = XMLLanguageDriver.class; } // 往脚本语言处理器注册表中注册默认的脚本语言处理器 getLanguageRegistry().setDefaultDriverClass(driver); } ``` ``` public LanguageDriverRegistry getLanguageRegistry() { return languageRegistry; } ``` ``` /** * 设置默认的脚本语言处理器的类型 * * @param defaultDriverClass 默认的脚本语言处理器的类型 */ public void setDefaultDriverClass(Class defaultDriverClass) { // 注册默认的脚本语言处理器 register(defaultDriverClass); this.defaultDriverClass = defaultDriverClass; } ``` 配置Mybatis 默认的Sql脚本语言处理器,默认使用`XMLLanguageDriver`SQL语言驱动。 在`LanguageDriver`中定义了三个方法: ``` /** * 语言驱动器 * * @see org.apache.ibatis.scripting.xmltags.XMLLanguageDriver Xml语言驱动器(默认) */ public interface LanguageDriver { /** * 创建一个{@link ParameterHandler},将实际参数传递给JDBC语句 * * @param mappedStatement 正在的执行的声明语句 * @param parameterObject 输入的参数对象 * @param boundSql 生成的SQL * @return * @author Frank D. Martinez [mnesarco] * @see DefaultParameterHandler */ ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); /** * 创建一个{@link SqlSource},它将保存从mapper xml文件中读取的语句。 * 从xml文件中读取映射语句,在启动期间调用。 * * @param configuration Mybatis配置 * @param script 从XMl文件解析的XNode * @param parameterType 输入的参数类型 * @return */ SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType); /** * 创建一个{@link SqlSource},它将保存从mapper xml文件中读取的语句。 * 从类中读取映射语句,在启动期间调用。 * * @param configuration Mybatis配置 * @param script 注解中的内容 * @param parameterType 参数类型 * @return */ SqlSource createSqlSource(Configuration configuration, String script, Class parameterType); } ``` 除了Mybatis中的``XMLLanguageDriver``驱动以外,比较典型的用法还有在`Mybatis-plus`中`MybatisXMLLanguageDriver`就是重写了`XMLLanguageDriver`的`createParameterHandler` 方法,进而实现使用自定义的`MybatisDefaultParameterHandler`。 `LanguageDriverRegistry`是Mybatis的Sql语句处理器的注册表,他维护了Mybatis中所有能处理Sql语言的驱动类型集合。 他持有一个`LANGUAGE_DRIVER_MAP`映射集合,用以存放语言处理器类型和语言处理器实例。 ``` /** * Mybatis Sql脚本语言处理器注册表 * * @author Frank D. Martinez [mnesarco] */ public class LanguageDriverRegistry { /** * 脚本语言处理器注册表 * 脚本语言处理器的类型=》脚本语言处理器实例 */ private final Map, LanguageDriver> LANGUAGE_DRIVER_MAP = new HashMap<>(); /** * 默认的脚本语言处理器 */ private Class defaultDriverClass; /** * 添加新的Mybatis脚本语言处理器 * * @param cls 脚本语言处理器的类型 */ public void register(Class cls) { if (cls == null) { throw new IllegalArgumentException("null is not a valid Language Driver"); } if (!LANGUAGE_DRIVER_MAP.containsKey(cls)) { try { // 注册 LANGUAGE_DRIVER_MAP.put(cls, cls.newInstance()); } catch (Exception ex) { throw new ScriptingException("Failed to load language driver for " + cls.getName(), ex); } } } /** * 添加新的Mybatis脚本语言处理器 * * @param instance 脚本语言处理器 */ public void register(LanguageDriver instance) { if (instance == null) { throw new IllegalArgumentException("null is not a valid Language Driver"); } // 获取Class类型 Class cls = instance.getClass(); if (!LANGUAGE_DRIVER_MAP.containsKey(cls)) { //注册 LANGUAGE_DRIVER_MAP.put(cls, instance); } } /** * 通过脚本语言处理器的类型获取脚本语言处理器 * * @param cls 脚本语言处理器的类型 * @return 脚本语言处理器 */ public LanguageDriver getDriver(Class cls) { return LANGUAGE_DRIVER_MAP.get(cls); } /** * 获取默认的脚本语言处理器实例 */ public LanguageDriver getDefaultDriver() { return getDriver(getDefaultDriverClass()); } /** * 获取默认的脚本语言处理的类型 */ public Class getDefaultDriverClass() { return defaultDriverClass; } /** * 设置默认的脚本语言处理器的类型 * * @param defaultDriverClass 默认的脚本语言处理器的类型 */ public void setDefaultDriverClass(Class defaultDriverClass) { // 注册默认的脚本语言处理器 register(defaultDriverClass); this.defaultDriverClass = defaultDriverClass; } } ``` 在`configuration#setDefaultScriptingLanguage(Class)`就是调用的`LanguageDriverRegistry`的`setDefaultDriverClass`方法来设置全局默认的Sql语言处理器。 从`configuration#setDefaultScriptingLanguage(Class)`中我们也可以看出,Mybatis的默认SQL语言处理器是`XMLLanguageDriver`。 ##### Configuration#typeHandlerRegistry ``` configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler"))) ``` ``` /** * 配置枚举类型的默认的TypeHandler,默认使用EnumTypeHandler。 * * @param typeHandler 用于处理枚举类型的类型处理器 * @since 3.4.5 */ public void setDefaultEnumTypeHandler(Class typeHandler) { if (typeHandler != null) { // 获取类型处理器注册表,并设置默认的枚举类型处理器 getTypeHandlerRegistry().setDefaultEnumTypeHandler(typeHandler); } } ``` 获取Configuration中的typeHandlerRegistry实例. ``` /** * 获取类型处理器注册表 */ public TypeHandlerRegistry getTypeHandlerRegistry() { return typeHandlerRegistry; } ``` 配置默认的枚举类型处理器. ``` /** * 配置枚举类型的默认的TypeHandler,默认使用EnumTypeHandler。 * * @param typeHandler 用于处理枚举类型的类型处理器 * @since 3.4.5 */ public void setDefaultEnumTypeHandler(Class typeHandler) { this.defaultEnumTypeHandler = typeHandler; } ``` `TypeHandlerRegistry`是Mybatis中的类型转换处理器注册表,它主要负责维护用于java类型和JDBC类型的转换的`TypeHandler`, 他是Mybatis中不可或缺的一个组件,作为一个注册表,在初始化的时候他注册了很多默认的类型转换处理器,便于我们使用。 `TypeHandlerRegistry`的`defaultEnumTypeHandler`属性用于记录默认的枚举类型处理器。 参数`defaultEnumTypeHandler`的作用就是指定`TypeHandlerRegistry#defaultEnumTypeHandler`的值,默认为`EnumTypeHandler`。 ##### Configuration#callSettersOnNulls(指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法) ``` configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false)); ``` 设置在Mybatis中当结果集中的某个值为null时,是否依然调用所属JAVA对象的属性对应的Setter方法,默认值为`false`。 ##### Configuration#returnInstanceForEmptyRow(当查询内容为空时,是否初始化一个空实例) ``` configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false)); ``` 设置当Mybatis的查询结果集所有的返回行都是空的时候,是否初始化一个空的实例,该值默认为false,即不使用。 > 需要注意的是,当开启了该功能之后,对于嵌套结果(collection和association)也是同样生效的。 ##### Configuration#logPrefix(指定 MyBatis 增加到日志名称的前缀) ``` configuration.setLogPrefix(props.getProperty("logPrefix")); ``` 设置Mybatis中日志使用的公共前缀。 ##### Configuration#configurationFactory(指定Configuration的工厂) ``` configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory"))); ``` 指定一个提供`Configuration`实例的类,这个被返回的`Configuration`实例用来加载被反序列化对象的懒加载属性。 > configurationFactory指向的类必须有一个`static Configuration getConfiguration()`方法. OK,基于settings元素配置Configuration的过程告一段落,这一部分的内容相对比较枯燥,也很繁琐,到这里,对这些不同的字段 有个大概的印象就好了。 ### 解析environments元素,配置Mybatis中的多环境。 在完成枯燥的基于`settings`配置`Configuration`对象的过程之后,就到了解析`environments`标签,配置Mybatis的多环境的过程了。 Mybatis默认是支持多环境配置的,在Mybatis中有一个`Environment`的对象,该对象有三个简单的参数: ``` /** * Mybatis环境容器 * * @author Clinton Begin */ public final class Environment { // 环境唯一标志 private final String id; // 事务工厂 private final TransactionFactory transactionFactory; // 数据源 private final DataSource dataSource; } ``` 其中`id`是当前环境的唯一标志,属于语义化属性。 `transactionFactory`属性对应的是`TransactionFactory`对象,他是一个事务创建工厂,用于创建`Transaction`对象, `Transaction`对象包装了JDBC的`Connection`,用于处理数据库链接的生命周期,包括链接的:创建,提交/回滚和关闭。 `dataSource`属性对应的是`DataSource`对象,指向了一个JDBC数据源。 在Mybatis关于`environments`的DTD定义是这样的: ``` ``` 在`environments`中必须指定`default`属性的值,他是默认环境的ID,用于指定默认使用的环境,同时在`environments` 中允许出现一个或多个`environment`子元素. ``` ``` `environment`元素有一个必填的ID属性,是当前环境配置的唯一标志,同时他下面必须配置一个`transactionManager`和 `dataSource`子元素。 #### transactionManager 其中`transactionManager`用来配置当前环境的事务管理器,其DTD定义如下: ``` ``` `transactionManager`有一个必填的`type`属性,表示使用的事务管理器的类型,在Mybatis中默认提供了两种类型的事务管 理器:`JDBC`和`MANAGED`。 这两个简短的名称依托于Mybatis的类型别名机制,他们是在`Configuration`的无参构造方法中注册的: ``` public Configuration() { // 注册别名 // 注册JDBC别名 typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); // 注册事务管理别名 typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); ... } ``` 其中`JDBC`对应的`JdbcTransactionFactory`创建的`JdbcTransaction`对象直接使用了默认的JDBC的提交和回滚设置,依赖 于从数据源得到的链接来管理事务作用域。 而`MANAGED`对应的`ManagedTransactionFactory`创建的`ManagedTransaction`对象是`Transaction`的实现之一,忽略所有的 提交和回滚请求,默认他关闭链接的功能是可用的,不过可以通过配置使他关闭链接的功能失效。 同时在`transactionManager`下可以配置多个`property`标签,用于自定义的事务管理器参数。 #### dataSource `dataSource`元素用来配置JDBC数据源,他的DTD定义如下: ``` ``` `dataSource`标签同样有一个必填的`type`属性,它用于指向JDBC数据源的具体实例,在Mybatis中默认提供了三种类型的数据源: `UNPOOLED`,`POOLED`和`JNDI`。 同样,这三个简短的名称也是Mybatis的类型别名,其注册也是在`Configuration`对象的无参构造中: ``` public Configuration() { // 注册别名 ... // 注册JNDI别名 typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); // 注册池化数据源别名 typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); // 注册为池化的数据源 typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); ... } ``` 其中`JNDI`对应的`JndiDataSourceFactory`的作用是基于JNDI加载一个可用的`DataSource`。 `POOLED`对应的`PooledDataSourceFactory`用于获取一个`PooledDataSource`,`PooledDataSource`是一个简单的,同步的, 线程安全的数据库连接池,通过复用JDBC`Connection`,避免了重复创建新的链接实例所必须的初始化和认证时间,提高了 应用程序对应数据库访问的并发能力。 `UNPOOLED`对应的`UnpooledDataSourceFactory`用于获取一个`UnpooledDataSource`,`UnpooledDataSource`是一个简单的数据源,他对每一次获取链接的请求都会打开一个新的链接。 `dataSource`标签下允许出现多个`property`子标签,这些`property`将会转换成`Properties`用于初始化和配置对应的`DataSource`. 在了解了每一个元素标签的作用之后,我们继续回到解析`environments`的代码上来。 调用解析的入口(`XmlConfigBuilder`): ``` // 加载多环境源配置,寻找当前环境(默认为default)对应的事务管理器和数据源 environmentsElement(root.evalNode("environments")); ``` 解析并构建Mybatis的Environment。 ``` /** * 解析environments节点 * * @param context 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"); 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); // 配置Mybatis当前的环境容器 configuration.setEnvironment(environmentBuilder.build()); } } } } ``` 解析`environments`标签的过程并不是很复杂,他首先通过`environments`的`default`属性获取用户指定的默认环境标志。 然后遍历`environments`下的所有`environment`节点,通过`environment`节点的`id`属性获取配置的`environment`的唯一标志, 如果当前标志和用户指定的默认环境标志一致的话,则处理该`environment`节点,否则跳过处理。 ``` private boolean isSpecifiedEnvironment(String id) { if (environment == null) { throw new BuilderException("No environment specified."); } else if (id == null) { throw new BuilderException("Environment requires an id attribute."); } else if (environment.equals(id)) { return true; } return false; } ``` 在处理`environment`节点的过程中,主要是通过元素`transactionManager`和`dataSource`的配置,获取`TransactionFactory` 对象以及`DataSourceFactory`对象,之后再通过`DataSourceFactory`获取`DataSource`对象的实例,最后使用`Environment` 对象的构建器来构建一个`Environment`对象,并同步到Mybatis的配置类`Configuration`中。 关于`transactionManager`的解析过程: ``` // 创建事务工厂 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); ``` ``` /** * 根据environments>environment>transactionManager节点配置事务管理机制 * * @param context environments>environment>transactionManager节点内容 */ 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."); } ``` `transactionManager`的解析过程比较简单,首先获取`transactionManager`元素的`type`属性执行的事务管理器的别名。 之后通过`BaseBuilder#resikveClass()`(该方法在前文有过解析)从Mybatis的别名注册表中找到具体的`TransactionFactory`实例的类型, 通过反射获取`TransactionFactory`的实例,之后通过`TransactionFactory#setProperties(Properties)`方法将用户自定 义的参数同步进去,这样就完成了`transactionManager`元素的解析。 关于`dataSource`的解析过程: ``` // 创建数据源工厂 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); ``` ``` /** * 解析dataSource元素 */ private DataSourceFactory dataSourceElement(XNode context) throws Exception { if (context != null) { // 获取dataSource的type属性指向的DataSourceFactory别名。 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."); } ``` `dataSource`元素的解析过程和`transactionManager`近乎一致,首先根据`type`属性获取指向的`DataSourceFactory`别名, 然后获取用户配置参数集合,之后通过`BaseBuilder#resolve(String)`获取`DataSourceFactory`实例的类型,并反射获取 实例,最后将用户配置的参数集合同步到`DataSourceFactory`中。 在完成`transactionManager`和`dataSource`的解析初始化之后,Mybatis通过`DataSourceFactory`获取到具体的`DataSource` 实例,此时,配置`Environment`对象的必备元素已经全部得到,接下来通过使用`Environment`对象的构建器来构建一个 `Environment`对象,并将该`Environment`实例同步到Mybatis的配置类`Configuration`中的`environment`属性中。 至此,整个`environments`元素的解析也已经完成。 ### 解析databaseIdProvider元素,配置数据库类型唯一标志生成器 Mybatis提供了一种【可以根据不同的数据库类型执行不同的语句】的功能,这个功能的实现就是基于在语句中定义的`databaseId`属性,在确定了当前数据库的类型之后,Mybatis会加载【所有未指定`databaseId`的语句以及和当前数据库类型匹配的语句】,如果同时定义了带有`databaseId`和无`databaseId`的语句,那么将会采用带有`databaseId`的语句。 至于如何确定当前数据库类型以及如何转换成`databaseId`,其功能就是由`DatabaseIdProvider`来实现的,`DatabaseIdProvider`接口定义了一个`getDatabaseId(DataSource)`的方法,使用该方法可以获取指定数据源的`databaseId`. `databaseIdProvider`元素用来指定Mybatis使用的`DatabaseIdProvider`的具体实现类。 他的DTD定义如下: ``` ``` `databaseIdProvider`有一个必填的属性`type`,指定了Mybatis用于生成`databaseId`的`DatabaseIdProvider`的实现类,该参数可以使用Mybatis的别名。 目前Mybaits在Configuration中只默认注册了一个`DB_VENDOR`别名,他指向了`VendorDatabaseIdProvider`实例。 ``` public Configuration() { ... // 注册处理数据库ID的提供者 typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); ... } ``` `VendorDatabaseIdProvider`基于`DatabaseMetaData#getDatabaseProductName`来判断`databaseId`。 当用户使用`VendorDatabaseIdProvider`来作为数据唯一标志的提供者的时候,需要用户自己提供数据库类型和`databaseId`的对应关系。 数据库类型用来给`VendorDatabaseIdProvider`判断指定数据源的元数据中的产品名称是否包含该值,进而根据用户的定义返回`databaseId`。 用户指定数据库类型和`databaseId`的关系,可以通过`databaseIdProvider`元素的`property`子元素来实现。 比如: ``` ``` 接下来回到`databaseIdProvider`元素的解析代码: ``` /** * 解析 databaseIdProvider节点 * * @param context databaseIdProvider节点 */ private void databaseIdProviderElement(XNode context) throws Exception { DatabaseIdProvider databaseIdProvider = null; if (context != null) { String type = context.getStringAttribute("type"); // awful patch to keep backward compatibility if ("VENDOR".equals(type)) { type = "DB_VENDOR"; } // 获取用户定义的数据库类型和databaseId的配置 Properties properties = context.getChildrenAsProperties(); // 获取databaseIdProvider实例 databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); // 配置数据库类型和databaseId的对应关系 databaseIdProvider.setProperties(properties); } // 获取Environment容器 Environment environment = configuration.getEnvironment(); if (environment != null && databaseIdProvider != null) { // 获取当前环境的databaseId String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); // 同步configuration#databaseId的值 configuration.setDatabaseId(databaseId); } } ``` 对于`databaseIdProvider`元素的解析相对比较简单,首先获取`databaseIdProvider`的`type`属性指定的`DatabaseIdProvider`实例名称,然后兼容`VENDOR`别名为`DB_VENDOR`,之后通过`BaseBuilder#resolveClass(String)`方法获取`DatabaseIdProvider`实例的类型,然后反射获取对应的实例,并初始化其数据库类型和`databaseId`的关系映射。 之后获取Mybatis当前环境的`Environment`对象(`Configuration#environment`)中的数据源,之后使用获取到的`DatabaseIdProvider`实例的`getDatabaseId(DataSource)`方法获取当前环境的`databaseId`,然后将`databaseId`赋值给`Configuration#databaseId`。 到这为止,`databaseIdProvider`元素的解析也已经完成。 #### 解析Mybatis的typeHandlers元素,配置Mybatis的类型转换器 在前面的文章中,我们有提到过`TypeHandlerRegistry`的作用是维护了Mybatis的类型转换处理器注册表. 类型转换处理器(`TypeHandler`)是一个定义如何处理java类型和jdbc类型互相转换的策略接口,它约束类型转换处理器实现类需要提供下列四种方法: - 根据参数索引和jdbc类型设置PreparedStatement语句中的值,即将?替换为实际值的`setParameter(PreparedStatement,int,T,JdbcType)`. - 获取指定`ResultSet`中指定名称的列对应的值,并转换为相应的JAVA类型的`getResult(ResultSet,String)`。 - 获取指定`ResultSet`中指定索引下面的列对应的值,并转换为相应的JAVA类型的`getResult(ResultSet,int)`。 - 获取指定存储过程(`CallableStatement`)中指定索引下标对应的值,并转换为相应的JAVA类型的`getResult(CallableStatement,int`。 在`TypeHandlerRegistry`中,默认为我们注册了一些常用的类型转换处理器,具体注册代码可以参考`TypeHandlerRegistry`的无参构造方法。 在Mybatis的整个生命周期中,触发`TypeHandlerRegistry`无参构造的位置是`XMLConfigBuilder#XMLConfigBuilder(XPathParser , String, Properties)`中的`super(new Configuration())`。 `Configuration`中有一个`final`修饰的`typeAliasRegistry`变量,他在`Configuration`初始化过程中调用了`typeAliasRegistry`的无参构造。 ``` /** * 类型别名注册表,主要用在执行SQL语句的出入参以及一些类的简写 */ protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry(); ``` 虽然Mybatis默认为我们注册了大量常用的`TypeHandler`,但是当我们遇到一些复杂的自定义对象的时候往往还需要提供自己的类型转换处理器,比如自定义枚举转换处理器。 这时候,我们就需要将我们自定义的类型转换处理器注册到Mybatis中,这时候我们就可以采用`typeHandlers`来进行配置了。 在Mybatis中关于`typeHandlers`的DTD是如此定义的: ``` ``` 在`typeHandlers`下面分别出现零个或多个的`typeHandlers`或者`package`标签。 其中`typeHandler`有三个属性`javaType`,`jdbcType`以及`handler`,其中`javaType`和`jdbcType`是可选的,他们分别表示java类型和jdbc类型,`handler`属性是必填的,他表示类型转换处理器的类型,每个`typeHandler`元素对应一个`TypeHandler`实例的配置。 这三个参数均可以使用Mybatis的别名机制,因为对于他们的解析采用的是`BaseBuilder#resolveClass(String)`方法。 `package`元素只有一个必填的`name`属性,他表示用户需要注册`TypeHandler`的基础包名,Mybatis将会递归处理该基础包及其子包下所有可用的`TypeHandler`实例。 解析`typeHandlers`的入口位置在`XmlConfigBuilder#parseConfiguration(XNode)`中: ``` // 注册类型转换器 typeHandlerElement(root.evalNode("typeHandlers")); ``` 他委托给了方法`typeHandlerElement(XNode)`来完成`typeHandlers`的解析和注册工作: ``` // 解析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"); // 解析出java类型 Class javaTypeClass = resolveClass(javaTypeName); // 解析jdbc类型 JdbcType jdbcType = resolveJdbcType(jdbcTypeName); // 解析出类型转换处理器的类型 Class typeHandlerClass = resolveClass(handlerTypeName); // 注册类型处理器 if (javaTypeClass != null) { if (jdbcType == null) { typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { typeHandlerRegistry.register(typeHandlerClass); } } } } } ``` 在处理`typeHandlers`的子节点时,Mybatis将其分为了两类,一类是`package`元素的解析,另一类是`typeHandler`元素的解析。 ##### 解析package元素 `package`元素只有一个必填的`name`属性,他指向了需要整包注册类型转换处理器的基础包地址。 Mybatis在获取到该值之后,将该值委托给`TypeHandlerRegistry.register(String)`来完成整包注册的工作。 在`TypeHandlerRegistry.register(String)`中,首先使用`ResolverUtil`(一个用于定位指定路径中所有满足条件的可用类集合的工具类)查找出指定路径中所有`TypeHandler`的实现类,在排除掉所有的匿名类,接口,抽象类之后,将这些类委托给`register(Class)`方法继续处理. ``` // 注册指定包下所有的java类 public void register(String packageName) { ResolverUtil> resolverUtil = new ResolverUtil<>(); // 返回当前已经找到的所有TypeHandler的子类 resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName); Set>> 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); } } } ``` 在`register(Class)`中,首先获取类型转换处理器类上的`MappedTypes`注解( `MappedTypes`注解用于给类型处理器指定需要处理的JAVA类型,他有一个`Class[] value()`属性,用于指定处理的java类型集合)。 如果类型转换处理器上标注了`MappedTypes`注解,则会继续委托给方法`register(Class, Class)`来完成注册处理,否则则会交给方法`register(TypeHandler)`来处理。 在这里代码的处理出现了分支,且分支的调用链有点长,不便于记忆,但是不重要,这两个分支待会会殊途同归到一个方法上,我们先按顺序依次解析这两个分支,这里做一个标记,记着我们待会要回到这个方法上哟。 我们先处理第一个分支,即持有`MappedTypes`注解。 `register(Class, Class)`方法会在调用`getInstance(Class, Class)`获取到`TypeHandler`实例之后,继续委托给`register(Class,TypeHandler)`方法来完成注册操作。 `getInstance(Class, Class)`方法的作用是基于给定的java类型和类型转换器类型生成类型转换器实例: ``` /** * 根据java类型和类型转换处理器的类型获取类型转换处理器 * * @param javaTypeClass java类型 * @param typeHandlerClass 类型转换处理器类型 * @param java类型 */ @SuppressWarnings("unchecked") public TypeHandler getInstance(Class javaTypeClass, Class typeHandlerClass) { if (javaTypeClass != null) { try { // 获取类型转换处理器的参数为Class的构造方法 Constructor c = typeHandlerClass.getConstructor(Class.class); // 通过构造方法返回类型转换处理器的实例 return (TypeHandler) c.newInstance(javaTypeClass); } catch (NoSuchMethodException ignored) { // ignored } catch (Exception e) { throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e); } } try { // 获取类型转换处理器的无参构造 Constructor c = typeHandlerClass.getConstructor(); // 通过无参构造生成类型转换处理器的实例 return (TypeHandler) c.newInstance(); } catch (Exception e) { throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e); } } ``` `register(Class,TypeHandler)`方法比较有意思,他的作用是将`Class`上转型为`Type`类型之后继续将注册的操作委托给`register(Type javaType, TypeHandler typeHandler)`来处理。 这里`register(Type javaType, TypeHandler typeHandler)`方法就是我们刚才做标记时说的殊途同归的最终方法,此处先不讲解,而是继续处理刚才的第二个分支:不包含`MappedTypes`注解时的处理。 当指定的`TypeHandler`类上不包含`MappedTypes`注解时,Mybatis会首先调用`getInstance(Class, Class)`方法获取`TypeHandler`的实例,之后将方法委托给`register(TypeHandler)`来处理。 在`register(TypeHandler)`方法中,Mybatis首先会获取`MappedTypes`注解,这里需要和`register(Class)`方法区别出来,这里再次获取了`MappedTypes`注解,我跟踪了该方法的代码,其调用方除去单元测试只有`register(Class)`方法,但是在`register(Class)`中只有在没有`MappedTypes`注解的时候才会调用该方法,因此下面这一部分代码近乎算是多余的: ``` boolean mappedTypeFound = false; // 获取MappedTypes注解 MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class handledType : mappedTypes.value()) { // 根据注解,注册类型处理器 register(handledType, typeHandler); mappedTypeFound = true; } } ``` 为什么说是近乎而不是绝对,是因为他其实为直接使用`TypeHandler`对象注册类型转换器留了口子。 回到正题上来,`register(TypeHandler)`方法中针对`TypeHandler`的注册提供了三种场景: - 持有`MapperTypes`注解,依据`MapperTypes`注解的值调用`register(Type, TypeHandler)`执行注册操作。 - 没有`MapperTypes`注解但是`TypeHandler`的实现类继承`TypeReference`抽象类,获取`TypeHandler`的泛型值,然后使用泛型类型和`typeHandler`实例调用`register(Type, TypeHandler)`执行注册操作。 - 不满足以上两种条件,即没有`MapperTypes`也没有继承`TypeReference`抽象类,将会调用`register(Class, TypeHandler)`方法将null值,转换为Type类型,进而调用`register(Type, TypeHandler)`执行注册操作。 我们在了解`register(Type, TypeHandler)`方法之前,先了解一下`TypeReference`抽象类。 `TypeReference`是一个抽象类,他的作用是获取类的泛型引用,比如: ``` class TypeReferenceTest extends TypeReference{ } ``` `TypeReferenceTest`继承了`TypeReference`抽象类,并指定了泛型类型为`Integer`,当我们调用`TypeReferenceTest`实例的`getRawType()`方法时,我们将会获得`Integer.class`. Mybatis定义了一个名为`BaseTypeHandler`的抽象类,他继承了`TypeReference`抽象类,实现了`TypeHandler`接口,他是Mybatis中默认的类型处理器的基类。 `BaseTypeHandler`的定义如下: ``` public abstract class BaseTypeHandler extends TypeReference implements TypeHandler ``` 除此之外`BaseTypeHandler`实现了`TypeHandler`接口定义的默认方法,添加了基本的数据校验,之后暴露出了新的抽象方法供子类实现,同时他还添加了对Mybatis配置类`Configuration`的引用,并提供了对应的设值方法。 继续说`TypeHandlerRegistry`的`register(Type, TypeHandler)`方法,我们刚才说有一个殊途同归的方法,指的就是该方法,通过前面的处理无论怎样的场景,能够调用该方法,说明已经有了两个参数java类型(`Type`)和类型转换处理器实例(`TypeHandler`),虽然第一个java类型参数可能为`null`。 在进入该方法之后,会首先寻找`TypeHandler`实例上的`MappedJdbcTypes`注解。 > `MappedJdbcTypes`注解用于给TypeHandler指定处理的JdbcType,他有两个参数,其中JdbcType[]类型的value属性用于指定该TypeHandler处理的JDBC类型集合,boolean类型的includeNullJdbcType用于表示是否可以处理null类型。 如果获取到了`MappedJdbcTypes`中的值,则使用java类型,jdbc类型和类型处理器实例进行类型转换处理器的实例,同时判断`MappedJdbcTypes`中的`includeNullJdbcType`参数确定其是否可以处理null值。 如果没有获取到了`MappedJdbcTypes`中的值,则直接使用java类型,null,以及类型处理器实例注册。 上诉两个分支中涉及到的三个注册操作,均委托给了`register(Type, JdbcType, TypeHandler)`方法来执行真正的注册操作。 `register(Type, JdbcType, TypeHandler)`方法的代码如下: ``` /** * 注册类型处理器 * * @param javaType java类型 * @param jdbcType jdbc类型 * @param handler 处理器 */ private void register(Type javaType, JdbcType jdbcType, TypeHandler handler) { if (javaType != null) { // 获取该java类型对应的jdbc类型转换器的集合 Map> map = TYPE_HANDLER_MAP.get(javaType); if (map == null || map == NULL_TYPE_HANDLER_MAP) { map = new HashMap<>(); // 注册java类型和【jdbc和处理器的映射关系】的映射关系 TYPE_HANDLER_MAP.put(javaType, map); } map.put(jdbcType, handler); } // 注册处理器类型和实例的关系 ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler); } ``` 如果传入的java类型不是null,那么就先获取当前java类型对应的类型转换器映射集合,这个获取到的映射集合以jdbc类型为key,以类型转换器为value。如果没有获取到映射集合或者获取到一个`NULL_TYPE_HANDLER_MAP`的集合,那么就新建一个映射集合,并将java类型和该映射集合的关系注册到`TYPE_HANDLER_MAP`中,然后再往映射集合中注册jdbc类型和类型转换处理器的关系。 之后无论有无java类型,均会往`ALL_TYPE_HANDLERS_MAP`中注册类型处理器类型和类型处理器实例的映射关系。 > `NULL_TYPE_HANDLER_MAP`是一个空的映射集合定义。 > `TYPE_HANDLER_MAP`是存储java类型和【JDBC类型和类型转换处理器映射集合】映射关系的集合。 > `ALL_TYPE_HANDLERS_MAP`是存放处理器类型和处理器实例的映射关系的集合。 到此,经过一个稍微有些复杂的调用关系,就完成扫描基础包注册类型转换处理器实例的任务了。 再来回顾一下大致流程: 首先根据基础包获取所有需要注册的类型转换处理器,然后通过注解或者泛型获取类型转换处理器处理的java类型,然后将java类型上转型为`Type`,之后根据注解获取映射处理器处理的JDBC类型,最后注册类型转换处理器。 OK,到此`package`元素的解析过程就结束了。 ##### 解析typeHandler元素,直接注册类型转换处理器 对于`typeHandler`元素的解析相对于`package`来说流程要简单很多,他首先根据属性`javaType`,`jdbcType`和`handler`获取定义的java类型名称,jdbc类型名称以及类型处理器名称,之后委托给`BaseBuilder#resolve(String)`方法解析出真正的java类型,jdbc类型和类型转换处理器类型,再根据是否有java类型和jdbc类型判断出注册别名流程的切入点。 > 解析`TypeHandler`元素直接注册类型转换处理器的逻辑大部分都被基于`package`注册处理器的逻辑所包含,此处就不重复赘述了,简单的记一下大致流程。 - **有java类型有jdbc类型**,直接调用`register(Class javaTypeClass, JdbcType jdbcType, Class typeHandlerClass)`方法,在将typeHandlerClass转换为具体的实例之后,委托给方法`register(Class type, JdbcType jdbcType, TypeHandler handler)`将java类型上转型为`Type`类型,最后调用`register(Type javaType, JdbcType jdbcType, TypeHandler handler)`执行真正的操作。 - **有java类型无jdbc类型**,调用方法`register(Class javaTypeClass, Class typeHandlerClass)`在将typeHandlerClass转换为具体的实例之后,委托给方法`register(Class javaType, TypeHandler typeHandler)`将java类型上转型为`Type`类型,然后调用`register(Type javaType, TypeHandler typeHandler)`方法获取JDBC类型,最后调用`register(Type javaType, JdbcType jdbcType, TypeHandler handler)`执行真正的操作。 - **无java类型**,这种场景和基于`package`注册类型处理器的逻辑里面的扫到类型转换处理器之后的注册逻辑一致。 至此,整个`typeHandlers`元素的解析也已经完成,Mybatis的基础准备工作也准备的差不多了。 接下来就是解析Mybatis的Mapper配置文件。 ## 解析Mybatis的Mapper配置,初始化SQL环境的过程 在Mybatis中对于映射器(定义DAO操作的接口)的配置通常有两种方式,一种是基于注解的形式,一种是基于XML配置的方式。比如: 基于注解 ``` @Select({ "SELECT * FROM blog"}) @MapKey("id") Map selectBlogsAsMapById(); ``` 基于XML ``` List selectBlogsFromXML(); ``` 无论使用哪种方式,我们都需要告诉Mybatis到哪里去找到这些映射器和SQL语句的配置,这时候我们就需要使用Mybatis主配置文件中定义的`mappers`元素了。 `mappers`元素的DTD定义如下: ``` ``` `mappers`元素允许包含零个或多个`mapper`/`package`子元素。 其中`package`子元素有一个必填的`name`属性,他指向一个基础包名。 ``` ``` Mybatis在解析`package`子元素的时候会将包内所有符合条件的映射器接口全部注册为映射器。 而`mapper`子元素有三个属性,这三个属性必须有一个有值且不能同时存在。 `mapper`子元素的DTD定义如下: ``` ``` 其中: - `resource`表示使用相对类路径的资源(XML资源)引用 - `url`表示使用完全限定资源定位符的资源(XML资源)引用 - `class`表示使用映射器接口实现类的完全限定类名 对于这四种注册映射器的方式主要可以分为两种,一种是基于映射器实例的注册方式,另一种是基于XML文件的注册方式。 > `package`和`class`属于基于映射器实例的注册方式,`resource`和`url`属于基于XML文件的注册方式。 解析Mybaits主配置文件的`mappers`元素的入口在`XmlConfigBuilder#parseConfiguration(XNode root)`方法中的: ``` // !!注册解析Dao对应的MapperXml文件 mapperElement(root.evalNode("mappers")); ``` `mapperElement`方法定义如下: ``` /** * 解析配置文件中的mappers节点 * * @param parent mappers节点内容 */ private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 解析package元素,将包内的映射器接口实现全部注册为映射器 // 比如: // 判断 configuration>mappers>package 属性 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); // 根据包名加载所有的DAO操作类并注册 configuration.addMappers(mapperPackage); } else { // 解析mapper元素 // 判断 configuration>mappers>mapper 属性 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { // 解析使用相对类路径的资源引用 // 比如: // 只有resource ErrorContext.instance().resource(resource); // 读取指定的XML资源文件 InputStream inputStream = Resources.getResourceAsStream(resource); // 继续解析Mapper Xml文件 XMLMapperBuilder mapperParser = new XMLMapperBuilder( inputStream, /*XML文件输入流*/ configuration, /*用户全局配置*/ resource, /*资源配置文件的地址*/ configuration.getSqlFragments() /*已有的XML代码块*/ ); /*解析该XML文件*/ mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { // 解析使用完全限定资源定位符的资源引用 // 比如: // 只有url 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) { // 解析使用映射器接口实现类的完全限定类名 // 比如: // 只有mapperClass 直接添加对象 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."); } } } } } ``` 在获取到`mappers`元素的所有子节点之后,Mybatis会根据`mappers`子元素的类型执行不同的逻辑。 其中`package`元素会交给`Configuration#addMappers(String)`来完成映射器的批量注册。 ``` // 解析package元素,将包内的映射器接口实现全部注册为映射器 // 比如: // 判断 configuration>mappers>package 属性 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); // 根据包名加载所有的DAO操作类并注册 configuration.addMappers(mapperPackage); } ``` `mapper`元素则会判断根据存在的不同属性来执行不同的注册方式,其中如果只有`class`属性,则会调用`Configuration#addMapper(Class type)`方法来完成映射器的注册操作。 ``` if (resource == null && url == null && mapperClass != null) { // 解析使用映射器接口实现类的完全限定类名 // 比如: // 只有mapperClass 直接添加对象 Class mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } ``` 而`resource`和`url`属性的处理区别只在于加载XML文件的寻址方式不同而已,在获取到了XML文件的输入流之后,Mybatis会生成一个`XMLMapperBuilder`对象来完成映射器的注册操作。 - `resouce`: ``` if (resource != null && url == null && mapperClass == null) { // 解析使用相对类路径的资源引用 // 比如: // 只有resource ErrorContext.instance().resource(resource); // 读取指定的XML资源文件 InputStream inputStream = Resources.getResourceAsStream(resource); // 继续解析Mapper Xml文件 XMLMapperBuilder mapperParser = new XMLMapperBuilder( inputStream, /*XML文件输入流*/ configuration, /*用户全局配置*/ resource, /*资源配置文件的地址*/ configuration.getSqlFragments() /*已有的XML代码块*/ ); /*解析该XML文件*/ mapperParser.parse(); } ``` - `url` ``` if (resource == null && url != null && mapperClass == null) { // 解析使用完全限定资源定位符的资源引用 // 比如: // 只有url ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder( inputStream/*XML文件输入流*/ , configuration/*用户全局配置*/ , url/*资源配置文件的地址*/ , configuration.getSqlFragments()/*已有的XML代码块*/ ); /*解析该XML文件*/ mapperParser.parse(); } ``` 仔细看上诉`resource`和`url`的处理代码,两者几乎完全一致,唯一的区别只在于加载文件的方式: `resource`加载XML文件使用的是基于类加载路径的:`InputStream inputStream = Resources.getResourceAsStream(resource);` 而`url`加载XML文件使用的是基于URL的:`InputStream inputStream = Resources.getUrlAsStream(url);`. 两者对于XML资源文件的加载都是委托给了`Resources`来实现,`Resources`是Mybatis封装的一个简化资源访问的工具类. --- 回到主要代码逻辑上来,我们先解析`package`和`mapper#class`元素的加载过程。 在方法`configuration.addMappers(mapperPackage);`中,`Configuration`将批量注册映射的业务委托给了`Configuration#mapperRegistry`来处理。 `Configuration#mapperRegistry`属性是`MapperRegistry`的一个实例。 `MapperRegistry`是Mybatis中映射器(Dao操作对象)注册表,同时肩负着注册映射器的光荣任务。 在Mybatis解析`package`元素的时候调用`Configuration#addMappers(String package)`的时候,`Configuration`对象转手就把这个任务交给了`MapperRegistry`的`addMappers(String packageName)`来完成。 在`addMappers(String packageName)`中,Mybatis限定了作为映射器必须实现/继承的类对象为`Object`,这也意味着在加载映射器的过程中,指定包名下的所有java类型都会被加载,作为映射器的备选对象存在。 ``` /** * 根据基础包名,批量注册映射器 * * @since 3.2.2 */ public void addMappers(String packageName) { // 注册指定包下所有的java类 addMappers(packageName, Object.class); } ``` 在方法`addMappers(String packageName, Class superType)`中,Mybatis借助`ResolverUtil`工具类找出指定包名下所有Object的子类列表,将所有的子类依次交给`addMapper(mapperClass)`方法来完成映射器的注册操作。 ``` /** * 根据基础包名和父类/接口限制,批量注册映射器 * @since 3.2.2 */ public void addMappers(String packageName, Class superType) { ResolverUtil> resolverUtil = new ResolverUtil<>(); // 加载指定包下的继承/实现了指定类型的所有类 resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set>> mapperSet = resolverUtil.getClasses(); for (Class mapperClass : mapperSet) { // 执行映射器的注册操作 addMapper(mapperClass); } } ``` 到了方法`addMapper(mapperClass)`中,首先排除掉了非接口类型的class,之后再判断当前注册表中是否已经注册了该class对应的映射器,如果没有,才会进行解析和注册的操作。 > `MapperRegistry`用来维护所有映射器的注册表本质上一个名为`knownMappers`的map集合,他存放了所有已经注册了的映射器类型和代理实例的映射关系。 在解析之前,会将当前映射器放入到 `MapperRegistry`的注册表`knownMappers`中,避免重复解析。 之后`MapperRegistry`会生成一个`MapperAnnotationBuilder`对象来继续完成映射器的解析和加载过程。 如果在`MapperAnnotationBuilder`中解析加载映射器失败,MapperRegistry将会从注册表`knownMappers`中移除该映射器的映射关系。 注意下面这个方法,`mapper#class`元素的解析也是委托给该方法完成的。 ``` /** * 加载指定类型的映射器 * @param type 映射器类型 */ public void addMapper(Class type) { if (type.isInterface()) { // 只处理接口 if (hasMapper(type)) { // 只处理一次 throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 放入已知的代理映射中,mapper接口->MapperProxyFactory代理对象 knownMappers.put(type, new MapperProxyFactory(type)); // 在运行解析器之前添加类型非常重要. // 否则,映射器解析器可能会自动尝试绑定。 // 如果类型已知,则不会尝试 // 解析注解 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 执行解析 parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { // 移除解析出现异常的接口 knownMappers.remove(type); } } } } ``` 对于`mapper#class`元素的解析操作`configuration.addMapper(mapperInterface)`则由`Configuration`直接委托给了`MapperRegistry`对象的`addMapper(Class type)`方法来完成。 因此可见,解析`package`元素时前置的那些操作目的仅仅是为了获取**所有**需要注册的映射器的类型。 在获取到映射器类型之后,真正的解析操作就完全交给了`MapperAnnotationBuilder`来完成, 这里我们先不继续跟踪`MapperAnnotationBuilder`的解析过程,而是回到基于XML文件的注册方式的元素`resource`和`url`的解析上来。 ##### `resource`和`url`的解析 关于`resource`和`url`的解析,我们在前文有提到过两者的代码几乎完全一致,区别只在于一个是通过相对路径类获取类加载路径上的XML文件,一个是通过URL获取XML文件。 二者的处理流程都是获取`resource`/`url`的值,然后将该值委托给`Reources`对象获取XML文件的输入流,之后通过该输入流构建一个`XmlMapperBuilder`对象来解析Mapper文件。 ``` XMLMapperBuilder mapperParser = new XMLMapperBuilder( inputStream/*XML文件输入流*/ , configuration/*Mybatis用户全局配置*/ , url/*资源配置文件的地址*/ , configuration.getSqlFragments()/*已有的XML代码块*/ ); ``` 对于``XmlMapperBuilder``的构造器,我们唯一不熟悉的应该是`configuration.getSqlFragments()`,`Configuration#sqlFragments`是一个用来保存代码块映射的集合,具体作用在接下来的解析过程中我们会讲到。 `XmlMapperBuilder`是`BaseBuilder`的一个实现,他的作用主要是用来解析Mybatis的`mapper`xml文件。 `XmlMapperBuilder`在构造过程中生成了一个`XPathParser`解析器的实例。 > `XPathParser`解析器我们在前文中提到过,它使用SAX解析出输入流对应的XML的DOM树,并存放到XpathParser对象中。 ``` public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map sqlFragments) { this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver())/*构建一个XPath解析器*/, configuration,/*Mybatis配置*/ resource/*资源路径*/, sqlFragments/*现存的Sql代码块*/ ); } ``` 之后继续调用自身的其他构造方法来完成整个`XmlMapperBuilder`对象的初始化过程: ``` private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map sqlFragments) { super(configuration); this.builderAssistant = new MapperBuilderAssistant(configuration, resource);/*创建Mapper文件解析助手*/ this.parser = parser; this.sqlFragments = sqlFragments; this.resource = resource; } ``` `MapperBuilderAssistant`是用来解析`mapper`文件的工具助手,实际上对于`mapper`文件的解析操作大多数均由他来完成。 > `MapperBuilderAssistant`也是`BaseBuilder`的一个实例,关于`MapperBuilderAssistant`更多的细节,我们在后面会逐步介绍。 完成`XmlMapperBuilder`的构造工作之后,Mybatis开始调用`XmlMapperBuilder#parse()`方法对`mapper`文件进行解析。 ``` /*解析该XML文件*/ mapperParser.parse(); ``` 在`XmlMapperBuilder`的`parse()`方法中,首先调用`Configuration`对象的`isResourceLoaded(String resource)`方法校验指定的资源是否已经被处理过,如果没有处理过将会对该文件执行完全的解析过程。 > `Configuration`对象的`Set loadedResources`属性用来维护所有已经加载过的资源集合,主要目的是为了防止重复解析`mapper`文件。 如果指定的mappe文件之前未被处理过,那么Mybatis将会使用`XPathParser`解析器将`mapper`文件的`mapper`根元素下的所有内容解析成`XNode`对象实体,用来完成xml文件的解析。 ``` /** * MapperXml文件的解析入口方法 */ public void parse() { if (!configuration.isResourceLoaded(resource)) { // 配置文件为第一次加载时才会执行完整的解析操作 // 读取并配置MapperXml文件的内容 核心逻辑 configurationElement(parser.evalNode("/mapper")); // 记录已加载当前的配置文件 configuration.addLoadedResource(resource); // 绑定DAO操作接口和当前配置的关系 bindMapperForNamespace(); } // 解析未完成处理的ResultMap parsePendingResultMaps(); // 解析未完成处理的缓存引用 parsePendingCacheRefs(); // 解析未完成处理的语句 parsePendingStatements(); } ``` 在获取到`mapper`文件对应的`XNode`对象之后,Mybatis开始调用`configurationElement(XNdoe context)`方法来解析`XNode`对象,完成一个映射器的配置。 Mybatis的`mapper`XMl文件的根元素为`mapper`,他有一个必填的`namespace`属性,该属性用来指定当前`mapper`文件对应的命名空间,这个命名空间的值在整个Mybatis中是唯一的,通常我们将其定义为映射器操作接口的全限定名称,交由Mybatis来自动帮我们绑定`mapper`xml文件内容与DAO操作接口(映射器)的关系. `mapper`元素下面定义了九个元素: - `cache-ref` 配置引用其他命名空间的缓存. - `cache` 配置缓存. - `resultMap` 配置返回结果映射集合,定义如何将数据库的列值转换为java对象. - `parameterMap` 配置请求参数映射集合,该参数目前已经弃用了. - `sql` 定义SQL代码块,可以用来被其他语句引用. - `insert` 定义插入语句 - `update` 定义更新语句 - `delete` 定义删除语句 - `select` 定义查询语句 针对这些子元素的解析过程和具体作用,我们会在后面的文章中具体讲解。 我们先看对`mapper`元素的解析操作。 当解析`mapper`元素时,Mybatis首先会获取当前`mapper`元素对应的`namespace`(命名空间)属性的值,`namespace`的值非常重要, 他用来在整个Mybatis环境中唯一标志一个`mapper`元素,同时`mapper`子元素唯一标志的生成也依赖于该值,正是因为`namespace`的存在,Mybatis才实现了跨XML文件引用的功能。 在拿到`namespace`的值之后,Mybatis会用它来初始化`XmlMapperBuilder`绑定的`MapperBuilderAssistant`的命名空间,因为接下来的解析工作将会依赖于该值。 ``` /** * 解析配置mapper节点 * * @param context mapper节点 */ private void configurationElement(XNode context) { try { // 获取当前配置文件的命名空间(工作空间),通常这个值我们会设置为DAO操作类的全限定名称 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } // 配置当前的命名空间(工作空间) builderAssistant.setCurrentNamespace(namespace); // 解析缓存引用 cacheRefElement(context.evalNode("cache-ref")); // 解析缓存配置,并给当前命名空间配置一个缓存,默认情况下Mybatis使用PerpetualCache cacheElement(context.evalNode("cache")); // 解析并注册parameterMap元素 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析并注册resultMap元素 resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析并注册Sql元素,此处只是简单的将所有的SQL片段读取出来,然后放到{@link Configuration#sqlFragments}中, // 不会执行太多额外的操作 sqlElement(context.evalNodes("/mapper/sql")); // 构建声明语句(CRUD) buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } ``` 在上文中`builderAssistant.setCurrentNamespace(namespace)`方法中对命名空间的值做了基础校验: ``` /** * 配置Mybatis Mapper文件解析助手的命名空间 * @param currentNamespace 命名空间 */ public void setCurrentNamespace(String currentNamespace) { if (currentNamespace == null) { throw new BuilderException("The mapper element requires a namespace attribute to be specified."); } // 校验命名空间,如果当前已经定义了命名空间的名称,且和传入的命名空间不一致,将会触发异常。 if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) { throw new BuilderException("Wrong namespace. Expected '" + this.currentNamespace + "' but found '" + currentNamespace + "'."); } // 赋值 this.currentNamespace = currentNamespace; } ``` #### Mybatis中跨命名空间的缓存引用 当Mybatis完成命名空间的处理之后,会首先解析`cache-ref`子元素的内容。 在此之前,我们先了解一下`MapperBuilderAssistant`Mapper文件解析助手的一些基本情况: ``` /** * 正在解析的MapperXml文件的命名空间(工作空间) */ private String currentNamespace; /** * 对应的资源文件 */ private final String resource; /** * 当前使用的缓存对象 */ private Cache currentCache; /** * 当前存在未解析的缓存引用 */ private boolean unresolvedCacheRef; // issue #676 ``` 他的方法很多,但是参数很少只有短短的五个参数: - `currentNamespace` 当前助手解析的XML文件的全局唯一标志 - `resource` 当前助手解析的xml文件的地址 - `currentCache` 当前助手解析XML文件过程中确定该Mapper使用的缓存对象 - `unresolvedCacheRef` 当前助手在解析过程中是否存在无法解析的缓存引用 - ok,我们可以开始解析Mybatis对Mapper文件的元素处理过程了。 `cache-ref`元素在Mybatis中用于配置引用其他命名空间的缓存对象,关于缓存对象,我们会在后面的文章中详细介绍。 `cache-ref`元素的DTD定义如下: ``` ``` `cache-ref`只有一个必填的`namespace`属性,该属性的值是一个在Mybatis中定义的缓存对象的全局唯一标志,该标志的生成方法在后续文章中会给出。 关于`cache-ref`元素的解析相对比较简单,Mybatis通过`XPathParser`从`mapper`元素中拿到`cache-ref`的`XNode`定义之后,会将当前命名空间和被引用缓存的唯一标志注册到`Configuration`的缓存引用注册表`cacheRefMap`中,之后使用当前的映射器构建助手`MapperBuilderAssistant`和被引用缓存的全局唯一标志生成一个缓存引用解析器`CacheRefResolver`的实例,之后通过缓存引用解析器的`resolveCacheRef()`方法完成缓存引用的处理。 如果再`resolveCacheRef()`方法中发生了未完成元素解析操作异常(`IncompleteElementException`)则会在`Configuration`中注册一个未完成缓存引用解析的`CacheRefResolver`实例。 ``` // 解析缓存引用 cacheRefElement(context.evalNode("cache-ref")); ``` ``` /** * 解析cache-ref 节点 * * @param context cache-ref节点 */ private void cacheRefElement(XNode context) { if (context != null) { // 处理引用其他命名空间的缓存 // 添加缓存引用 configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); // 构建缓存引用对象 CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); try { // 解析引用的缓存配置,为builderAssistant设置使用的缓存实例 cacheRefResolver.resolveCacheRef(); } catch (IncompleteElementException e) { // 如果引用的缓存不存在(可能是还未加载),那么将会添加一个未完成的引用标记,之后会调用该方法完成补偿 // 添加一个尚未完成的缓存引用标记 configuration.addIncompleteCacheRef(cacheRefResolver); } } } ``` 在上文中提到了缓存引用解析器`CacheRefResolver `,`CacheRefResolver `是一个相对比较简单的对象,他只有两个属性和两个方法。 '其中属性`MapperBuilderAssistant assistant`表示解析当前XML文件的映射构建助手,`String cacheRefNamespace`则表示待引用缓存的全局唯一标志。 这两个参数都是在构造方法中完成赋值的,而剩下唯一一个用于解析引用缓存的方法`resolveCacheRef()`,则把真正的解析操作委托给了`MapperBuilderAssistant`的`useCacheRef(String namespace)`方法来完成。 ``` /** * 解析引用的缓存 * * @return 缓存 */ public Cache resolveCacheRef() { // 委托给缓存引用助手完成缓存引入的配置 return assistant.useCacheRef(cacheRefNamespace); } ``` 在`MapperBuilderAssistant`的`useCacheRef(String namespace)`方法中,Mybatis首先会从`Configuration`对象的缓存对象注册表`caches`中取出待引用的缓存对象,如果待引用的缓存对象存在,则赋值给`MapperBuilderAssistant`当前使用的缓存字段`currentCache`,并给`unresolvedCacheRef`赋值为`true`,表示持有引用的缓存对象,如果没有获取到缓存对象,则给`unresolvedCacheRef`赋值为`false`,并抛出一个未完成元素解析操作异常(`IncompleteElementException`)。 ok,到这缓存引用的解析就完成了,接下来是缓存配置的解析。 在Mybatis`mapper`xml文件中配置一个缓存使用的是`cache`元素,`cache`元素的DTD定义如下: ``` ``` `cache`元素下可以配置零个或多个`proerty`元素,同时`cache`元素有六个非必填的属性。 其中: - `type`表示使用的缓存实例类型。 - `eviction`表示使用的缓存回收策略。 - `flushInterval`表示缓存刷新的时间间隔,单位为MS。 - `size`表示缓存可用内容的大小 - `readOnly`表示缓存是否为只读 - `blocking`表示缓存是否为阻塞性的。 持有的`property`元素集合用来配置用户对缓存的自定义配置。 对于`cache`元素的解析,在`XMLMapperBuilder#configurationElement(XNode context)`中调用` cacheElement(context.evalNode("cache"))`开始执行解析工作。 `cacheElement(XNode context)`会先获取`cache`的`type`属性,该属性用来标记当前映射器使用的缓存类型,`type`值可以使用Mybatis的别名机制,同样在`Configutaion`的无参构造中注册了一个名为`PERPETUAL`的永久缓存。 在获取到想要使用的缓存类型名称之后,会使用`TypeAliasRegistry#resolveAlias(String string)`来解析出缓存的实例类型,默认使用是名为`PERPETUAL`的永久缓存。 确定了缓存类型之后,Mybatis会获取`cache`的`evication`属性值,该值用于确定使用的缓存回收机制,同样,`evication`的值也可以使用Mybatis的别名机制,Mybatis在`Configutaion`的无参构造中默认提供了四种缓存回收机制及其别名: - `LRU`,最近最少使用的:移除最长时间不被使用的对象。 - `FIFO`,先进先出:按对象进入缓存的顺序来移除它们。 - `SOFT`,软引用:移除基于垃圾回收器状态和软引用规则的对象。 - `WEAK`,弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。 同样在获取到想要使用的缓存回收策略类型名称之后,通过`TypeAliasRegistry#resolveAlias(String string)`来解析出缓存回收测控也的的实例类型,默认使用的是名为`LRU`的缓存回收策略。 在确定了缓存类型以及缓存回收策略之后,就开始依次读取一些参数性的属性。 首先获取定义缓存刷新时间间隔的配置`flushInterval`,之后获取定义缓存可用内存大小的`size`属性,然后再获取用于定义缓存类型(是否为只读)的属性`readOnly`(默认为`flase`),接着在读取用户配置缓存阻塞性质的属性`blocking`(默认为`flase`),最后读取`cache`下的用户自己参数集合`property`。 > 需要注意的是对于`readOnly`属性的处理,前面加了一个非(!),因为该属性实际对应的是`CacheBuilder`的`readWrite`属性,后面会讲到`CacheBuilder`. 在获取到这些属性之后,一窝蜂的交给了映射器构建助手`MapperBuilderAssistant`的`useNewCache(Class typeClass,Class evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props)`来完成缓存的配置。 ``` /** * 解析cache节点 * 配置缓存 */ private void cacheElement(XNode context) { if (context != null) { // 二级缓存,默认为PERPETUAL String type = context.getStringAttribute("type", "PERPETUAL"); // 解析出缓存实现类 Class typeClass = typeAliasRegistry.resolveAlias(type); // 缓存回收策略 // LRU – 最近最少使用的:移除最长时间不被使用的对象。 // // FIFO – 先进先出:按对象进入缓存的顺序来移除它们。 // // SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。 // // WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。 // 解析出缓存策略 String eviction = context.getStringAttribute("eviction", "LRU"); // 解析出缓存策略类 Class evictionClass = typeAliasRegistry.resolveAlias(eviction); // 设置刷新间隔 Long flushInterval = context.getLongAttribute("flushInterval"); // 可用内存大小 Integer size = context.getIntAttribute("size"); // 是否为只读缓存,主要读取参数前面的'!'。 boolean readWrite = !context.getBooleanAttribute("readOnly", false); // 阻塞缓存 boolean blocking = context.getBooleanAttribute("blocking", false); // 配置用户自定义参数 Properties props = context.getChildrenAsProperties(); // 使用一个新的缓存 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } } ``` `MapperBuilderAssistant`的`useNewCache`方法,在拿到参数之后,将这些参数交由`CacheBuilder`来创建一个缓存对象,之后Mybatis将该缓存对象的实例注册到`Configuration`的缓存对象注册表(`caches`)中,最后再将生成的缓存对象赋值给当前映射器构建助手的`currentCache`属性,这也就意味着`chache`标签配置的缓存优先级要比使用`cache-ref`标签配置的缓存高。 当然这并不意味着`cache-ref`配置的缓存永远不会生效,因为在`cacheElement(XNode context)`方法处理`cache`标签的开头有有一行代码,用于判断用户是否配置了`cache`标签。 ``` private void cacheElement(XNode context) { if (context != null) { // do parse cache element... } } ``` 我们刚才说到`cache`对象实际上是有`CacheBuilder`来创建的,`CacheBuilder`是一个用于创建`Cache`对象的建造器,当然从名字上也能看出来这是一个建造者模式的使用。 `CacheBuilder`对象有下面这么几个属性: ``` /** * 缓存全局唯一的标志 */ private final String id; /** * 缓存实现类型 */ private Class implementation; /** * 缓存包装器 */ private final List> decorators; /** * 缓存大小 */ private Integer size; /** * 缓存刷新间隔,单位为MS */ private Long clearInterval; /** * 是否为可读可写缓存 */ private boolean readWrite; /** * 参数 */ private Properties properties; /** * 是否阻塞 */ private boolean blocking; ``` 这些属性对应着在使用`CacheBuilder`时允许使用的参数。 当然他的核心代码还是在`build()`方法中,我们回顾一下映射器构建助手中使用`CacheBuilder`的代码,代码中我将会给出每个参数的具体作用: ``` / 给当前的命名空间注册一个新的缓存 Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) /*配置缓存实例,默认PerpetualCache.class*/ .addDecorator(valueOrDefault(evictionClass, LruCache.class))/*配置缓存回收策略,默认LruCache.class*/ .clearInterval(flushInterval)/*配置缓存清理时间间隔*/ .size(size)/*配置缓存能够使用的内存大小*/ .readWrite(readWrite)/*配置缓存的读写能力*/ .blocking(blocking)/*配置缓存的阻塞性*/ .properties(props)/*配置缓存对应的自定义配置*/ .build(); ``` 接下来我们看一下`Cache`对象的构建过程。 在`CacheBuilder`的`build()`的方法中,首先会调用`setDefaultImplementations()`方法来配置默认的缓存实例,当然如果用户配置了缓存实例 该方法不会做任何事情,如果用户没配置缓存实例类型,那么就默认使用`PerpetualCache`,同时如果用户还刚好没配置缓存的回收策略 那么还会配置缓存的回收策略为`LruCache`。 ``` /** * 配置缓存的默认实现类 */ private void setDefaultImplementations() { if (implementation == null) { implementation = PerpetualCache.class; if (decorators.isEmpty()) { // 添加默认缓存回收策略 decorators.add(LruCache.class); } } } ``` 待处理完默认的缓存实现类之后,通过反射获取缓存实现类的实例,对于缓存实现类有一个要求,就是缓存实现类必须提供女 一个包含了String类型的单参构造函数,然后通过反射将用户自定义的有效参数设置到缓存实例中,对于是否为有效参数的判断取 决于对应的缓存实例中是否拥有用户定义的参数名称的setter方法。 在完成了用户自定义参数的注入之后,将会对缓存实例进行包装操作。 针对于`PerpetualCache`类型的缓存会使用现有的包装器集合`decorators`一次包装缓存实例,每次包装`Cache`实例都会尝试注入用户的自定义参数, 完成`decorators`包装器集合的包装之后再根据用户生成`CacheBuilder`时配置的参数进行再次包装。 - 如果用户传入了`size`参数,同时缓存中拥有`size`的setter方法,那么将会设置缓存的`size`的值为用户传入的值。 - 如果用户传入了`clearInterval`的参数,那么将会为缓存包装一层具有定时清理缓存功能的包装器,清理缓存的时间间隔 为`clearInterval`的值。 - 如果用户配置了`readWrite`参数,那么将会为缓存包装一层具有序列化功能的包装器。 - 直接为缓存包装一层提供日志功能的包装器。 - 直接为缓存包装一层提供同步锁的功能。 - 如果用户配置了`blocking`且值为`true`,那么将会为缓存的方法提供阻塞性。 在完成上诉包装流程之后,返回包装后的`PerpetualCache`缓存实例。 > 如果不了解上诉包装器的概念,可以了解一下设计模式中的装饰器模式。 针对于非`PerpetualCache`实例同时也不是`LoggingCache`的缓存,将会为其包装一层具有日志功能的包装器。 截止到这,缓存实例的创建已经完成。 ``` public Cache build() { // 配置缓存的默认实现类 setDefaultImplementations(); // 通过反射获取指定缓存的实例,并配置其唯一标志 Cache cache = newBaseCacheInstance(implementation, id); // 通过反射配置用户对于缓存的自定义参数 setCacheProperties(cache); // issue #352, do not apply decorators to custom caches if (PerpetualCache.class.equals(cache.getClass())) { // 添加缓存装饰器 for (Class decorator : decorators) { // 缓存装饰器实例 cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } // 为缓存添加标准的包装 cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { // 如果缓存不具有日志功能,包装日志功能 cache = new LoggingCache(cache); } return cache; } ``` 对`PerpetualCache`根据参数进行标准包装的方法: ``` private Cache setStandardDecorators(Cache cache) { try { // 获取使用配置使用缓存的元数据 MetaObject metaCache = SystemMetaObject.forObject(cache); if (size != null && metaCache.hasSetter("size")) { // 配置缓存允许使用的大小 metaCache.setValue("size", size); } if (clearInterval != null) { // 配置清理缓存的时间间隔 cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval); } if (readWrite) { // 配置为具有读写能力的缓存 cache = new SerializedCache(cache); } // 包装日志功能 cache = new LoggingCache(cache); // 包装同步锁功能 cache = new SynchronizedCache(cache); if (blocking) { // 为缓存方法提供阻塞性 cache = new BlockingCache(cache); } return cache; } catch (Exception e) { throw new CacheException("Error building standard cache decorators. Cause: " + e, e); } } ```