Mybatis配置文件解析过程详解

记录是一种精神,是加深理解最好的方式之一。

最近看了下Mybatis的源码,了解了下Mybatis对配置文件的解析过程,在这里把他记下来。虽然这不复杂,对这方面的博客也有很多,写的也很好。但我坚信看懂了是其一,能够教别人或者描述清楚记下来才能真正的掌握。
曹金桂 [email protected] (如有欠缺还请指教)
时间:2016年10月9日16:00

这篇文章能够帮你
  • 学会如何对Mybatis进行有效配置,理解对应的配置含义,知其然知其所以然。
  • 学会在Mybatis默认实现无法满足需求的时候怎么去扩展。

从构建SqlSessionFactory说起

   Mybatis的核心对象就是SqlSession,它封装了框架方法数据库的所有操作。使用Mybatis框架第一件事就是获取SqlSession对象。那SqlSession对象从何而来呢?Mybatis提供了工厂对象(SqlSessionFactory)来构建SqlSession。那么我们下面来看下Mybatis是怎么通过读取XML配置文件来构建SqlSessionFactory工厂对象的。
   以下代码是我们使用Mybatis的代码示例:

InputStream stream = SqlSessionFactory.class.getClassLoader().getResourceAsStream("Mybatis-conf.xml");
SqlSessionFactory  sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 调用Mybatis进行数据库操作代码 ......
sqlSession.close();

   我们知道,SqlSessionFactory是通过SqlSessionFactoryBuilder来构建的。我们继续看下SqlSessionFacotyBuilder的build方法。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        Configuration config = parser.parse(); //Mybatis框架配置对象
        return new DefaultSqlSessionFactory(config);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        try {
            inputStream.close();
        } catch (IOException e) {}
    }
}

   以上代码可以发现,Mybatis是通过XMLConfigBuilder对象来解析配置文件,生成Mybatis的配置对象ConfigurationConfiguration对象是Mybatis的基础,包含了框架所有的配置。Mybatis在运行时,都是通过此对象的属性来构建JDBC操作的封装。此篇文章就是详细说明XMLConfigBuilder是怎么来解析我们的Mybatis的XMl配置文件,生成Configuration对象的。知道了框架怎么解析配置,那自然懂得怎么去有效配置Mybatis框架,也即能够熟练使用Mybatis。

XMLConfigBuilder配置解析

Mybatis配置文件解析过程详解_第1张图片
   知道了Mybatis框架是通过XmlConfigBuilder来解析配置文件生成需要的Configuation配置对象的。我们自己看下XmlConfigBuilder的parse()解析方法:

// 解析mybatis的配置文件,返回Configuration对象
public Configuration parse() {
    if (parsed) { //重复解析控制
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    XNode root = parser.evalNode("/configuration");
    try {
        propertiesElement(root.evalNode("properties"));
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        settingsElement(root.evalNode("settings"));
        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);
    }
    return configuration;
}

   源码中我们看到有个XNode对象,这个对象是Mybatis框架中解析XML文件的对象。这个对象封装了很多XML解析中的通用方法,在平常开发中,如果需要对XML解析,可以直接使用,非常方便(大家有兴趣可以看下它的源码)。另外,对于XML配置文件的解析,我们应该同其约束文件一起来分析其解析过程(对于其他框架亦是如此)。

typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>

在约束文件中看到,Mybatis配置文件根节点只能包含properties, typeAliases, plugins等几个节点。那对应Mybatis的配置文件解析,当然也是对这几个节点进行解析了。那下面我们就对各个节点的配置进行详细说明。

1. 解析properties配置

   如果用过Spring就知道,Spring的配置文件中可以配置PropertyPlaceholderConfigurer对象,用于其XML配置中的变量配置。而Mybatis的properties标签于其作用一样。Mybatis配置文件的properteis节点DTD约束如下:

property*)>
#IMPLIED
        url CDATA #IMPLIED
>

   我们可以配置resource和url属性,也可以有property子标签。那我们看下Mybatis是怎么解析的。

// 解析properties配置
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) { // resource和url只能同时配置一个
            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);
        }
        configuration.setVariables(defaults);        // 将属性设置到Configuration对象
        parser.setVariables(defaults);
    }
}

