转载

MyBatis启动之XMLConfigBuilder解析配置文件(二)

MyBatis启动之XMLConfigBuilder解析配置文件(二)

前言

MyBatis启动之XMLConfigBuilder解析配置文件(二)

XMLConfigBuilderBaseBuilder (解析中会涉及到讲解)的其中一个子类,它的作用是把MyBatis的XML及相关配置解析出来,然后保存到 Configuration 中。本文就解析过程按照执行顺序进行分析,掌握常用配置的解析原理。

使用

调用 XMLConfigBuilder 进行解析,要进行两步操作,上篇文章中【MyBatis之启动分析(一)】有提到。

实例化 XMLConfigBuilder 对象。

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;
      }
复制代码

实例化 Configuration

通过 new Configuration() 的方式实例化:

MyBatis启动之XMLConfigBuilder解析配置文件(二)
typeAliasRegistry 是一个类型别名注册器,实现原理就是维护一份 HashMap ,别名作为 key ,类的全限定名作为 value 。这里将框架中使用的类注册到类型别名注册器中。 TypeAliasRegistry.registerAlias

代码如下:

public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748
    //  在验证是否存在key和保存kv前,统一将key转换成小写
    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 为定义的一个HashMap
    TYPE_ALIASES.put(key, value);
    }
复制代码

在实例化 Configuration 类过程中,在该类里除了实例化了 TypeAliasRegistry 还实例化了另外一个下面用到的的类:

// 类型处理器注册器
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
复制代码

TypeHandlerRegistryTypeAliasRegistry 实例化逻辑相似,里面注册了一些常用类型和处理器,代码易懂。 TypeHandlerRegistry 的属性

// jdbc类型和TypeHandler的映射关系,key必须是JdbcType的枚举类型,读取结果集数据时,将jdbc类型转换成java类型
    private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(JdbcType.class);
    // Java类型与JdbcType类型的键值对,存在一对多的映射关系
    private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<Type, Map<JdbcType, TypeHandler<?>>>();
    // 没有相应的类型处理器时,使用的处理器
    private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
    // 类型处理器类类型和类型处理器的映射关系
    private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<Class<?>, TypeHandler<?>>();
    // 空处理器的值,用来做校验
    private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
    // 默认枚举类型处理器
    private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
复制代码

TypeHandlerRegistry 构造函数:

