Mybatis(二) - 全局配置文件及其加载机制

1. 全局配置文件详解

MyBatis 的全局配置文件,自上而下的编写内容如下:

  • properties(属性)

  • settings(设置)

  • typeAliases(类型别名)

  • typeHandlers(类型处理器)

  • objectFactory(对象工厂)

  • plugins(插件)

  • environments(环境配置)

    • environment(环境变量)

      • transactionManager(事务管理器)

      • dataSource(数据源)

  • databaseIdProvider(数据库厂商标识)

  • mappers(映射器)

1.1 属性(properties)

这些属性可以在外部进行配置,并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性,也可以在 properties 元素的子元素中设置。

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值。比如:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

也可以在 SqlSessionFactoryBuilder.build() 方法中传入属性值。例如:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, props);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, props);

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

  • 首先读取在 properties 元素体内指定的属性。

  • 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。

  • 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。

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

从 MyBatis 3.4.2 开始,你可以为占位符指定一个默认值。例如:

<dataSource type="POOLED">
  <!-- ... -->
  <property name="username" value="${username:ut_user}"/> <!-- 如果属性 'username' 没有被配置,'username' 属性的值将为 'ut_user' -->
</dataSource>

这个特性默认是关闭的。要启用这个特性,需要添加一个特定的属性来开启这个特性org.apache.ibatis.parsing.PropertyParser.enable-default-value。例如:

<properties resource="org/mybatis/example/config.properties">
  <!-- ... -->
  <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> <!-- 启用默认值特性 -->
</properties>

1.2 设置(settings)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 下表描述了设置中各项设置的含义、默认值等。

设置名描述有效值默认值

cacheEnabled

全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。

true | false

true

lazyLoadingEnabled

延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。

true | false

false

aggressiveLazyLoading

开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。

true | false

false (在 3.4.1 及之前的版本中默认为 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

autoMappingUnknownColumnBehavior

指定发现自动映射目标未知列(或未知属性类型)的行为。NONE: 不做任何反应WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARNFAILING: 映射失败 (抛出 SqlSessionException)

NONE, WARNING, FAILING

NONE

defaultExecutorType

配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。

SIMPLE REUSE BATCH

SIMPLE

defaultStatementTimeout

设置超时时间,它决定数据库驱动等待数据库响应的秒数。

任意正整数

未设置 (null)

defaultFetchSize

为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。

任意正整数

未设置 (null)

defaultResultSetType

指定语句默认的滚动策略。(新增于 3.5.2)

FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置)

未设置 (null)

safeRowBoundsEnabled

是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。

true | false

False

safeResultHandlerEnabled

是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。

true | false

True

mapUnderscoreToCamelCase

是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。

true | false

False

localCacheScope

MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。

SESSION | STATEMENT

SESSION

jdbcTypeForNull

当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。

JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。

OTHER

lazyLoadTriggerMethods

指定对象的哪些方法触发一次延迟加载。

用逗号分隔的方法列表。

equals,clone,hashCode,toString

defaultScriptingLanguage

指定动态 SQL 生成使用的默认脚本语言。

一个类型别名或全限定类名。

org.apache.ibatis.scripting.xmltags.XMLLanguageDriver

defaultEnumTypeHandler

指定 Enum 使用的默认 TypeHandler 。(新增于 3.4.5)

一个类型别名或全限定类名。

org.apache.ibatis.type.EnumTypeHandler

callSettersOnNulls

指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。

true | false

false

returnInstanceForEmptyRow

当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2)

true | false

false

logPrefix

指定 MyBatis 增加到日志名称的前缀。

任何字符串

未设置

logImpl

指定 MyBatis 所用日志的具体实现,未指定时将自动查找。

SLF4J | LOG4J(3.5.9 起废弃) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING

未设置

proxyFactory

指定 Mybatis 创建可延迟加载对象所用到的代理工具。

CGLIB (3.5.10 起废弃) | JAVASSIST

JAVASSIST (MyBatis 3.3 以上)

vfsImpl

指定 VFS 的实现

自定义 VFS 的实现的类全限定名,以逗号分隔。

未设置

useActualParamName

允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1)

true | false

true

configurationFactory

指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3)

一个类型别名或完全限定类名。

未设置

shrinkWhitespacesInSql

从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5)

true | false

false

defaultSqlProviderType