我们可以看出,properties标签是不能同时配置resource和url属性的。并且,properties下的property标签的值会覆盖resource或者url外部的联接文件。properties标签就是这么简单,解析完生成java.util.Properties对象设置到Configuration对象属性中,供其他配置使用。当然,其他配置项必须是property标签, 如:

2. 解析typeAliases配置

Mybatis中的别名就是用来简化配置的。如果有一对象com.tianba.mybatis.domain.User,我们要配置SQL查询的返回结果是User对象,那么我们需要在Select标签中的resultType属性值为com.tianba.mybatis.domain.User。使用别名之后,我们给这个对象定义别名为user,那么我们在用到这个对象的配置时候,只要写它的别名即可,无需写类的全名。先看Mybatis怎么配置别名。

<typeAliases>
    <typeAlias type="com.tianba.mybatis.domain.User" alias="user"/>
    "com.tianba.mybatis.domain"/>
typeAliases>

当然,如果项目中有很多个POJO对象,也不需要我们一个一个的配置,只需要指定package即可。

private void typeAliasesElement(XNode parent) {
    for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
            // package 配置
            String typeAliasPackage = child.getStringAttribute("name");
            typeAliasRegistry.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);
            }
        }
    }
}

我们发现,Mybatis的别名是通过TypeAliasRegistry对象来注册的。TypeAliasRegistry其实很简单,使用map来保存Class的别名。它有多个registerAlias方法,可以提供package,这样的话他默认使用类名来做别名(com.tianba.mybatis.domain.User -> User),另外也可以使用@Alias注解在类上标记别名。具体可以看它的源码。PS:TypeAliasRegistry中使用ResolverUtil来获取package下所有的Class。这个工具类也很实用哦。
另外,Mybatis在创建Configuration对象的时候就已经添加了不少别名,相关别名可以参考Configuration的构造函数和TypeAliasRegistry的构造函数。

3. 解析plugins配置

MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。我们这里只介绍拦截器怎么配置,及Mybatis怎么解析拦截器的配置(拦截器的原理详解请关注我的文集,后续会继续推出相关文章)。配置示例:

<plugins>
    <plugin interceptor="com.tianba.mybatis.interceptor.PageHelper">
        <property name="defaultPageSize" value="20" />
    plugin>
    <plugin interceptor="com.tianba.mybatis.interceptor.CloseLocalCacheInterceptor" />
plugins>

继续看下Mybatis怎么解析

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

// configuration.addInterceptor
public void addInterceptor(Interceptor interceptor) {
   interceptorChain.addInterceptor(interceptor); //拦截器执行链
}

Mybatis的配置对象中维护着一个拦截器执行链对象InterceptorChain,解析plugins也就是简单的网执行链中添加一个拦截器对象。当然,拦截器类的@Intercepts注解是必须的。

4. 解析settings配置

Mybatis中的setting配置和properties配置一样,也是配置参数名和参数值。但到底和properteis有什么区别呢?这个也是我当时使用Mybatis中困恼的一个问题。但看了源码之后就很清晰了。我们知道properties的配置参数是为其他的配置服务的,配置项不是不定的。而settings的配置项是配置Configuration对象的属性的,配置项定死就那么几个,不配的话框架有默认值。当然setting的范围没有在XML中约束,个人觉得Mybatis团队应该把这个约束加上。所以,没看源码是不清晰settings配置的。我们先看源码:

// 解析settings配置,对应Configuration对象属性的set方法
private void settingsElement(XNode context) throws Exception {
    if (context != null) {
        Properties props = context.getChildrenAsProperties();
        // 检查所有配置的key在Configuration中是否有对应Set方法
        MetaClass metaConfig = MetaClass.forClass(Configuration.class);
        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).");
            }
        }
        configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
        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"), true));
        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.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
        configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
        configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
        configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
        configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
        configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
        configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
        configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
        configuration.setLogPrefix(props.getProperty("logPrefix"));
        configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
        configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
    }
}

一看源码就清晰了,settings配置能配哪些属性都在这里了。那具体这些属性该配置什么呢?从官方文档截图如下:
Mybatis配置文件解析过程详解_第2张图片

5. 解析environments配置