public TypeHandlerRegistry() {
        register(Boolean.class, new BooleanTypeHandler());
        register(boolean.class, new BooleanTypeHandler());
        register(JdbcType.BOOLEAN, new BooleanTypeHandler());
        register(JdbcType.BIT, new BooleanTypeHandler());
    
        register(Byte.class, new ByteTypeHandler());
        register(byte.class, new ByteTypeHandler());
        register(JdbcType.TINYINT, new ByteTypeHandler());
    
        register(Short.class, new ShortTypeHandler());
        register(short.class, new ShortTypeHandler());
        register(JdbcType.SMALLINT, new ShortTypeHandler());
    
        register(Integer.class, new IntegerTypeHandler());
        register(int.class, new IntegerTypeHandler());
        register(JdbcType.INTEGER, new IntegerTypeHandler());
    
        register(Long.class, new LongTypeHandler());
        register(long.class, new LongTypeHandler());
    
        register(Float.class, new FloatTypeHandler());
        register(float.class, new FloatTypeHandler());
        register(JdbcType.FLOAT, new FloatTypeHandler());
    
        register(Double.class, new DoubleTypeHandler());
        register(double.class, new DoubleTypeHandler());
        register(JdbcType.DOUBLE, new DoubleTypeHandler());
    
        register(Reader.class, new ClobReaderTypeHandler());
        register(String.class, new StringTypeHandler());
        register(String.class, JdbcType.CHAR, new StringTypeHandler());
        register(String.class, JdbcType.CLOB, new ClobTypeHandler());
        register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
        register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler());
        register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
        register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
        register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
        register(JdbcType.CHAR, new StringTypeHandler());
        register(JdbcType.VARCHAR, new StringTypeHandler());
        register(JdbcType.CLOB, new ClobTypeHandler());
        register(JdbcType.LONGVARCHAR, new ClobTypeHandler());
        register(JdbcType.NVARCHAR, new NStringTypeHandler());
        register(JdbcType.NCHAR, new NStringTypeHandler());
        register(JdbcType.NCLOB, new NClobTypeHandler());
    
        register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
        register(JdbcType.ARRAY, new ArrayTypeHandler());
    
        register(BigInteger.class, new BigIntegerTypeHandler());
        register(JdbcType.BIGINT, new LongTypeHandler());
    
        register(BigDecimal.class, new BigDecimalTypeHandler());
        register(JdbcType.REAL, new BigDecimalTypeHandler());
        register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
        register(JdbcType.NUMERIC, new BigDecimalTypeHandler());
    
        register(InputStream.class, new BlobInputStreamTypeHandler());
        register(Byte[].class, new ByteObjectArrayTypeHandler());
        register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
        register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
        register(byte[].class, new ByteArrayTypeHandler());
        register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
        register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
        register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
        register(JdbcType.BLOB, new BlobTypeHandler());
    
        register(Object.class, UNKNOWN_TYPE_HANDLER);
        register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
        register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
    
        register(Date.class, new DateTypeHandler());
        register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
        register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
        register(JdbcType.TIMESTAMP, new DateTypeHandler());
        register(JdbcType.DATE, new DateOnlyTypeHandler());
        register(JdbcType.TIME, new TimeOnlyTypeHandler());
    
        register(java.sql.Date.class, new SqlDateTypeHandler());
        register(java.sql.Time.class, new SqlTimeTypeHandler());
        register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());
    
        // mybatis-typehandlers-jsr310
        // 是否包含日期,时间相关的Api,通过判断是否加载java.time.Clock作为依据
        if (Jdk.dateAndTimeApiExists) {
          this.register(Instant.class, InstantTypeHandler.class);
          this.register(LocalDateTime.class, LocalDateTimeTypeHandler.class);
          this.register(LocalDate.class, LocalDateTypeHandler.class);
          this.register(LocalTime.class, LocalTimeTypeHandler.class);
          this.register(OffsetDateTime.class, OffsetDateTimeTypeHandler.class);
          this.register(OffsetTime.class, OffsetTimeTypeHandler.class);
          this.register(ZonedDateTime.class, ZonedDateTimeTypeHandler.class);
          this.register(Month.class, MonthTypeHandler.class);
          this.register(Year.class, YearTypeHandler.class);
          this.register(YearMonth.class, YearMonthTypeHandler.class);
          this.register(JapaneseDate.class, JapaneseDateTypeHandler.class);
        }
    
        // issue #273
        register(Character.class, new CharacterTypeHandler());
        register(char.class, new CharacterTypeHandler());
    }
复制代码

里面调用了两个 register() 重载方法, type + handler 参的 TypeHandlerRegistry.register(Class<T> javaType, TypeHandler<? extends T> typeHandler)type + jdbc type + handler 参的 TypeHandlerRegistry.register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler)

// java type + handler
    public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
        register((Type) javaType, typeHandler);
    }
    
    private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
        MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
        if (mappedJdbcTypes != null) {
          for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
            register(javaType, handledJdbcType, typeHandler);
          }
          if (mappedJdbcTypes.includeNullJdbcType()) {
            register(javaType, null, typeHandler);
          }
        } else {
          register(javaType, null, typeHandler);
        }
    }
    
    // java type + jdbc type + handler
    public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {
        register((Type) type, jdbcType, handler);
    }
    
    // type + handler 和 type + jdbc type + handler 最终都调用此方法
    private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
        if (javaType != null) {
          // 当 javaType 不为空时, 获取 java 类型的的映射
          Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
          if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            // 若映射为空,新建一个映射关系
            map = new HashMap<JdbcType, TypeHandler<?>>();
            // 保存至类型处理器映射关系中
            TYPE_HANDLER_MAP.put(javaType, map);
          }
          // 保存jdbcType和处理器关系,完成 java类型,jdbc类型,处理器三者之间的注册
          map.put(jdbcType, handler);
        }
        // 保存处理器信息中
        ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
    }
           
    // MappedJdbcTypes 注解        
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MappedJdbcTypes {
      JdbcType[] value();
      boolean includeNullJdbcType() default false;
    }