指定一个拥有 provider 方法的 sql provider 类 (新增于 3.5.6). 这个类适用于指定 sql provider 注解上的type(或 value) 属性(当这些属性在注解中被忽略时)。 (e.g. @SelectProvider)

类型别名或者全限定名

未设置

nullableOnForEach

为 'foreach' 标签的 'nullable' 属性指定默认值。(新增于 3.5.9)

true | false

false

argNameBasedConstructorAutoMapping

当应用构造器自动映射时,参数名称被用来搜索要映射的列,而不再依赖列的顺序。(新增于 3.5.10)

true | false

false

一个配置完整的 settings 元素的示例如下:

<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>

1.3 类型别名(typeAliases)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
  <typeAlias alias="Post" type="domain.blog.Post"/>
  <typeAlias alias="Section" type="domain.blog.Section"/>
  <typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

当这样配置时,Blog 可以用在任何使用 domain.blog.Blog 的地方。

也可以指定一个包名,MyBatis 会在包名下面搜索需要的Java Bean,比如:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

每一个在包 domain.blog 中的 Java Bean在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 domain.blog.Author 的别名为 author;若有注解,则别名为其注解值。见下面的例子:

@Alias("author")
public class Author {
    ...
}

1.4 类型处理器(typeHandlers)

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。

类型处理器Java 类型JDBC 类型

BooleanTypeHandler

java.lang.Boolean, boolean

数据库兼容的 BOOLEAN

ByteTypeHandler

java.lang.Byte, byte

数据库兼容的 NUMERICBYTE

ShortTypeHandler

java.lang.Short, short

数据库兼容的 NUMERICSMALLINT

IntegerTypeHandler

java.lang.Integer, int

数据库兼容的 NUMERICINTEGER

LongTypeHandler

java.lang.Long, long

数据库兼容的 NUMERICBIGINT

FloatTypeHandler

java.lang.Float, float

数据库兼容的 NUMERICFLOAT

DoubleTypeHandler

java.lang.Double, double

数据库兼容的 NUMERICDOUBLE

BigDecimalTypeHandler

java.math.BigDecimal

数据库兼容的 NUMERICDECIMAL

StringTypeHandler

java.lang.String

CHAR, VARCHAR

ClobReaderTypeHandler

java.io.Reader

-

ClobTypeHandler

java.lang.String

CLOB, LONGVARCHAR

NStringTypeHandler

java.lang.String

NVARCHAR, NCHAR

NClobTypeHandler

java.lang.String

NCLOB

BlobInputStreamTypeHandler

java.io.InputStream

-

ByteArrayTypeHandler

byte[]

数据库兼容的字节流类型

BlobTypeHandler

byte[]

BLOB, LONGVARBINARY

DateTypeHandler

java.util.Date

TIMESTAMP

DateOnlyTypeHandler

java.util.Date

DATE

TimeOnlyTypeHandler

java.util.Date

TIME

SqlTimestampTypeHandler

java.sql.Timestamp

TIMESTAMP

SqlDateTypeHandler

java.sql.Date

DATE

SqlTimeTypeHandler

java.sql.Time

TIME

ObjectTypeHandler

Any

OTHER 或未指定类型

EnumTypeHandler

Enumeration Type

VARCHAR 或任何兼容的字符串类型,用来存储枚举的名称(而不是索引序数值)

EnumOrdinalTypeHandler

Enumeration Type

任何兼容的 NUMERICDOUBLE 类型,用来存储枚举的序数值(而不是名称)。

SqlxmlTypeHandler

java.lang.String

SQLXML

InstantTypeHandler

java.time.Instant

TIMESTAMP

LocalDateTimeTypeHandler

java.time.LocalDateTime

TIMESTAMP

LocalDateTypeHandler

java.time.LocalDate

DATE

LocalTimeTypeHandler

java.time.LocalTime

TIME

OffsetDateTimeTypeHandler

java.time.OffsetDateTime

TIMESTAMP

OffsetTimeTypeHandler

java.time.OffsetTime

TIME

ZonedDateTimeTypeHandler

java.time.ZonedDateTime

TIMESTAMP

YearTypeHandler

java.time.Year

INTEGER

MonthTypeHandler

java.time.Month

INTEGER

YearMonthTypeHandler

java.time.YearMonth

