Mybatis配置文件及源码简析

properties 配置参数

作用:可以在整个配置文件中使用,替换需要动态配置的属性值

配置文件

1
2
3
4
5
6
7
<!-- 方法1:从外部指定properties配置文件(可选),resource 和 url 二选一 -->
<properties resource="application.yml">
    <!-- 方法2: 直接配置为xml -->
    <property name="failFast" value="true"/>
    <property name="useGeneratedKeys" value="true"/>
    <property name="mapUnderscoreToCamelCase" value="true"/>
</properties>

两种方法都使用的情况下,以外部指定的配置文件为准。原因见下面源码:

XMLConfigBuilder # propertiesElement()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      // 先是设置了具体的property参数
      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);
    }
  }

environments 数据库环境

作用:配置数据库环境,可以多个

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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://10.125.38.91:3306/wtest"/>
            <property name="username" value="root"/>
            <property name="password" value="cloudos"/>
        </dataSource>
    </environment>
    <environment id="test">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <!-- 上面properties标签指定了数据库配置文件, 配置文件里面也是对应的这四个属性 -->
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
</environments>

注意:

  • 一个 <environment/> 对应一个数据库,一个数据库对应一个 SqlSessionFactory
  • 每个 <environment/> 有一个 id,<environments/>的default属性要对应其中一个id;
  • 每个 <environment/> 需要配置事务管理器 <transactionManager/> 和数据源<dataSource/>
  • 事务管理器 <transactionManager/> 有两种类型(也就是 type=”[JDBC or MANAGED]”),JDBC直接使用JDBC的事务,MANAGED则是将事务托管给容器;

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

  • 数据源 <dataSource/> 有三种内建的数据源类型(也就是 type=”[UNPOOLED or POOLED or JNDI]”)

源码简析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      // 循环处理多个 environment 子标签
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        // 只处理 default 属性指定的那个id的environment
        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());
        }
      }
    }
  }

typeAliases 别名

作用:为Java的限定类名指定一个简短的别名

配置文件

1
2
3
4
<typeAliases>
    <typeAlias type="com.wzt.batis.UserDo" alias="UserDo"/>
    <package name="com.wzt.batis"/>
</typeAliases>

<typeAliases/>有两类子节点:

  • <typeAlias/>:为某个类指定别名,alias属性可选,不设置时默认为非限定类名的小写;
  • <package/>:为某个package下所有(非匿名/接口/局部类)类指定别名,若类无@Alias注解,默认为非限定类名的小写;

要求:package 标签必须在 typeAlias 后面

源码简析

来看源码:

XMLConfigBuilder#typeAliasesElement()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 解析 package 子标签
        if ("package".equals(child.getName())) {
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          // 解析 typeAlias 子标签
          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 子标签所指向的包下类
TypeAliasRegistry.java

1
2
3
4
5
6
7
8
9
10
11
12
  public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for (Class<?> type : typeSet) {
      // 匿名类、接口、局部类,通通退散!
      if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
        // 看这里
        registerAlias(type);
      }
    }
  }

如果有@Alias注解,则用其值,否则用非限定类名;
对单个 typeAlias 的解析也会进到这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  public void registerAlias(Class<?> type) {
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    }
    // 向下
    registerAlias(alias, type);
  }

  public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // 看这里,所有的别名最后都是小写的
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
      throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
    }
    // 所谓注册,就是放到一个Map里
    typeAliases.put(key, value);
  }

总结一下:

  • 通过 <typeAlias/> 标签可以单独为某个类设置别名;
  • 通过 <package/> 标签可以为某个包下所有类设置别名(匿名类、接口、局部类除外);
  • <typeAlias/> 不指定别名,则用类的非限定名作为别名;
  • <package/> 若没在类上加 @Alias 注解,则用类的非限定名作为别名;
  • 类的别名注册在一个Map中,key(别名)实际都是小写的,但mapper文件中使用时不分大小写;
  • 有些我们可以直接在Mapper文件中使用的别名(如string/int等),是Mybatis提前给注册好的。

typeHandlers 类型处理器

作用:自定义方法参数或返回体的java类型和数据库类型的映射

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
<typeHandlers>
    <!-- handler属性直接配置我们要指定的TypeHandler -->
    <typeHandler handler=""/>
    <!-- javaType 配置java类型,例如String, 如果配上javaType, 那么指定的typeHandler就只作用于指定的类型 -->
    <typeHandler javaType="" handler=""/>
    <!-- jdbcType 配置数据库基本数据类型,例如varchar, 如果配上jdbcType, 那么指定的typeHandler就只作用于指定的类型  -->
    <typeHandler jdbcType="" handler=""/>
    <!-- 也可两者都配置 -->
    <typeHandler javaType="" jdbcType="" handler=""/>
    <!--当配置package的时候,mybatis会去配置的package扫描TypeHandler-->
    <package name="com.wzt.batis"/>
</typeHandlers>

源码简析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  private void typeHandlerElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 解析 package 标签
        if ("package".equals(child.getName())) {
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          // 解析其它 typeHandler
          // 参数可以有 javaType/jdbcType,也可两者都有
          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.java
在这个类中,Mybatis已经注册了很多默认的 handler

来看几个注册方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  // 配置文件中没有配置 javaType 参数的注册方法: 
  public <T> void register(TypeHandler<T> typeHandler) {
    boolean mappedTypeFound = false;
    //  在自定义typeHandler的时候,可以加上注解MappedTypes 去指定关联的javaType
    //  因此,此处需要扫描MappedTypes注解
    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);
    }
  }
  
  // 配置了 javaType 的注册方法
  public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
    register((Type) javaType, typeHandler);
  }  

  // 三个参数齐全的注册方法
  private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
        typeHandlerMap.put(javaType, map);
      }
      map.put(jdbcType, handler);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
  }