复制代码
  • type + handler 方法:先获取处理器的 MappedJdbcTypes 注解(自定义处理器注解),若注解的 value 值不为空时,由于该值为 JdbcType[] 类型,所以 for 循环 javaType+jdbcType+TypeHandler 注册,若 includeNullJdbcTypejdbcType 是否包含 null )为 true ,默认值为 false ,注册到相应映射中。若注解的 valuenull ,直接调用注册操作,里面不会注册 type + jdbc type + handler 关系。
  • type + jdbc type + handler 方法:该方法将java类强制转换为 java.lang.reflect.Type 类型,然后调用最终注册的方法。

调用父类 BaseBuilder 的构造方法

BaseBuilder 定义有三个属性

protected final Configuration configuration;
    // 类型别名注册器
    protected final TypeAliasRegistry typeAliasRegistry;
    // 类型处理器注册器
    protected final TypeHandlerRegistry typeHandlerRegistry;
复制代码

BaseBuilder 构造方法

public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
    }
复制代码

这里属性,就是上面讲解到的。

调用 XMLConfigBuilder.parse() 作为解析入口。

parse() 实现配置文件是否解析过

public Configuration parse() {
        // 若parsed为true,配置文件解析过
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        // 标志已解析过
        parsed = true;
        // 从根节点 configuration 开始解析
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }
复制代码

解析 /configuration 里的配置

private void parseConfiguration(XNode root) {
        try {
          //issue #117 read properties first
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(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);
        }
    }
复制代码

从上面源码中,不难看出这里是解析 /configuration 中的各个子节点。

properties 节点解析

properties 配置方式

<!-- 方法一 -->
    <properties>
        <property name="username" value="${jdbc.username}" />
    </properties>
    
    <!-- 方法二 -->
    <properties resource="xxxConfig.properties">
    </properties>
    
    <!-- 方法三 -->
    <properties url="file:///D:/xxxConfig.properties">
    </properties>
复制代码

propertiesElement() 方法

private void propertiesElement(XNode context) throws Exception {
        if (context != null) {
          // 获取 propertie 节点,并保存 Properties 中
          Properties defaults = context.getChildrenAsProperties();
          // 获取 resource 的值
          String resource = context.getStringAttribute("resource");
          // 获取 url 的值
          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);
          }
          // 将解析的值保存到 XPathParser 中
          parser.setVariables(defaults);
          // 将解析的值保存到 Configuration 中
          configuration.setVariables(defaults);
        }
    }
复制代码

从上面源码中, resourceurl 的配置形式不允许同时存在,否则抛出 BuilderException 异常。先解析 propertie 的配置值,再解析 resourceurl 的值。 当 propertie 存在与 resourceurl 相同的 key 时, propertie 的配置会被覆盖,应为 Properties 实现的原理就是继承的 Hashtable 类来实现的。

settings 节点解析

settings 配置方式

<settings>
        <setting name="cacheEnabled" value="true" />
        ......
    </settings>
复制代码

设置中各项的意图、默认值图(引用来源:w3cschool)

