• Mybatis主配置—Configuration


      从《Mybatis引子—Mybatis简单使用》不难看出Mybatis应用是通过如下一个主配置文件拉起来的,其中所有属性的配置都能在Configuration类中找到属于自己的一席之地。

    
    DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
      <environments default="development">
        <environment id="development">
          <transactionManager type="JDBC"/>
          <dataSource type="POOLED">
            <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
          dataSource>
        environment>
      environments>
      <mappers>
        <package name="com.eugene.mybatis.follow"/>
      mappers>
    configuration>
    

      Mybatis配置信息主要分为Mybatis项目级配置信息(Configuration)和执行SQL语句的Mapper配置文件,那么什么是Mybatis项目级配置信息Configuration呐?Configuration的主要职责就是用于描述Mybatis的配置信息,其内部定义了大量用于控制Mybatis运行时行为的属性。其它组件可以通过调用Configuration对象的形式来获取配置信息。

    构造Configuration

      编写好一个Mybatis XML配置文件后可以通过使用SqlSessionFactoryBuilder类来构造一个Configuration类,在Mybatis源码中SqlSessionFactoryBuilder类提供了多个以重载编写的build方法。

    public class SqlSessionFactoryBuilder {
        
        public SqlSessionFactory build(Reader reader) {
            return build(reader, null, null);
        }
        
        /**
         * 省略多个build(Reader reader)重载方法
         */
        
        public SqlSessionFactory build(InputStream inputStream) {
            return build(inputStream, null, null);
        }
        
        /**
         * 省略多个build(InputStream inputStream)重载方法
         */
        
        public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
            try {
                XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
                return build(parser.parse());
            } 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.
                }
            }
        }
        
        public SqlSessionFactory build(Configuration config) {
            return new DefaultSqlSessionFactory(config);
        }
        
    }
    

      SqlSessionFactoryBuilder源码不难看出无论重载了多少个build方法,最终都会调用build(InputStream inputStream, String environment, Properties properties)方法来解析XML配置文件和构造Configuration对象。其入参分别为:

    1. InputStream inputStream:XML配置文件对应的文件流;
    2. String environment:指定环境变量;
    3. Properties properties:通过 Java 属性文件配置的属性值。

      其中inputStream用于解析XML配置,environment用于指定当前运行的环境信息,properties则提供某些属性的动态属性值。执行逻辑一共分为三步走:

    1. XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties),在XMLConfigBuilder构造函数中调用XPathParser的构造函数,其XPathParser利用Java XPath工具完成解析XML配置文件并返回一个操作XML 配置文件的XPathParser对象。
    public XMLConfigBuilder(
        InputStream inputStream, 
        String environment, 
        Properties props) {
        
        this(new XPathParser(
            inputStream, true, 
            props, new XMLMapperEntityResolver()), 
            environment, props);
    }
    
    private XMLConfigBuilder(
        XPathParser parser,
        String environment, 
        Properties props) {
        
        super(new Configuration());
        ErrorContext.instance().resource("SQL Mapper Configuration");
        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
    }
    
    1. 通过步骤一完成构造XMLConfigBuilder后,紧接着会调用XMLConfigBuilder的parse()方法来解析构造Configuration对象,parse方法中又会调用parseConfiguration方法来完成XML配置文件中各类属性的读取和解析,最终将读取和解析到属性和属性值一一对应的将其放入Configuration对象中以供后续使用。
     public Configuration parse() {
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }
    
      private void parseConfiguration(XNode root) {
        try {
          // issue #117 read properties first
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          loadCustomLogImpl(settings);
          typeAliasesElement(root.evalNode("typeAliases"));
          pluginElement(root.evalNode("plugins"));
          objectFactoryElement(root.evalNode("objectFactory"));
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          settingsElement(settings);
          // read it after objectFactory and objectWrapperFactory issue #631
          environmentsElement(root.evalNode("environments"));
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          typeHandlerElement(root.evalNode("typeHandlers"));
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
    
    1. 在完成解析XML配置文件和构造Configuration对象后执行finally代码块下的逻辑来关闭文件流。

    XML配置结构

      根据XMLConfigBuilder的parseConfiguration方法的显示,在XML配置文件中分别解析读取properties、 setting、 typeAliasess、plugins、objectFactory、objectWrapperFactory、reflectorFactory、environment、 databaseIdProvider、typeHandlers和mappers属性来完成configuration对象的赋值,其结构如下所示:

    • configuration
      • properties(属性)
      • settings
      • typeAliases
      • plugins
      • objectFactory
      • objectWrapperFactory
      • reflectorFactory
      • environments
        • environment(环境变量)
          • transactionManager(事务管理器)
          • dataSource(数据源)
      • databaseIdProvider
      • typeHandlers
      • mappers

    属性(properties)

      在真实的生产场景中往往希望配置是可以在外部进行配置,并可以进行动态替换。即可以简单理解为,将某些属性值配置到配置中心或某个外部文件中,然后在Mybatis XML配置文件中使用properties标签来进行动态替换。

    
    DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    
        <properties resource="com/eugene/mybatis/follow/config.properties">>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        properties>
    
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://127.0.0.1:3306/follow"/>
                    <property name="username" value="${username}"/>
                    <property name="password" value="${password}"/>
                dataSource>
            environment>
        environments>
       
    configuration>
    

      上述XML配置文件利用properties属性定义了一个username和一个password属性和属性值,然后在dataSource标签中以 u s e r n a m e 和 {username}和 username{password}的形式来赋值,这样一来就实现了username和password的动态赋值。除了在XML配置文件中以properties标签的形式来配置属性和属性值以外,还可以使用Java Properties类来配置这些属性,最后在调用SqlSessionFactoryBuilder类build方法的时候以参数的形式传入即可。

    执行过程

      根据XMLConfigBuilder的parseConfiguration方法所示,properties属性的解析读取是由propertiesElement(root.evalNode(“properties”));这段代码完成了。这段代码又分为两个部分,首先在之前处理好的XNode中读取出带有properties标签的XNode,然后交给propertiesElement方法来处理properties标签下的属性和属性值。

    private void propertiesElement(XNode context) throws Exception {
        if (context != null) {
          // 获取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) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
          } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
          }
          Properties vars = configuration.getVariables();
          if (vars != null) {
            defaults.putAll(vars);
          }
          parser.setVariables(defaults);
          configuration.setVariables(defaults);
        }
      }
    

      XMLConfigBuilder类propertiesElement方法的处理逻辑分为四个部分,

    1. 调用properties标签对应XNode的getChildrenAsProperties(),遍历解析获取properties标签下所有子标签的信息,如上文中username=root和password=123456。最后以Java Properties对象存储返回;
    2. 分别到properties标签中resource和url参数指定的位置去读取解析对应的配置信息,然后将得到的数据追加到Java Properties对象中存储;
    3. 将在调用SqlSessionFactoryBuilder类build方法时传入的属性配置信息追加到Java Properties对象中存储;
    4. 在完成所有解析存储工作之后,最后将完整的properties信息放入configuration对象必备后用。
    处理子标签

      调用properties标签对应XNode的getChildrenAsProperties()可以得到properties标签下所有子标签的信息,从XML配置文件可知子标签中的信息才是我们真正需要的数据。在Mybatis源码之中处理子标签需要经过,调用XNode#getChildrenAsProperties -> 触发调用XNode#getChildren -> 遍历获调用Xnode的构造函数 -> 触发调用XNode#parseAttributes在得到子标签中属性节点后 -> 遍历调用PropertyParser#parse方法来获取子标签配置的属性和属性值。

    • XNode#getChildrenAsProperties

      XNode#getChildrenAsProperties的源代码处理逻辑很简单,一共只完成了两件事:
      一是调用XNode#getChildren得到所有子标签的XNode;
      二是遍历所有子标签的XNode获取对应的name和value,来给Java Properties对象赋值。

    /**
     * 遍历读取properties标签下的所有子标签数据
     */
    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;
    }
    
    • XNode#getChildren

      XNode#getChildrenAsProperties触发调用getChildren方法来获取所有子标签的XNode,其getChildren源代码处理核心逻辑就是判断当前Node是ELEMENT_NODE(元素节点)则调用XNode的构造函数来构造XNode,并将构造后的XNode放入子标签XNode数组中。

    /**
     * 遍历读取properties标签下的所有信息,
     * 并以XNode的形式返回备用
     */
    public List<XNode> getChildren() {
        List<XNode> children = new ArrayList<>();
        NodeList nodeList = node.getChildNodes();
        if (nodeList != null) {
          for (int i = 0, n = nodeList.getLength(); i < n; i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
              children.add(new XNode(xpathParser, node, variables));
            }
          }
        }
        return children;
    }
    
    • XNode#parseAttributes

      XNode的构造函数中主要逻辑就是调用parseAttributes方法来解析读取Node节点里面的属性和属性值,所以parseAttributes方法源代码中的PropertyParser.parse(attribute.getNodeValue(), variables)是整个properties标签解析读取的关键所在。

    private Properties parseAttributes(Node n) {
        Properties attributes = new Properties();
        NamedNodeMap attributeNodes = n.getAttributes();
        if (attributeNodes != null) {
          for (int i = 0; i < attributeNodes.getLength(); i++) {
            Node attribute = attributeNodes.item(i);
            String value = PropertyParser.parse(attribute.getNodeValue(), variables);
            attributes.put(attribute.getNodeName(), value);
          }
        }
        return attributes;
    }
    
    • PropertyParser#parse

      PropertyParser#parse源代码对应的PropertyParser类定义了两个子标签属性一个是用于指定是否开启默认值读取的enable-default-value,另一个是用于定义默认值读取分隔符的default-value-separator。即我们可以在使用以下XML配置文件来增加默认值逻辑的处理,

    <properties>
      <property name="username" value="root"/>
      
      <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> 
    properties>
    
    <environments default="development">
      <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
          <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
          <property name="url" value="jdbc:mysql://127.0.0.1:3306/rainflower"/>
          <property name="username" value="${username}"/>
          
          <property name="password" value="${password:123456}"/>
        dataSource>
      environment>
    environments>
    

      这里以为例详细剖析PropertyParser是如何处理子标签下属性和属性:

    1. 调用PropertyParser#parse之后,首先会调用内部类VariableTokenHandler的构造函数构造VariableTokenHandler对象,构造的过程中会从当前Properties中获取enable-default-value和default-value-separator两个属性的值,如没有单独指定就是用系统默认值,否则就是用XML配置文件中所配置的数据;
    2. 完成构造内部类VariableTokenHandler之后,紧接着构造GenericTokenParser对象用于后续对子标签的解析和读取;
    3. 调用GenericTokenParser类的parse方法解析读取子标签数据信息。
    public static String parse(String string, Properties variables) {
        // 构造VariableTokenHandler内部类
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        return parser.parse(string);
    }
    
    private static class VariableTokenHandler implements TokenHandler {
        
        private VariableTokenHandler(Properties variables) {
          this.variables = variables;
          this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
          this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
        }
    
        private String getPropertyValue(String key, String defaultValue) {
          return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
        }
        
        @Override
        public String handleToken(String content) {
          if (variables != null) {
            String key = content;
            if (enableDefaultValue) {
              final int separatorIndex = content.indexOf(defaultValueSeparator);
              String defaultValue = null;
              if (separatorIndex >= 0) {
                key = content.substring(0, separatorIndex);
                defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
              }
              if (defaultValue != null) {
                return variables.getProperty(key, defaultValue);
              }
            }
            if (variables.containsKey(key)) {
              return variables.getProperty(key);
            }
          }
          return "${" + content + "}";
        }
    }
    

    加载顺序

      如果一个属性不只在一个地方进行了配置,那么MyBatis将按照下面的顺序来加载:

    • 首先读取在properties元素体内指定的属性。
    • 然后根据properties元素中的resource属性读取类路径下属性文件,或根据url属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
    • 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。

    因此通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是properties元素中指定的属性。

    设置(settings)

      按照XMLConfigBuilder类parseConfiguration的代码执行顺序,在解析读取完properties之后,紧接着会解析读取settings标签下定义的属性和属性值。settings对于MyBatis来讲是极为重要的设置,它们会改变 MyBatis 的运行时行为。

    <settings>
      <setting name="cacheEnabled" value="true"/>
      <setting name="lazyLoadingEnabled" value="true"/>
      <setting name="multipleResultSetsEnabled" value="true"/>
      <setting name="useColumnLabel" value="true"/>
      <setting name="useGeneratedKeys" value="false"/>
      <setting name="autoMappingBehavior" value="PARTIAL"/>
      <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
      <setting name="defaultExecutorType" value="SIMPLE"/>
      <setting name="defaultStatementTimeout" value="25"/>
      <setting name="defaultFetchSize" value="100"/>
      <setting name="safeRowBoundsEnabled" value="false"/>
      <setting name="mapUnderscoreToCamelCase" value="false"/>
      <setting name="localCacheScope" value="SESSION"/>
      <setting name="jdbcTypeForNull" value="OTHER"/>
      <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
    settings>
    

      上述就是部分settings标签的属性配置,其中各个属性具体代表着什么意思,可查阅Mybatis配置文档

    执行过程

      将XMLConfigBuilder类parseConfiguration方法中与settings无关的代码剔除后,就只剩下四句与其相关的代码,

    1. 遍历解析读取settings标签下所有的配置信息;
    2. 加载自定义的vfs实现类;
    3. 加载自定义的日志实现类;
    4. 依次设置XML配置文件中设置的属性,并对未显式设置的属性赋上默认值。
    private void parseConfiguration(XNode root) {
        try {
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            loadCustomVfs(settings);
            loadCustomLogImpl(settings);
            settingsElement(settings);
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
    

    loadCustomVfs

      loadCustomVfs见名知意就是加载自定义的vfs,vfs是一个能方便读取本地文件系统、FTP文件系统等系统里文件资源的一段程序代码,那么我们就可以通过该属性来配置加载自定义的虚拟文件系统应用程序。Mybatis源代码中提供了一个VFS(org.apache.ibatis.io.VFS)抽象类,该抽象类在提供服务的同时,也用于自定义vfs继承实现。

    private void loadCustomVfs(Properties props) throws ClassNotFoundException {
        String value = props.getProperty("vfsImpl");
        if (value != null) {
          String[] clazzes = value.split(",");
          for (String clazz : clazzes) {
            if (!clazz.isEmpty()) {
              @SuppressWarnings("unchecked")
              Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
              configuration.setVfsImpl(vfsImpl);
            }
          }
        }
      }
    

      loadCustomVfs会从之前解析完毕的settings信息中获取name等于vfsImpl的值,如果当前settings中有配置vfsImpl就会对其值用逗号将其分隔读取到完整的vfs配置,然后再依次遍历vfsImpl构造对应的vfsImpl对象,最后将其所得vfsImpl对象放入configuration对象中以备后用。

    loadCustomLogImpl

      loadCustomLogImpl直接从之前解析得到的Properties对象中获取name等于logImpl的值,然后将得到的logImpl实例化后,放入configuration对象以备后用。

    private void loadCustomLogImpl(Properties props) {
        Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
        configuration.setLogImpl(logImpl);
    }
    

    settingsElement

      settingsElement依次从Properties对象中获取XML配置文件中的setting配置数据,然后交给对应的程序处理之后放入configuration对象,如果遇到XML配置文件没有配置的属性则会利用之前约定好的默认值填充。

    private void settingsElement(Properties props) {
        configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
        configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
        configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
        configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
        configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
        configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
        configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
        configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
        configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
        configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
        configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
        configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
        configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
        configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
        configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
        configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
        configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
        configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
        configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
        configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
        configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
        configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
        configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
        configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
        configuration.setLogPrefix(props.getProperty("logPrefix"));
        configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
        configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
        configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
        configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false));
    }
    

    类型别名(typeAliases)

      如果想要给某一个类取一个别名,那么就可以使用typeAliases标签来为某一个Java类型设置一个别名。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如,以下配置就是给com.eugene.User类型设置一个user的别名,且这个user别名可以用于任何地方。

    <typeAliases>
      <typeAlias alias="user" type="com.eugene.User"/>
    typeAliases>
    

      也可以指定一个包名,MyBatis 会在包名下面搜索需要的Java Bean,且在指定包中找到的Java Bean,如果没有使用Alias注解来指定别名的话,那么会使用Java Bean首字母小写的非限定类名来作为它的别名,例如:

    <typeAliases>
      <package name="com.eugene"/>
    typeAliases>
    
    @Alias("user")
    public class User {
        ...
    }
    

    执行过程

      同样以parseConfiguration方法为切入点,从源代码不难看出在parseConfiguration方法中直接就调用了专门处理typeAliases标签的typeAliasesElement方法,其处理逻辑如下:

    1. 获取typeAliases标签下所有的子标签;
    2. 遍历子标签,如果当前子标签指定的是package,那么获取对应的包名。调用configuration的registerAliases方法注册对应包下所有的Java Bean;
    3. 遍历子标签,如果当前子标签是以alias和type关键字来配置的,那么先获取alias和type对应的值,然后实例化type对应Java Bean,最后以alias是否有值分别调用不同的registerAlias方法注册。
    private void parseConfiguration(XNode root) {
        try {
          typeAliasesElement(root.evalNode("typeAliases"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    
    private void typeAliasesElement(XNode parent) {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
              String typeAliasPackage = child.getStringAttribute("name");
              configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
              String alias = child.getStringAttribute("alias");
              String type = child.getStringAttribute("type");
              try {
                Class<?> clazz = Resources.classForName(type);
                if (alias == null) {
                  typeAliasRegistry.registerAlias(clazz);
                } else {
                  typeAliasRegistry.registerAlias(alias, clazz);
                }
              } catch (ClassNotFoundException e) {
                throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
              }
            }
          }
        }
    }
    

    扫描包注册

      以package关键字配置别名时会扫描所配置包下所有的Java Bean,结合下述源代码简单梳理得,

    1. 扫描package配置包名获得其中所有Java Bean;
    2. 遍历Java Bean依次调用registerAlias方法注册;
    3. 在registerAlias方法中,首先检查当前Java Bean是否含有Alias注解,如有获取其中配置的别名,否则就以非限定类名当做别名。
    public void registerAliases(String packageName) {
        registerAliases(packageName, Object.class);
    }
    
    public void registerAliases(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
        for (Class<?> type : typeSet) {
          // Ignore inner classes and interfaces (including package-info.java)
          // Skip also inner classes. See issue #6
          if (!type.isAnonymousClass() && 
              !type.isInterface() && 
              !type.isMemberClass()) {
            registerAlias(type);
          }
        }
    }
    
    public void registerAlias(Class<?> type) {
        String alias = type.getSimpleName();
        Alias aliasAnnotation = type.getAnnotation(Alias.class);
        if (aliasAnnotation != null) {
          alias = aliasAnnotation.value();
        }
        registerAlias(alias, type);
    }
    

    注册别名

      除了直接使用package关键字扫描注册别名之外,还可以使用alias和type关键字来配置别名。相比使用package关键字的形式,使用alias和type关键字可以直接指定某个Java Bean的别名,且如果alias没有配置也会检查当前Java Bean是否含有Alias注解,如有获取其中配置的别名,否则就以非限定类名当做别名。
      无论是以package关键字,还是以alias和type关键字来配置,最终都会走到registerAlias(String alias, Class value)方法来完成注册并放入configuration对象。

    public void registerAlias(String alias, Class<?> value) {
        if (alias == null) {
          throw new TypeException("The parameter alias cannot be null");
        }
        // issue #748
        String key = alias.toLowerCase(Locale.ENGLISH);
        if (typeAliases.containsKey(key) && 
            typeAliases.get(key) != null && 
            !typeAliases.get(key).equals(value)) {
          throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
        }
        typeAliases.put(key, value);
    }
    

    类型处理器(typeHandlers)

      MyBatis在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时,都会用类型处理器将获取到的值以合适的方式转换成Java 类型。Mybatis在提供大量默认的类型处理器的基础之上,也提供自定义的类型处理器,默认类型处理器详见Mybatis文档传送门
      想要实现自定义的类型处理器,可以重写已有的类型处理器,也可以实现org.apache.ibatis.type.TypeHandler接口或继承org.apache.ibatis.type.BaseTypeHandler来完成自定义类型处理器。并可以通过@MappedJdbcTypes和@MappedTypes,亦可以在标签中使用javaType和jdbcType属性来指定映射的Java类型和Jdbc类型。如下代码所示:

    @MappedJdbcTypes(JdbcType.VARCHAR)
    public class MyTypeHandler extends BaseTypeHandler<String> {
    
        @Override
        public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
            
        }
    
        @Override
        public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
            return null;
        }
    
        @Override
        public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
            return null;
        }
    
        @Override
        public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
            return null;
        }
    }
    
    
    <typeHandlers>
      <typeHandler handler="com.eugene.mybatis.follow.MyTypeHandler" javaType=“String”/>
    typeHandlers>
    

    执行过程

      在了解如何使用默认类型处理器和如何实现自定义类型处理器之后,一起看看类型处理器(typeHandlers)在Mybatis源代码中的是如何执行的。同样从XMLConfigurationBuilder类的parseConfiguration方法开始,

    private void parseConfiguration(XNode root) {
        try {       
            typeHandlerElement(root.evalNode("typeHandlers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
    

      从typeHandlerElement方法向下探发现typeHandlerElement方法承载着整个typeHandlers处理过程的调度作用:

    1. 遍历获取typeHandlers标签下所有的子标签;
    2. 如果当前子标签的名称等于“package”,那么则说明该子标签规定需要扫描指定包下的所有类,以找到满足条件的类型处理器;
    3. 如果当前子标签的名称不等于“package”,就进一步获取子标签中指定的javaType、jdbcType和handler三个属性的值,最后通过是否指定JavaType来分流到不同的类型处理器注册器。
    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 {
              String javaTypeName = child.getStringAttribute("javaType");
              String jdbcTypeName = child.getStringAttribute("jdbcType");
              String handlerTypeName = child.getStringAttribute("handler");
              Class<?> javaTypeClass = resolveClass(javaTypeName);
              JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
              Class<?> typeHandlerClass = resolveClass(handlerTypeName);
              if (javaTypeClass != null) {
                if (jdbcType == null) {
                  typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                } else {
                  typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                }
              } else {
                typeHandlerRegistry.register(typeHandlerClass);
              }
            }
          }
        }
      }
    

    注册类型处理器

      无论是扫描包下所有类,还是指定某个类型处理器,最后都会调用TypeHandlerRegistry类中的register方法,可见在TypeHandlerRegistry类中有多个register的重载方法。

    • register—包扫描

      Mybatis源代码在拿到指定packageName指定的包之后,首先找出该包下实现于TypeHandler接口的所有类,然后判断其不是接口,亦不是抽象类就调用register方法注册扫描到的类型处理器。

    public void register(String packageName) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
        resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
        Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
        for (Class<?> type : handlerSet) {
          //Ignore inner classes and interfaces (including package-info.java) and abstract classes
          if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
            register(type);
          }
        }
    }
    
    • register—注册

      虽然register重载的方法相当多,但是其大概可以分为两类,一类用于注册指定javaType的配置,一类用于注册仅指定handler的配置,最终会将所有类型处理器放入typeHandlerMap以备后用。

    public void register(String javaTypeClassName, String typeHandlerClassName) throws ClassNotFoundException {
        register(Resources.classForName(javaTypeClassName), Resources.classForName(typeHandlerClassName));
    }
    
    public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
       register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
    }
    
    public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
      register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
    }
    
    public void register(Class<?> typeHandlerClass) {
        boolean mappedTypeFound = false;
        MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
        if (mappedTypes != null) {
          for (Class<?> javaTypeClass : mappedTypes.value()) {
            register(javaTypeClass, typeHandlerClass);
            mappedTypeFound = true;
          }
        }
        if (!mappedTypeFound) {
          register(getInstance(null, typeHandlerClass));
        }
    }
    

    对象工厂(objectFactory)

      Mybatis每次在处理SQL返回结果时都会为其创建一个新的实例,且它都会使用对象工厂(ObjectFactory)来完成实例化工作。同样Mybatis在提供默认对象工厂的基础之上,也提供了自定义对象工厂来实例化实例。默认对象工厂要么通过默认无参构造方法,要么通过参数映射来调用带参的构造方法。自定义对象工厂则需要通过继承DefaultObjectFactory来完成自定义动作,然后通过objectFactory标签完成自定义对象工厂的注册。如下:

    <objectFactory type="com.eugene.MyObjectFactory">
      <property name="" value=""/>
    </objectFactory>
    

    执行过程

      同样还是从XMLConfigurationBuilder类的parseConfiguration方法开始,由此可见,对象工厂主要使用objectFactory标签来指定其自定义对象工厂并完成注册。

    private void parseConfiguration(XNode root) {
        try {
          objectFactoryElement(root.evalNode("objectFactory"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    
    private void objectFactoryElement(XNode context) throws Exception {
        if (context != null) {
          String type = context.getStringAttribute("type");
          Properties properties = context.getChildrenAsProperties();
          ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
          factory.setProperties(properties);
          configuration.setObjectFactory(factory);
        }
    }
    

      从objectFactoryElement方法可以看出自定义对象工厂的处理逻辑相对简单,

    1. 如果objectFactory标签对应配置不为空,就获取objectFactory标签对应的type属性;
    2. 获取objectFactory标签下通过name和value子标签配置所有的属性值;
    3. 将type属性配置的类进行实例化,然后将步骤二获取到的所有属性值放入已经已经实例好的对象工厂中;
    4. 放入configuration对象中的objectFactory属性以备后用。

    插件(plugins

      Mybatis允许在SQL语句执行过程中的某一阶段进行切面拦截处理,其允许使用插件来拦截的方法有Executor、ParameterHandler、ResultSetHandler和StatementHandler,作用于这些方法的插件切面拦截可以改变SQL语句执行的参数、结果等其它一些行为。

    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    • ParameterHandler (getParameterObject, setParameters)
    • ResultSetHandler (handleResultSets, handleOutputParameters)
    • StatementHandler (prepare, parameterize, batch, update, query)

      可以通过一下示例简单了解其使用:

    @Intercepts({@Signature(
      type= Executor.class,
      method = "update",
      args = {MappedStatement.class,Object.class})})
    public class MyPlugin implements Interceptor {
      
      Object intercept(Invocation invocation) throws Throwable{
          
      }
    
      default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    
      default void setProperties(Properties properties) {
        // NOP
      }
    }
    
    <plugins>
      <plugin interceptor="com.eugene.MyPlugin">
        <property name="" value=""/>
      plugin>
    plugins>
    

    执行过程

      同样还是从XMLConfigurationBuilder类的parseConfiguration方法开始,plugins的处理逻辑交给了pluginElement方法

    private void parseConfiguration(XNode root) {
        try {        
            pluginElement(root.evalNode("plugins"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    
    private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
          }
        }
    }
    

      pluginElement在拿到plugins标签配置的XNode信息之后,

    1. 遍历plugins标签下所有的子标签,然后获取interceptor属性值;
    2. 获取plugin标签下以name和value子标签配置所有的属性值;
    3. 实例化interceptor指定的类,并将步骤二获取到的值放入实例化后的对象;
    4. 将拦截器放入configuration对象以备后用。

    环境配置(environments)

      Mybaits提供支持多环境变量的配置,

    <environments default="development">
      <environment id="development">
        <transactionManager type="JDBC">
          <property name="..." value="..."/>
        transactionManager>
        <dataSource type="POOLED">
          <property name="driver" value="${driver}"/>
          <property name="url" value="${url}"/>
          <property name="username" value="${username}"/>
          <property name="password" value="${password}"/>
        dataSource>
      environment>
    environments>
    

    执行过程

      同样还是从XMLConfigurationBuilder类的parseConfiguration方法开始,environments的处理逻辑交给了environmentsElement方法。

    private void parseConfiguration(XNode root) {
        try {
            environmentsElement(root.evalNode("environments"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    
    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);
              configuration.setEnvironment(environmentBuilder.build());
              break;
            }
          }
        }
    }
    

      environmentsElement方法在没有获取environments配置时将会获取Mybatis默认值,如有配置就会按照下述步骤处理:

    1. 获取environment标签中id属性指定的值,用于确定当前属于哪一个环境;
    2. 获取transactionManager标签指定的值,确定当前环境的事务管理器;
    3. 获取dataSource标签指定的值,确定当前环境的数据源配置;
    4. 使用数据源和事务管理器构造当前环境的Environment对象,放入configuration以备后用。

    映射器(mappers)

      Mybatis通过定义大量的Mapper文件来映射SQL语句,因此也就需要告诉Mybatis在哪里去找对应的Mapper文件。

    <mappers>
      <package name="com.eugene.mybatis.follow"/>
    mappers>
    

    执行过程

      同样还是从XMLConfigurationBuilder类的parseConfiguration方法开始,mappers的处理逻辑交给了mapperElement方法。

    private void parseConfiguration(XNode root) {
        try {
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    
    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
              String mapperPackage = child.getStringAttribute("name");
              configuration.addMappers(mapperPackage);
            } else {
              String resource = child.getStringAttribute("resource");
              String url = child.getStringAttribute("url");
              String mapperClass = child.getStringAttribute("class");
              if (resource != null && url == null && mapperClass == null) {
                ErrorContext.instance().resource(resource);
                try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
                  XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                  mapperParser.parse();
                }
              } else if (resource == null && url != null && mapperClass == null) {
                ErrorContext.instance().resource(url);
                try(InputStream inputStream = Resources.getUrlAsStream(url)){
                  XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                  mapperParser.parse();
                }
              } else if (resource == null && url == null && mapperClass != null) {
                Class<?> mapperInterface = Resources.classForName(mapperClass);
                configuration.addMapper(mapperInterface);
              } else {
                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
              }
            }
          }
        }
    }
    

      mapperElement在获取到mappers标签的XNode之后,

    1. 遍历获取mappers下子标签,如果是package子标签就获取name属性对应的值,然后将其放入configuration对象;
    2. 如果不是package子标签,就分别resource、url和class属性所配置的值,
      1. resource,使用相对于类路径的资源引用,表示具体的哪个XML Mapper配置文件
      2. url,使用完全限定资源定位符(URL),表示具体的哪个XML Mapper配置文件
      3. class,使用映射器接口实现类的完全限定类名,表示具体的哪个Java类
    3. 如果是resource和url,首先构造一个文件流,然后使用这个文件流构造XMLMapperBuilder对象,最后通过XMLMapperBuilder对象构造Mapper;
    4. 如果是class,就直接实例化一个对象放入configuration对象中的MapperRegistry属性中。

    XMLMapperBuilder

      XMLMapperBuilder可以在得到resource和url配置的XML Mapper配置文件后,将其内容解析构造成具体的Mapper放入configuration对象中的MapperRegistry属性中,具体由parse方法完成。

    public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          bindMapperForNamespace();
        }
    
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }
    
    1. 判断当前资源是否已经加载过,如果已经被加载过就跳过加载过程,反之执行加载逻辑;
    2. configurationElement(parser.evalNode(“/mapper”));解析当前XML Mapper配置文件中的parameterMap、resultMap和select|insert|update|delete等信息
    3. 加载当前resource和url配置的XML Mapper配置,
      1. 获取当前namespace,如果不为空就调用Resources类中的classForName方法构造对应类的对象
      2. 如果当前Mapper类在configuration对象中的MapperRegistry属性中不存在,就将其放入
      3. 最后将namespace放入标识已经加载过的loadedResources属性中,标识该namespace已经被加载
    private void bindMapperForNamespace() {
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
          Class<?> boundType = null;
          try {
            boundType = Resources.classForName(namespace);
          } catch (ClassNotFoundException e) {
            // ignore, bound type is not required
          }
          if (boundType != null && !configuration.hasMapper(boundType)) {
            // Spring may not know the real resource name so we set a flag
            // to prevent loading again this resource from the mapper interface
            // look at MapperAnnotationBuilder#loadXmlResource
            configuration.addLoadedResource("namespace:" + namespace);
            configuration.addMapper(boundType);
          }
        }
    }
    
    1. 最后依次调用parsePendingResultMaps、parsePendingCacheRefs和parsePendingStatements方法,解析在步骤二中从XML Mapper配置文件里获取到的resultMap、Cache和Statements配置信息,最后将所有的信息放入configuration以备后用。
  • 相关阅读:
    (yum+内网)centos7两种方式安装jdk11
    MySQL运维8-Mycat分库分表之范围分片
    C++:C++三方库创建web程序
    【EDA课程设计】FPGA交通信号灯的设计(含动画视频、超详细思路/步骤分析、完整代码与效果详解)
    力扣每日一题-第25天-495.提莫攻击
    方案丨如何通过TSINGSEE青犀防溺水AI算法,实现水域的智能监管与风险预警?
    LeetCode_266_回文排列
    HTML笔记-狂神
    Qt学习14 计算器核心解析算法 (下)
    若依前端-应用路径发布和使用
  • 原文地址:https://blog.csdn.net/Jas000/article/details/126942897