VARCHARLONGVARCHAR

JapaneseDateTypeHandler

java.time.chrono.JapaneseDate

DATE

你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。

TypeHandler 接口中定义了 4 个方法,大面上分两类:当 mapper.xml 中定义的 statement 中出现指定泛型类型的参数时,如何对 **PreparedStatement** 操作查询动作封装结果集时,对于实体类中出现的指定泛型类型的属性时,应该如何从 ResultSet 中取到数据,并转换为指定类型

比如:

public class DepartmentTypeHandler implements TypeHandler<Department> {
    
    @Override
    public void setParameter(PreparedStatement ps, int i, Department department, JdbcType jdbcType) throws SQLException {
        ps.setString(i, department.getId());
    }
    
    @Override
    public Department getResult(ResultSet rs, String columnName) throws SQLException {
        Department department = new Department();
        department.setId(rs.getString(columnName));
        return department;
    }
    
    @Override
    public Department getResult(ResultSet rs, int columnIndex) throws SQLException {
        Department department = new Department();
        department.setId(rs.getString(columnIndex));
        return department;
    }
    
    @Override
    public Department getResult(CallableStatement cs, int columnIndex) throws SQLException {
        Department department = new Department();
        department.setId(cs.getString(columnIndex));
        return department;
    }
}
<typeHandlers>
  <typeHandler handler="org.mybatis.example.DepartmentTypeHandler"/>
</typeHandlers>

通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:

  • 在类型处理器的配置元素(typeHandler 元素)上增加一个 javaType 属性(比如:javaType="String");

  • 在类型处理器的类上增加一个 @MappedTypes 注解指定与其关联的 Java 类型列表。 如果在 javaType 属性中也同时指定,则注解上的配置将被忽略,javaType的优先级更高。

可以通过两种方式来指定关联的 JDBC 类型:

  • 在类型处理器的配置元素上增加一个 jdbcType 属性(比如:jdbcType="VARCHAR");

  • 在类型处理器的类上增加一个 @MappedJdbcTypes 注解指定与其关联的 JDBC 类型列表。 如果在 jdbcType 属性中也同时指定,则注解上的配置将被忽略,jdbcType的优先级更高。

1.5 对象工厂(objectFactory)

每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 默认的对象工厂(**DefaultObjectFactory**)需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。

如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。比如:

public class ExampleObjectFactory extends DefaultObjectFactory {
    @Override
    public <T> T create(Class<T> type) {
        return super.create(type);
    }

    @Override
    public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        return super.create(type, constructorArgTypes, constructorArgs);
    }

    @Override
    public void setProperties(Properties properties) {
        super.setProperties(properties);
    }

    @Override
    public <T> boolean isCollection(Class<T> type) {
        return Collection.class.isAssignableFrom(type);
    }}
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

ObjectFactory 接口很简单,它包含两个创建实例用的方法,一个是处理默认无参构造方法的,另外一个是处理带参数的构造方法的。 另外,setProperties 方法可以被用来配置 ObjectFactory,在初始化你的 ObjectFactory 实例后, objectFactory 元素体中定义的属性会被传递给 setProperties 方法

properties在哪里用到的?

1.6 插件(plugins)

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

@Intercepts({@Signature(
    type= Executor.class,
    method = "update",
    args = {MappedStatement.class,Object.class})})
    public class ExamplePlugin implements Interceptor {
        private Properties properties = new Properties();

        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            // implement pre processing if need
            Object returnObject = invocation.proceed();
            // implement post processing if need
            return returnObject;
        }

        @Override
        public void setProperties(Properties properties) {
            this.properties = properties;
        }
    }

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。

除了用插件来修改 MyBatis 核心行为以外,还可以通过完全覆盖配置类来达到目的。只需继承配置类后覆盖其中的某个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会极大影响 MyBatis 的行为,务请慎之又慎。

1.7 环境配置(environments)

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。

尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。

所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单。

<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>

注意一些关键点:

  • 默认使用的环境 ID(比如:default="development")。

  • 每个 environment 元素定义的环境 ID(比如:id="development")。

  • 事务管理器的配置(比如:type="JDBC")。

  • 数据源的配置(比如:type="POOLED")。

默认环境和环境 ID 顾名思义。 环境可以随意命名,但务必保证默认的环境 ID 要匹配其中一个环境 ID。