设置参数 描述 有效值 默认值
cacheEnabled 该配置影响的所有映射器中配置的缓存的全局开关。 true,false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 true,false false
aggressiveLazyLoading 当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载。 true,false,true
multipleResultSetsEnabled 是否允许单一语句返回多结果集(需要兼容驱动)。 true,false true
useColumnLabel 使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。 true,false true
useGeneratedKeys 允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 true,false False
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。 NONE, PARTIAL, FULL PARTIAL
defaultExecutorType 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 设置超时时间,它决定驱动等待数据库响应的秒数。 Any positive integer Not Set (null)
safeRowBoundsEnabled 允许在嵌套语句中使用分页(RowBounds)。 true,false False
mapUnderscoreToCamelCase 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 true, false False
localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 SESSION,STATEMENT SESSION
jdbcTypeForNull 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER OTHER
lazyLoadTriggerMethods 指定哪个对象的方法触发一次延迟加载。 A method name list separated by commas equals,clone,hashCode,toString
defaultScriptingLanguage 指定动态 SQL 生成的默认语言。 A type alias or fully qualified class name. org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
callSettersOnNulls 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。注意基本类型(int、boolean等)是不能设置成 null 的。 true,false false
logPrefix 指定 MyBatis 增加到日志名称的前缀。 Any String Not set
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J, LOG4J, LOG4J2, JDK_LOGGING, COMMONS_LOGGING, STDOUT_LOGGING, NO_LOGGING Not set
proxyFactory 指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具。 CGLIB JAVASSIST CGLIB

settingsAsProperties() 方法

private Properties settingsAsProperties(XNode context) {
        if (context == null) {
          return new Properties();
        }
        // 获取setting节点的name和value,并保存至Properties返回
        Properties props = context.getChildrenAsProperties();
        // Check that all settings are known to the configuration class
        // 创建Configuration的MetaClass
        MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
        // 校验Configuration中是否有setting设置的name值
        for (Object key : props.keySet()) {
          if (!metaConfig.hasSetter(String.valueOf(key))) {
            throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
          }
        }
        return props;
    }
复制代码

这里获取到 setting 的值,并返回 Properties 对象。然后做配置的 name 是否合法。 org.apache.ibatis.reflection.MetaClass 类是保存着一个利用反射获取到的类信息, metaConfig.hasSetter(String.valueOf(key)) 是判断 metaConfig 对象中是否包含 key 属性。

vfsImpl() 方法

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);
            }
          }
        }
    }
复制代码

该方法是解析虚拟文件系统配置,用来加载自定义虚拟文件系统的资源。类保存在 Configuration.vfsImpl 中。

settingsElement() 方法

这个方法的作用就是将解析的 settings 设置到 configuration

private void settingsElement(Properties props) throws Exception {
        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.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")));
        @SuppressWarnings("unchecked")
        Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>)resolveClass(props.getProperty("defaultEnumTypeHandler"));
        configuration.setDefaultEnumTypeHandler(typeHandler);
        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"));
        @SuppressWarnings("unchecked")
        Class<? extends Log> logImpl = (Class<? extends Log>)resolveClass(props.getProperty("logImpl"));
        configuration.setLogImpl(logImpl);
        configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
    }
复制代码

typeAliases 节点解析

typeAliases 配置方式

<typeAliases>
        <package name="com.ytao.main.model"/>
        // 或
        <typeAlias type="com.ytao.main.model.Student" alias="student"/>
        <typeAlias type="com.ytao.main.model.Person"/>
    </typeAliases>
复制代码

该节点是配置类和别名的关系

  1. package 节点是配置整个包下的类
  2. typeAlias 节点是指定配置单个类, type 为必填值且为类全限定名, alias 为选填。 配置后,是该类时,可直接使用别名。

typeAliasesElement() 方法

private void typeAliasesElement(XNode parent) {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
              // 以 package 方式配置
              String typeAliasPackage = child.getStringAttribute("name");
              configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
              // 以 alias 方式配置
              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 配置

当扫描 package 时,获取到包名后 TypeAliasRegistry.registerAliases(typeAliasPackage)

public void registerAliases(String packageName){
        registerAliases(packageName, Object.class);
    }

    public void registerAliases(String packageName, Class<?> superType){
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
        // 获取 package 下所有已 .class 结尾的文件
        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);
          }
        }
    }