所谓的注册和 typeAlias 一样,就是添加到Map中。

实现一个自定义的 TypeHandler

Mybatis 实现了很多默认的 handler,都是继承自 BaseTypeHandler,我们要实现自己的自定义Handler,也需要继承这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 两个注解分别对应配置文件中的 jdbcType 和 javaType 参数
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(String.class)
public class MyTypeHandler extends BaseTypeHandler<String> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter);
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getString(columnName);
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getString(columnIndex);
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getString(columnIndex);
    }
}

然后在配置文件中配置:

1
2
3
<typeHandlers>
    <typeHandler javaType="String" jdbcType="VARCHAR" handler="com.wzt.batis.UserDo"/>
</typeHandlers>

objectFactory 对象工厂

Mybatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。 默认情况下,我们不需要配置,mybatis会调用默认实现的objectFactory。 除非我们要自定义ObjectFactory的实现, 那么我们才需要去手动配置。

自定义ObjectFactory只需要去继承DefaultObjectFactory(是ObjectFactory接口的实现类),并重写其方法即可。 具体的,本处不多说,后面再具体讲解。

写好了ObjectFactory, 仅需做如下配置:

1
2
3
<objectFactory type="com.wzt.batis.MyObjectFactory">
    <property name="p1" value="v1"/>
</objectFactory>

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
  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();
      // 设置properties
      factory.setProperties(properties);
      // 添到 configuration 中
      configuration.setObjectFactory(factory);
    }
  }

plugins 插件

plugins 是一个可选配置。mybatis中的plugin其实就是个interceptor, 它可以拦截ExecutorParameterHandlerResultSetHandlerStatementHandler 的部分方法,处理我们自己的逻辑。

  1. Executor就是真正执行sql语句的东西;
  2. ParameterHandler 是处理我们传入参数的,还记得前面讲TypeHandler的时候提到过,mybatis默认帮我们实现了不少的typeHandler, 当我们不显式配置typeHandler的时候,mybatis会根据参数类型自动选择合适的typeHandler执行,其实就是ParameterHandler 在选择。
  3. ResultSetHandler 就是处理返回结果的。

要自定义一个plugin, 需要去实现Interceptor接口, 这儿不细说, 后面实战部分会详细讲解。定义好之后,配置如下:

1
2
3
4
5
<plugins>
  <plugin interceptor="org.wzt.batis.ExamplePlugin">
    <property name="p1" value="v1"/>
  </plugin>
</plugins>

源码如下:

1
2
3
4
5
6
7
8
9
10
11
  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).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

有一个常用的插件——分页插件(PageHelper),可以方便地实现分页功能:

1
2
3
4
5
6
7
8
9
10
11
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <property name="dialect" value="mysql" />
        <property name="offsetAsPageNum" value="false"/>
        <property name="rowBoundsWithCount" value="false"/>
        <property name="pageSizeZero" value="true"/>
        <property name="reasonable" value="false"/>
        <property name="supportMethodsArguments" value="false"/>
        <property name="returnPageInfo" value="none"/>
    </plugin>
</plugins>

mappers 映射文件

mappers 节点下,配置我们的mapper映射文件, 所谓的mapper映射文件,就是让mybatis 用来建立数据表和javabean映射的一个桥梁。 在我们实际开发中,通常一个mapper文件对应一个dao接口, 这个mapper可以看做是dao的实现。所以,mappers必须配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mappers>
    <!-- 第一种方式:通过resource指定 -->
    <mapper resource="mapper/userMapper.xml"/>
    
    <!-- 第二种方式, 通过class指定接口,进而将接口与对应的xml文件形成映射关系
         不过,使用这种方式必须保证 接口与mapper文件同名(不区分大小写), 
         我这儿接口是 UserMapper,那么意味着mapper文件为 UserMapper.xml 
    <mapper class="com.wzt.batis.UserMapper"/>
    -->
    
    <!-- 第三种方式,直接指定包,自动扫描,与方法二同理 
    <package name="com.wzt.batis"/>
    -->
    <!-- 第四种方式:通过url指定mapper文件位置
    <mapper url="file://........"/>
    -->
</mappers>

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 三选一
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            // 上面2种解析后都会调用该方法,本质还是放到一个Map中
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

其它

Mybatis的配置文件中,除了上面讲到的标签,还有以下几种尚未提及:

  1. <settings/>,设置能影响Mybatis运行时行为的一些参数,这些参数都是Configuration类里的属性,此处列举几个:
    • cacheEnabled:所有映射器中配置的缓存的全局开关;
    • lazyLoadingEnabled:延迟加载的全局开关;
    • useGeneratedKeys:允许 JDBC 支持自动生成主键,需要驱动兼容;
    • autoMappingBehavior:指定 MyBatis 应如何自动映射列到字段或属性[NONE, PARTIAL, FULL];
    • mapUnderscoreToCamelCase:是否开启自动驼峰命名规则映射;
    • logImpl:指定 MyBatis 所用日志的具体实现,未指定时将自动查找;
  2. <databaseIdProvider/>,使 MyBatis 可以根据不同的数据库厂商执行不同的语句;
  3. <objectWrapperFactory/>:不知啥用,直到了再补
  4. <reflectorFactory/>:同上

总结

本文介绍了Mybatis配置文件中出现的相关标签的功能作用,并顺带简单地过了一下其源码的解析过程。 相信看完此文,对Mybatis的配置文件多少能有一点了解。 接下来,将会继续深入源码,看一些更有意思的东西。

参考文章