事务管理器(transactionManager)

在 MyBatis 中有两种类型的事务管理器(JDBC,MANAGED)。

  • JDBC – 这个配置直接使用了 JDBC 的提交和回滚功能,它依赖从数据源获得的连接来管理事务作用域。默认情况下,为了与某些驱动程序兼容,它在关闭连接时启用自动提交。然而,对于某些驱动程序来说,启用自动提交不仅是不必要的,而且是一个代价高昂的操作。因此,从 3.5.10 版本开始,你可以通过将 "skipSetAutoCommitOnClose" 属性设置为 "true" 来跳过这个步骤。

<transactionManager type="JDBC">
  <property name="skipSetAutoCommitOnClose" value="true"/>
</transactionManager>
  • MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。

<transactionManager type="MANAGED">
  <property name="closeConnection" value="false"/>
</transactionManager>

如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。

数据源(dataSource)

dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。

大多数 MyBatis 应用程序会按示例中的例子来配置数据源。虽然数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源。

有三种内建的数据源类型(UNPOOLED,POOLED,JNDI)

  • UNPOOLED:这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。

  • POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。

  • JNDI:这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。

另外,你可以通过实现接口 org.apache.ibatis.datasource.DataSourceFactory 来使用第三方数据源实现。

1.8 数据库厂商标识(databaseIdProvider)

MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。 MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。 为支持多厂商特性,只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider 即可:

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
</databaseIdProvider>
  1. databaseIdProvider的type属性是必须的,不配置时会报错。DB_VENDOR这个属性值使用的是VendorDatabaseIdProvider类的别名。

  2. property子元素是配置一个数据库,其中的name属性是数据库名称,value是我们自定义的别名,通过别名我们可以在SQL语句中标识适用于哪种数据库运行。

<select id="getTime" resultType="string" databaseId="mysql">
   select  now() from dual 
</select>

<select id="getTime" resultType="string" databaseId="oracle">
   select  'oralce'||to_char(sysdate,'yyyy-mm-dd hh24:mi:ss')  from dual 
</select>

另外,你可以通过实现接口 org.apache.ibatis.mapping.DatabaseIdProvider 并在 mybatis-config.xml 中注册来构建自己的 DatabaseIdProvider

public interface DatabaseIdProvider {
    default void setProperties(Properties p) { // 从 3.5.2 开始,该方法为默认方法
        // 空实现
    }
    String getDatabaseId(DataSource dataSource) throws SQLException;
}

1.9 映射器(mappers)

既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。例如:

<mappers>
  <!-- 使用相对于类路径的资源引用 -->
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <!-- 使用完全限定资源定位符(URL) -->
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <!-- 使用映射器接口实现类的完全限定类名 -->
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <!-- 将包内的映射器接口实现全部注册为映射器 -->
  <package name="org.mybatis.builder"/>
</mappers>

这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了,也就是接下来我们要讨论的。

2. 全局配置文件加载机制

2.1 全局配置文件的加载

InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);

首先我们来看配置文件的加载,这个 Resources.getResourceAsStream 方法,只从方法名上,想必小伙伴也能猜出来,它应该是借助类加载器吧,我们快速的看一眼源码:

public static InputStream getResourceAsStream(String resource) throws IOException {
    return getResourceAsStream(null, resource);
}

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
        throw new IOException("Could not find resource " + resource);
    }
    return in;
}

这个地方看上去没有传入 ClassLoader ,实际上取 ClassLoader 的地方在另外一个位置:

ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,
        defaultClassLoader,
        Thread.currentThread().getContextClassLoader(),
        getClass().getClassLoader(),
        systemClassLoader
    };
}

一下子取这么多 ClassLoader ,很明显,它是想挨个 **ClassLoader** 都试一遍,只要能取到资源,就 OK 。下面是实际利用 ClassLoader 加载全局配置文件的底层源码:

InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    for (ClassLoader cl : classLoader) {
        if (null != cl) {
            // try to find the resource as passed
            InputStream returnValue = cl.getResourceAsStream(resource);
            // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource
            if (null == returnValue) {
                returnValue = cl.getResourceAsStream("/" + resource);
            }
            if (null != returnValue) {
                return returnValue;
            }
        }
    }
    return null;
}

2.2 解析配置文件