复制代码

扫描到指定 package 下所有以 .class 结尾文件的类,并保存至Set集合中,然后遍历集合,过滤掉没有名称,接口,和底层特定类。 最后 TypeAliasRegistry.registerAlias(Class<?> type) 注册到别名注册器中。

public void registerAlias(Class<?> type) {
        // 使用类的 simpleName 作为别名,也就是默认的别名命名规则
        String alias = type.getSimpleName();
        Alias aliasAnnotation = type.getAnnotation(Alias.class);
        if (aliasAnnotation != null) {
          alias = aliasAnnotation.value();
        } 
        // 上面分析的最终注册的方法
        registerAlias(alias, type);
    }
复制代码

通过类注册到注册器中时,如果该注册类有使用 @Aliasorg.apache.ibatis.type.Alias )注解,那么XML配置中配置的别名会被注解配置覆盖。

使用 typeAlias 配置

如果 typeAliasalias 有设置值,使用自定名称方式注册,否则使用默认方式注册,即类的simpleName作为别名。

plugins 节点解析

plugins 配置方式

<plugins>
        // 配置自定义插件,可指定在某个点进行拦截
        <plugin interceptor="com.ytao.main.plugin.DemoInterceptor">
            // 当前插件属性
            <property name="name" value="100"/>
        </plugin>
    </plugins>
复制代码

自定义插件需要实现 org.apache.ibatis.plugin.Interceptor 接口,同时在注解上指定拦截的方法。

pluginElement() 方法

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
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            // 设置插件属性到插件中
            interceptorInstance.setProperties(properties);
            // 将插件保存在 configuration 中
            configuration.addInterceptor(interceptorInstance);
          }
        }
    }
复制代码

这里取 <plugin> 节点的 interceptor 可以使用别名设置。从源码中 resolveClass 方法

// 
    protected Class<?> resolveClass(String alias) {
        if (alias == null) {
          return null;
        }
        try {
          return resolveAlias(alias);
        } catch (Exception e) {
          throw new BuilderException("Error resolving class. Cause: " + e, e);
        }
    }
    
    // 
    protected Class<?> resolveAlias(String alias) {
        return typeAliasRegistry.resolveAlias(alias);
    }
    
    // 
    public <T> Class<T> resolveAlias(String string) {
        try {
          if (string == null) {
            return null;
          }
          // issue #748
          // 将传入的 类 名称统一转换
          String key = string.toLowerCase(Locale.ENGLISH);
          Class<T> value;
          // 验证别名中是否有当前传入的key
          if (TYPE_ALIASES.containsKey(key)) {
            value = (Class<T>) TYPE_ALIASES.get(key);
          } else {
            value = (Class<T>) Resources.classForName(string);
          }
          return value;
        } catch (ClassNotFoundException e) {
          throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
        }
    }
复制代码

以上源码为别名解析过程,其他别名的解析也是调用此方法,先去保存的别名中去找,是否有别名,如果没有就通过 Resources.classForName 生成实例。

objectFactory,objectWrapperFactory,reflectorFactory 节点解析

以上都是对实现类都是对MyBatis进行扩展。解析方法也类似,最后都是保存在 configuration

// objectFactory 解析
    private void objectFactoryElement(XNode context) throws Exception {
        if (context != null) {
          String type = context.getStringAttribute("type");
          Properties properties = context.getChildrenAsProperties();
          ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
          factory.setProperties(properties);
          configuration.setObjectFactory(factory);
        }
    }
    
    // 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);
        }
    }
    
    // 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);
        }
    }
    
复制代码

以上为解析 objectFactory,objectWrapperFactory,reflectorFactory 源码,经过前面的分析后,这里比较容易看懂。

environments 节点解析

environments 配置方式