如果在项目中Mybatis和Spring配合使用,那environments的配置是省略的。此配置项是配置数据库连接池和事物管理的。若何Spring配合使用,则事务的管理和数据库连接池一般都是交给Spring控制。先看配置的示例:

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

在environments标签下可以配置多个environment标签。这个怎么理解呢?在我们开发的时候有关数据库的配置肯定都是生产和测试分开的。那我们这里可以配置两个environment节点,product对应生产,development对应测试开发。当我们环境切换的时候,只要把environments的default属性改成要使用的environment的id属性即可。

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // 解析environments节点的default属性的值 例如: 
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            if (isSpecifiedEnvironment(id)) { //isSpecial就是根据由environments的default属性去选择对应的enviroment
                // 事务, mybatis有两种:JDBC 和 MANAGED, 配置为JDBC则直接使用JDBC的事务,配置为MANAGED则是将事务托管给容器,
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // enviroment节点下面就是dataSource节点了,解析dataSource节点
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                DataSource dataSource = dsFactory.getDataSource();
                configuration.setEnvironment(new Environment(id, txFactory, dataSource));
            }
        }
    }
}

从源码看出来,我们的transactionManager中的type属性即配置了事物工厂的实现类,当然,这里使用的就是别名。dataSource中的type也是配置了DataSourceFactory的实现类。在Configuration初始化时候,已经添加了这两个工厂接口对应的实现类别名了。

public Configuration() {
   //TransactionFactory
   typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); // Mybatis内部的JDBC事务管理器
   typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); // 外部容器事务管理器
   //DataSourceFactory
   typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); // Jndi数据源
   typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
   typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
   // other alias
   .......
}

那这两个type的配置和其含义已经很清晰了。具体区别可以看其对应实现类的源码,很简单明了。如果这些都不能满足需求,那我可以还自定义自己的实现,只有实现相应的接口即可,然后在type属性配置上我们自己的实现类(package.className)即可。从这里看出,Mybatis的扩展还是做得很好的。

6. 解析typeHandlers配置

Java有java的数据类型,数据库有数据库的数据类型,那么Mybatis在往数据库中插入数据的时候是如何把java类型转换成数据库类型插入数据库,在从数据库读取数据的时候又是如何把数据库类型转换成java类型来处理呢?这中间必然要经过一个类型转换。Mybatis中提供一个叫做TypeHandler类型处理器的东西,通过它可以实现Java类型跟数据库类型的相互转换。我们先看配置

<typeHandlers>
    <typeHandler handler="com.tianba.mybatis.typehandlers.CustomTimeStampHandler" javaType="java.util.Date" jdbcType="VARCHAR" />
    <package name="com.tianba.mybatis.typehandlers" />
typeHandlers>

看配置,和typeAliases的配置项很相似。

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

是的,和typeAliases类似,typeHandler也是通过typehandlerRegistry来注册的,只是在注册的时候要确定javaType和对应的jdbcType。如果是通过package扫描注册的,则必须在类上通过注解标记。大部分情况下,Mybatis提供的类型转换已经足够我们使用了,在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());
    ......
}

要是有需要自定义类型转换器,也很简单。参考Mybatis内部类型转换实现类,依葫芦画瓢。

7. 解析mappers配置

在Mybatis配置文件解析中,对Mapper的解析是最复杂的。Mapper的解析是由XMLMapperBuilder对象来进行的。先看配置

<mappers>
    <mapper resource="MybatisConfig/UserMapper.xml"/>
    <mapper class="com.tianba.mybatis.persistence.UserMapper"/>
    <mapper url="http://www.52tianba.com/xxx/xxx"/>
mappers>

我们看到,可以配置Mapper.xml,还可以配置接口。配置接口的话那必须保证所对应的xml文件名和文件路径都和类保持一致。

private void mapperElement(XNode parent) throws Exception {
    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);
                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进行解析的,后面我会出一篇文章专门分析Mybatis怎么解析Mapper.xml文件的(期待吧…)。

小结

Mybatis配置文件解析过程其实就是把XML文件的配置解析成Configuration对象。当然,如果你熟悉的话完全可以不需要使用他的配置,直接使用代码初始化Configuration对象也是一样的。我相信只要你理解了这篇文章,你完全可以做到。

你可能感兴趣的:(Mybatis随笔)