下面就是解析的过程了,我们的测试代码是直接 new 了一个 SqlSessionFactoryBuilder ,随后调 build 方法构造出 SqlSessionFactory

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);

build 方法最终来到了一个三参数的重载方法中:

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    } // catch finally ......
}

可见,这里面用到的第一个底层核心组件,是 **XMLConfigBuilder** ,直译为基于 xml 的配置建造器(建造器模式的体现)。而这个 XMLConfigBuilder ,首先继承了一个叫 **BaseBuilder** 的东西:

public class XMLConfigBuilder extends BaseBuilder {
    // ......
}

2.2.1 BaseBuilder

BaseBuilder 顾名思义,它是一个基础的构造器,它的初始化需要传入 MyBatis 的全局配置对象 Configuration

public abstract class BaseBuilder {
    protected final Configuration configuration;
    protected final TypeAliasRegistry typeAliasRegistry;
    protected final TypeHandlerRegistry typeHandlerRegistry;

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

这个 Configuration 我们都知道,最终 MyBatis 初始化完成后,所有的配置项、Mapper 、statement 都会存放到这里,小伙伴们能回忆起来就 OK 。

往下大致扫一眼定义的方法,大多数都是一些解析、获取之类的方法,看上去更像是提供基础的工具类方法支撑

2.2.2 XMLConfigBuilder

构造方法定义

public class XMLConfigBuilder extends BaseBuilder {

    private boolean parsed;
    private final XPathParser parser;
    private String environment;
    private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
    
    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;
    }
}

源码中的构造方法重载的特别多,它重载的构造方法不再需要 InputStream ,而是构造了一个 **XPathParser** ,这个家伙虽然我们也没见过,但也能大概猜出来,它就是解析 xml 全局配置文件的解析器,这个东西我们没有必要先去研究,到下面用到的时候再顺道着看就 OK 。

核心parse方法

接下来的代码就是 return build(parser.parse()); 了,这一行代码实际上是两个方法,首先它先调用 XMLConfigBuilderparse 方法,生成 Configuration ,之后才是 SqlSessionFactoryBuilderbuild 方法。我们先来看 XMLConfigBuilder 是如何解析配置文件的。

public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

最核心的方法还是中间的 parseConfiguration 方法,不过在此之前,我们关注一下 parser.evalNode("/configuration") 这个动作。

XPathParser#evalNode

XPathParser 的作用就是将 xml 配置文件转为 Document 对象,并提供对应的 xml 标签节点。它的 evalNode 方法,就是用来获取 xml 中指定的标签:

public XNode evalNode(String expression) {
    return evalNode(document, expression);
}

public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    }
    return new XNode(this, node, variables);
}

private Object evaluate(String expression, Object root, QName returnType) {
    try {
        // 使用javax的XPath解析xml
        return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
        throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
}

至于 XPath 内部怎么做的,那是 xml 解析的机制了,我们不关心,我们唯一要关心的是,中间的 evalNode 方法解析出 Node 之后,return 处又封装成了一个 XNode 。它到底是图个啥呢?

Node封装为XNode的意图

注意看 XNode 的构造方法,它额外传入了一个 variables 对象,而这个 variables 实际上就是我们在全局配置文件中,定义的那些 <properties> 标签,以及引入的 .properties 文件。可为什么又跟这些配置属性值牵扯上了呢?我们回忆一下,之前我们在全局配置文件中写过这样的代码吧:

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

如果真的要取这里面的 driver 属性,肯定不能把 ${jdbc.driverClassName} 拿出来吧,得动态替换配置属性的值。但是 javax 原生的 Node 可实现不了这玩意,所以 MyBatis 就基于 javax 的 Node 封装了一个 XNode ,并组合 XPathParser ,就可以实现动态解析配置属性值的效果了。

// XNode
public String evalString(String expression) {
    // 自己不解析,委托XPathParser去解析
    return xpathParser.evalString(node, expression);
}

// XPathParser
public String evalString(Object root, String expression) {
    // 先从标签中取出明文属性值
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    // 交由占位服务解析器,处理占位符,替换为真实配置值
    result = PropertyParser.parse(result, variables);
    return result;
}

处理思路一目了然,所以这也就能明白,为什么 MyBatis 选择自己额外封一层 XNode ,而不是直接用 javax 的那个 Node 了吧。

parseConfiguration

回到上面的 parse 方法中,parse 方法的本质,是解析 <configuration> 标签的内容,所以下面我们进入 parseConfiguration 方法中:

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);
    }
}