<environments default="development">
        <environment id="development">
            <!-- 事务管理 -->
            <transactionManager type="JDBC">
                <property name="prop" value="100"/>
            </transactionManager>
            <!-- 数据源 -->
            <dataSource type="UNPOOLED">
                <!-- JDBC 驱动 -->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <!-- 数据库的 url -->
                <property name="url" value="${jdbc.url}"/>
                <!-- 数据库登录名 -->
                <property name="username" value="${jdbc.username}"/>
                <!-- 数据库登录密码 -->
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
        <!-- 一个环境,对应一个environment -->
        ......
    </environments>
复制代码

该节点可设置多个环境,针对不同的环境单独配置。 environments 的属性 default 是默认环境,该值对应一个 environment 的属性 id 的值。

  • transactionManager 为事务管理,属性 type 为事务管理类型,上面的介绍的 new Configuration() 有定义类型有:JDBC 和 MANAGED事务管理类型。
  • dataSource 是数据源, type 为数据源类型,与 transactionManager 同理,可知内建的数据源类型有:JNDI,POOLED,UNPOOLED数据源类型。

environmentsElement() 方法

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");
            // 验证 id
            if (isSpecifiedEnvironment(id)) {
              // 解析 transactionManager, 并实例化 TransactionFactory
              TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
              // 解析 dataSource,并实例化 DataSourceFactory
              DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
              // 获取 dataSource
              DataSource dataSource = dsFactory.getDataSource();
              Environment.Builder environmentBuilder = new Environment.Builder(id)
                  .transactionFactory(txFactory)
                  .dataSource(dataSource);
              configuration.setEnvironment(environmentBuilder.build());
            }
          }
        }
    }
    
    private boolean isSpecifiedEnvironment(String id) {
        if (environment == null) {
          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 环境或环境没有给 id 属性,则会抛出异常,若当前 id 是要使用的就返回 true ,否则返回 falseTransactionFactory 实例化过程比较简单,与创建 DataSourceFactory 类似。

数据源的获取

获取数据源,首先得创建 DataSourceFactory ,上面使用 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")) 创建

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
        if (context != null) {
          String type = context.getStringAttribute("type");
          Properties props = context.getChildrenAsProperties();
          DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
          factory.setProperties(props);
          return factory;
        }
        throw new BuilderException("Environment declaration requires a DataSourceFactory.");
    }
复制代码

这里就是获取到数据源得 type 后,利用上面所讲到得 resolveClass() 方法获取到 DataSourceFactory 。 以 UNPOOLED 为例,对应的 DataSourceFactory 实现类为 UnpooledDataSourceFactory 。实例化过程中就给该类的属性 dataSource 数据源赋值了

/**
     * UnpooledDataSourceFactory 类
     */
    protected DataSource dataSource;
    
    public UnpooledDataSourceFactory() {
        this.dataSource = new UnpooledDataSource();
    }
    
    @Override
    public DataSource getDataSource() {
       return dataSource;
    }
复制代码

UnpooledDataSource 类里面有静态代码块所以数据源被加载

/**
     * UnpooledDataSource 类
     */
    static {
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
          Driver driver = drivers.nextElement();
          registeredDrivers.put(driver.getClass().getName(), driver);
        }
    }
复制代码

databaseIdProvider 节点解析

databaseIdProvider 配置方式

<databaseIdProvider type="DB_VENDOR">
        <property name="SQL Server" value="sqlserver"/>
        <property name="DB2" value="db2"/>
        <property name="Oracle" value="oracle" />
        <property name="MySQL" value="mysql"/>
    </databaseIdProvider>
    
    <select id="select" resultType="com.ytao.main.model.Student" databaseId="mysql">
        select
          *
        from student
    </select>
复制代码

基于映射语句中的 databaseId 属性,可以根据不同数据库厂商执行不同的sql。

databaseIdProviderElement() 方法

private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
      String type = context.getStringAttribute("type");
      // 保持向后兼容
      if ("VENDOR".equals(type)) {
          type = "DB_VENDOR";
      }
      Properties properties = context.getChildrenAsProperties();
      databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
      databaseIdProvider.setProperties(properties);
    }
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
      String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
      configuration.setDatabaseId(databaseId);
    }
  }
复制代码

根据匹配的数据库厂商类型匹配数据源 databaseIdProvider.getDatabaseId(environment.getDataSource())

@Override
  public String getDatabaseId(DataSource dataSource) {
    if (dataSource == null) {
      throw new NullPointerException("dataSource cannot be null");
    }
    try {
      return getDatabaseName(dataSource);
    } catch (Exception e) {
      log.error("Could not get a databaseId from dataSource", e);
    }
    return null;
  }
  
  private String getDatabaseName(DataSource dataSource) throws SQLException {
    // 根据数据源获取数据库产品名称
    String productName = getDatabaseProductName(dataSource);
    if (this.properties != null) {
      for (Map.Entry<Object, Object> property : properties.entrySet()) {
        // 判断是否包含,选择使用的数据库产品
        if (productName.contains((String) property.getKey())) {
          return (String) property.getValue();
        }
      }
      // no match, return null
      return null;
    }
    return productName;
  }
    
  private String getDatabaseProductName(DataSource dataSource) throws SQLException {
    Connection con = null;
    try {
      // 数据库连接
      con = dataSource.getConnection();
      // 获取连接元数据
      DatabaseMetaData metaData = con.getMetaData();
      // 获取数据库产品名称
      return metaData.getDatabaseProductName();
    } finally {
      if (con != null) {
        try {
          con.close();
        } catch (SQLException e) {
          // ignored
        }
      }
    }
  }    
复制代码

这里需要注意的是配置:比如使用 mysql ,我踩过这里的坑,这里Name为 MySQL ,我把 y 写成大写,结果匹配不上。 另外这里写个 My 也能匹配上,应为是使用的 String.contains 方法,只要包含就会符合,这里代码应该不够严谨。

typeHandlers 节点解析

typeHandlers 配置方式

<typeHandlers>
        <package name="com.ytao.main.handler"/>
        // 或
        <typeHandler javaType="java.util.Date"  jdbcType="TIMESTAMP" handler="com.ytao.main.handler.DemoDateHandler" />
    </typeHandlers>
复制代码

扫描整个包或者指定类型之间的映射, javaType , jdbcType 非必需, handler 必填项

typeHandlerElement() 方法

private void typeHandlerElement(XNode parent) throws Exception {
    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);
          }
        }
      }
    }
  }

复制代码

源码分析会根据包下所有处理器或者指定处理器进行解析,最后会根据上面分析到的 type + handlertype + jdbc type + handler 不同情况注册。 另外这里还有个 TypeHandlerRegistry.register(Class<?> typeHandlerClass) 注册类

public void register(Class<?> typeHandlerClass) {
    // 标志是否从 MappedTypes 注解中获取 javaType 注册
    boolean mappedTypeFound = false;
    // 获取 MappedTypes 的值
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> javaTypeClass : mappedTypes.value()) {
        // 已 type + handler 的方式注册
        register(javaTypeClass, typeHandlerClass);
        // 标志已通过注解注册类型
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {
      // 通过 TypeHandler 注册
      register(getInstance(null, typeHandlerClass));
    }
  }
  
  // 实例化
  public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    if (javaTypeClass != null) {
      try {
        // 获取有参构造函数
        Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
        // 实例化对象
        return (TypeHandler<T>) 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<T>) c.newInstance();
    } catch (Exception e) {
      throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
    }
  }  
  
  // 注册实例
  public <T> void register(TypeHandler<T> typeHandler) {
    boolean mappedTypeFound = false;
    MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> handledType : mappedTypes.value()) {
        register(handledType, typeHandler);
        mappedTypeFound = true;
      }
    }
    // @since 3.1.0 - try to auto-discover the mapped type
    if (!mappedTypeFound && typeHandler instanceof TypeReference) {
      try {
        TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
        register(typeReference.getRawType(), typeHandler);
        mappedTypeFound = true;
      } catch (Throwable t) {
        // maybe users define the TypeReference with a different type and are not assignable, so just ignore it
      }
    }
    if (!mappedTypeFound) {
      register((Class<T>) null, typeHandler);
    }
  }  
  