呀,哎呀呀呀呀呀呀呀,这不就是把整个 MyBatis 全局配置文件,从头到尾顺序解析了一遍吗?这也太朴实无华了吧!OK ,那既然我们挖掘到了这个地方,下面我们就可以逐个来看了。

propertiesElement-解析properties

首先解析的是 <properties> 标签,这里面它就会解析内部定义的 <property> ,以及配置的 resourceurl 属性:(关键注释已标注在源码中)

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      Properties defaults = context.getChildrenAsProperties();
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      if (resource != null && url != null) {
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
 }

纵读整段源码,可以发现整个加载的流程,就是我们在上一章中解释的流程。而且通过源码的走读,我们也能明白为什么配置的优先级会是编程式的最高,properties 文件次之,配置文件内定义的最低了。

settingsAsProperties-加载配置项

下面是解析 <settings> 标签了,这个标签的解析涉及到 3 行代码:

Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);

也不难理解,这个操作很明显是将 <settings> 标签中的一行行配置,封装为一个 Properties ,然后额外处理一下 VFS 和 Log 组件。关于 VFS 的内容小册暂时不提,我们先来看看底层如何处理 Log 组件的配置:

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

嚯,这么简单的逻辑吗?它直接从配置中取到 logImpl 的配置值,然后设置到 Configuration 中就完事了,而这个 resolveClass 方法,其实是用别名解析的:

protected <T> Class<? extends T> resolveClass(String alias) {
    if (alias == null) {
        return null;
    }
    try {
        return resolveAlias(alias);
    } // catch ......
}

protected <T> Class<? extends T> resolveAlias(String alias) {
    return typeAliasRegistry.resolveAlias(alias);
}

而这些别名的注册,早在 Configuration 创建的时候,就全部初始化好了:

public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    // ......

    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

    // ......
}

所以我们在配置那些 setting 配置项的时候,可供填充的内容,其实是参考自这里 MyBatis 预定的别名(这也能解释为什么我们如果在 settings 中配置 log4j 不好使,必须配置 LOG4J 才可以)。

看源码时注意到TypeAliasRegistry别名注册的地方:

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

typeAliasesElement-注册类型别名

下面一个要解析的是 typeAliases 标签了,这里面我们知道可以用 <package> 直接扫描,也可以使用 <typeAlias> 直接声明指定类型的别名。底层针对这两种情况分别做了处理:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <package name="domain.blog"/>
</typeAliases>
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String typeAliasPackage = child.getStringAttribute("name");
                // 注意这里调用的是registerAliases注册一组
                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> 标签声明的包扫描,此处调用的方法也不一样了!进入到 registerAliases 方法中,我们会发现,这里会使用一个 ResolverUtil 的工具类,来扫描所有类(父类是 Object ),扫描完成后,在下面的 for 循环中,判断这些类是否为普通的类(非接口、非匿名内部类、非内部类),是则注册别名。

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);
        }
    }
}

注意一点!这里面有一个全层次包扫描的动作!所以配置的 package 实际上是扫描的指定包及其子包下的所有类,并将他们全部注册 alias 别名。

pluginElement-注册插件

接下来注册的是 plugins 插件了,这里面的代码逻辑倒是简单:

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);
        }
    }
}

整体下来,它就是简单的把拦截器创建出来,注册进全局 Configuration 中。当然我们要意识到的一件事:拦截器是 MyBatis 自行创建的,如果我们要用 Spring 整合 MyBatis ,并且想让 Spring 管理 MyBatis 的拦截器,似乎不太现实

注册一堆Factory

objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));

这 3 种 Factory 的注册,在底层几乎是一模一样(以 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).getDeclaredConstructor().newInstance();
        factory.setProperties(properties);
        configuration.setObjectFactory(factory);
    }
}

可以发现,跟上面初始化拦截器的逻辑几乎一模一样吧!所以这几个逻辑咱也都知道一下就 OK 。

settingsElement-应用配置项

接下来的动作是应用之前初始化的配置项,这个方法那是又长又宽(每行代码都好长啊!实在不想全部贴过来),所以我们只截取其中几行代码象征性的看一眼就行:

private void settingsElement(Properties props) {
    // ......
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
    configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
    // ......
}

这不就是把全局配置文件中的那些 <settings> 都应用进全局的 Configuration 对象中嘛,那这也没什么神秘的了,小伙伴们快速扫一遍就 OK 。

environmentsElement-数据源环境配置

下面是解析数据库环境配置的部分了,这里面因为存在嵌套标签 <transactionManager><dataSource> ,所以这里面的源码会稍微复杂一点:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // 从default中取出默认的数据库环境配置标识
            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());
            }
        }
    }
}

纵读整段源码,思路还是非常清晰的吧,它干的事情就是把事务管理器,以及数据源的配置加载好,构造进 Environment 对象中。而解析 transactionManager 标签,以及 dataSource 标签的逻辑,跟上面解析那一堆 Factory 也都几乎完全一致,所以小册也不重复贴源码了,小伙伴们跟着 IDE 翻一下源码就好。

另外,Environment 的结构,也仅仅是组合了上面的 TransactionFactoryDataSource

public final class Environment {
    private final String id;
    private final TransactionFactory transactionFactory;
    private final DataSource dataSource;
    
    // ......
}

databaseIdProviderElement-数据库厂商标识解析

接下来是 <databaseIdProvider> 标签的解析,这个东西我们说如果要用的话,就是声明 "DB_VENDOR" ,然后根据不同的数据库厂商,定义好别名即可。这个逻辑反映到源码中也不难理解:

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";
        }
        Properties properties = context.getChildrenAsProperties();
        databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
        databaseIdProvider.setProperties(properties);
    }
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
        String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
        configuration.setDatabaseId(databaseId);
    }
}

前半段就是正常的初始化 DatabaseIdProvider 类型的对象,关键是下面还有个 configuration.setDatabaseId(databaseId); 的动作,它是干什么呢?

仔细想一下,我们上一章提过,databasseIdProvider 是配合 mapper.xml 中定义 statement 用的,而源码走到这个位置的时候,还没有轮到 mapper.xml 解析,如果像我们上一章那样,在一个 mapper.xml 中定义了两个一样的 statement ,那后面轮到 mapper.xml 解析的时候,MyBatis 一看,呦,两个 statement 竟然 id 一模一样? 这不相当于撞车了吗?不行,这我得给他挂了!所以就会抛出 statement 的 id 相同的异常。但是!这逻辑不对啊,虽然两个 statement 的 id 一致,但 databaseId 不一样啊,一个 SqlSessionFactory 只能连一个数据源,而这个数据源的数据库厂商是确定的,所以这两个 statement 只能有一个可用,所以在解析 mapper.xml ,读取 statement 的时候还要比对一下 databaseId 呢!那这个 databaseId 从哪来呢?很明显可以从全局 Configuration 得到,那上面的这个 setDatabaseId 的动作就可以理解了吧!这个动作就是为了提前确定好数据源对应的数据库厂商,为后面解析 mapper.xml 做准备

typeHandlerElement-注册类型处理器

下面一个解析的是 TypeHandler 了,这里面的逻辑也是比较简单,要么包扫描,要么逐个注册,但最终都是注册到了 typeHandlerRegistry 中:

public final class TypeHandlerRegistry {

  private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
  private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
  private final TypeHandler<Object> unknownTypeHandler;
  private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
  ...
}
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);
                }
            }
        }
    }
}

mapperElement-解析mapper.xml

最后一个节点的解析是 mapper 了,这里面乍一看似乎不是很复杂:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 包扫描Mapper接口
            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");
                // 处理resource加载的mapper.xml
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    // 处理url加载的mapper.xml
                    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) {
                    // 注册单个Mapper接口
                    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.");
                }
            }
        }
    }
}

但请不要忘记!这个方法完事之后,整个 MyBatis 的初始化工作就完成了!但此时 mapper.xml 还没有加载呢!所以这个环节也是相当重要的!源码中,可以发现,除了包扫描 Mapper 接口,以及单个注册 Mapper 接口之外,其余两个都是解析 mapper.xml 文件。至于解析 mapper.xml 的底层是如何处理,我们放到映射文件的讲解之后再展开讲解,这里我们先知道一点即可:**mapper.xml** 的解析是使用 **XMLMapperBuilder** 完成的

参考

【官网】MyBatis XML配置

最后更新于