复制代码

以上的 register 方法中,了解 type + jdbc type + handler 后,其他的 register 重载方法比较容易理解,其他的都是基于它上面的封装。

mappers 节点解析

mappers 配置方式

<mappers>
        <package name="com.ytao.main.mapper"/>
        // 或
        <mapper resource="mapper/studentMapper.xml"/>
        // 或
        <mapper url="file:///D:/mybatis-3-mybatis-3.4.6/src/main/resources/mapper/studentMapper.xml"/>
        // 或
        <mapper class="com.ytao.main.mapper.StudentMapper"/>
    </mappers>
复制代码

可通过以上四种形式配置 mappers 节点, <package><mapper> 为互斥节点。

mapperElement() 方法

该方法是负责解析 <mappers> 节点

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 如果配置 package 节点,则扫描
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          // 解析包下类Mapper接口,并注册到configuration的mapperRegistry中
          configuration.addMappers(mapperPackage);
        } else {
          // 获取mapper节点的resource,url,class属性
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 根据resource解析,并且url,class值必须为空,也就不能配置值。url,class同理,其它两个属性也不能配置值
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            // 通过resource获取流
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 创建XMLMapperBuilder对象
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 解析映射配置文件
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            // 通过url获取流
            InputStream inputStream = Resources.getUrlAsStream(url);
            // 和resource解析方式一样,创建XMLMapperBuilder对象,然后解析映射配置文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            // 加载class属性的接口
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            // 将接口注册到configuration的mapperRegistry中
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }
复制代码

<package> 的包扫描到的类,然后单个单个注册到configuration的mapperRegistry中,这里和 <mapper> 使用 class 属性是一样逻辑。 解析 package 方式

// Configuration 中定义了
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

  /**
   * 步骤一
   * 该函数于 Configuration 中  
   */  
  public void addMappers(String packageName) {
    // mapperRegistry定义在Configuration中的一个属性
    mapperRegistry.addMappers(packageName);
  }
  
  /**
   * 步骤二
   * 该函数于 MapperRegistry 中  
   */   
  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }
  
  /**
   * 步骤三
   * 该函数于 MapperRegistry 中  
   */       
  public void addMappers(String packageName, Class<?> superType) {
    // 通过 ResolverUtil 获取包下的类
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      // 遍历获取到的类,注册到 MapperRegistry
      addMapper(mapperClass);
    }
  }   
   
  /**
   * 步骤四
   * 该函数于 MapperRegistry 中
   */
  public <T> void addMapper(Class<T> type) {
    // mapper 类为 interface 接口
    if (type.isInterface()) {
      // 判断当前class是否已经注册过
      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<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        // 解析xml和注解
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        // 标志加载完成
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }   
  
复制代码

解析 mapperclass 属性

// 该函数于 Configuration 中  
  public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }
  
  // ... 这里调用上面的【步骤四】
复制代码

这两中方式是直接注册接口到 mapperRegistry ,另外两种是解析 xml 的方式就是获取映射文件的 namespace ,再注册进来, XMLMapperBuilder 是负责解析映射配置文件的类,今后会单独详细分析这个类,这里不展开讲。

这里对XMLConfigBuilder解析配置文件到此分析完,本文对配置文件解析的流程大致了解流程和原理。相信遇到配置问题异常,大致能排查到根本原因。

我的公众号 ytao

MyBatis启动之XMLConfigBuilder解析配置文件(二)
原文  https://juejin.im/post/5d9f4b1ff265da5bad405a06
正文到此结束
Loading...