Mybatis源码解读-初始化过程详解

在使用Mybatis时,我们通常将其配置在Spring容器中,当Spring启动的时候会自动加载Mybatis的所有配置文件然后生成注入到Spring中的Bean,本文从实用的角度进行Mybatis源码解读,会关注以下一些方面:

  • Mybatis都有哪些配置文件和配置项
  • Mybatis初始化的源码流程;
  • Mybatis初始化后,产生了哪些对象;

Mybatis初始化环境并且执行SQL语句的JAVA代码

先看一段初始化Mybatis环境并且执行SQL语句的Java代码:

package org.apache.ibatis.session;
import java.io.Reader;
import org.apache.ibatis.io.Resources;
public class MyTest {
    public static void main(String[] args) throws Exception {
        // 开始初始化
        final String resource = "org/apache/ibatis/builder/MapperConfig.xml";
        final Reader reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        // 开始执行SQL
        SqlSession session = sqlMapper.openSession();
        Integer count = session.selectOne("org.apache.ibatis.domain.blog.mappers.BlogMapper.selectCountOfPosts");
        System.out.println(count);
    }
}

这段代码完成了这些事情:

  1. 读取Mybatis的配置文件
  2. 构建SqlSessionFactory
  3. 从SqlSessionFactory中创建一个SqlSession
  4. 使用SqlSession执行一个select语句,参数是Mapper.java的一个方法名
  5. 打印结果

在这里前三行代码包括读取配置文件和创建SqlSessionFactory,这就是Mybatis的一次初始化过程。

如果查看一下Spring配置Mybatis的文件,就会发现它使用mybatis-spring的包也主要是初始化了这个SqlSessionFactory对象:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:conf/MapperConfig.xml" />
        <property name="mapperLocations">
            <list>
                <value>classpath*:mapper
/*.xml</value>
            </list>
        </property>
    </bean>

该Spring配置sqlSessionFactory接收了三个参数,分别是数据源dataSource、Mybatis的主配置文件MapperConfig.xml、mapper.xml文件的扫描路径。

可以看出Mybatis的初始化过程就是读取配置文件然后构建出sqlSessionFactory的过程。

Mybatis都有哪些配置文件和配置项?

上面的Java代码中初始化Mybatis只使用了配置文件MapperConfig.xml,然而在Spring配置文件中构建sqlSessionFactory时也使用了mapper.xml配置文件,其实Mybatis最多也就这两类文件,主配置文件MapperConfig.xml可以通过<mappers>XML元素包含普通的mapper.xml配置文件。

主配置文件:MapperConfig.xml
一个包含了所有属性的MapperConfig.xml实例:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="org/apache/ibatis/databases/blog/blog-derby.properties" />
    <settings>
        <setting name="cacheEnabled" value="true" />
        <setting name="lazyLoadingEnabled" value="false" />
        <setting name="multipleResultSetsEnabled" value="true" />
        <setting name="useColumnLabel" value="true" />
        <setting name="useGeneratedKeys" value="false" />
        <setting name="defaultExecutorType" value="SIMPLE" />
        <setting name="defaultStatementTimeout" value="25" />
    </settings>
    <typeAliases>
        <typeAlias alias="Author" type="org.apache.ibatis.domain.blog.Author" />
        <typeAlias alias="Blog" type="org.apache.ibatis.domain.blog.Blog" />
    </typeAliases>
    <typeHandlers>
        <typeHandler javaType="String" jdbcType="VARCHAR"
            handler="org.apache.ibatis.builder.CustomStringTypeHandler" />
    </typeHandlers>
    <objectFactory type="org.apache.ibatis.builder.ExampleObjectFactory">
        <property name="objectFactoryProperty" value="100" />
    </objectFactory>
    <plugins>
        <plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">
            <property name="pluginProperty" value="100" />
        </plugin>
    </plugins>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC">
                <property name="" value="" />
            </transactionManager>
            <dataSource type="UNPOOLED">
                <property name="driver" value="${driver}" />
                <property name="url" value="${url}" />
                <property name="username" value="${username}" />
                <property name="password" value="${password}" />
            </dataSource>
        </environment>
    </environments>
    <databaseIdProvider type="DB_VENDOR">
        <property name="SQL Server" value="sqlserver" />
        <property name="DB2" value="db2" />
        <property name="Oracle" value="oracle" />
    </databaseIdProvider>
    <mappers>
        <mapper resource="org/apache/ibatis/builder/AuthorMapper.xml" />
        <mapper resource="org/apache/ibatis/builder/BlogMapper.xml" />
    </mappers>
</configuration>

主配置文件只有一个XML节点,就是configuration,它包含9种配置项:

  1. properties 属性:在这里配置的属性可以在整个配置文件中使用来替换需要动态配置的属性值
  2. settings 设置:MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为,比如是否使用缓存和日志记录的方式
  3. typeAliases 类型命名:类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。
  4. typeHandlers 类型处理器:无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。
  5. objectFactory 对象工厂:MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。
  6. plugins 插件:MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用,比如增加分页功能、格式化输出最终的SQL等扩展;
  7. environments 环境:MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中,比如设置不同的开发、测试、线上配置,在每个配置中可以配置事务管理器和数据源对象;
  8. databaseIdProvider 数据库厂商标识:MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。
  9. mappers 映射器:MyBatis 的行为已经由上述元素配置完了,通过这里配置的mappers文件,我们可以去查找定义 的SQL 映射语句用于执行;

可以看出,前8个配置项用户设定Mybatis运行的一些环境,而第9个mappers映射器才是需要执行的SQL的配置,在正常情况下,我们只需要配置第9个mapper映射器的地址即可,前面的的Mybatis行为配置都有默认值正常情况下不需要设定。

包含最终SQL的mapper映射器配置文件

虽然我们经常写mapper文件,知道有select/insert/update/delete四种元素,还有sql/resultmap等配置项,就感觉配置项好多好多,但其实mapper总共也就8种配置,我们常用的6种就包含在内:

  1. cache – 给定命名空间的缓存配置。
  2. cache-ref – 其他命名空间缓存配置的引用。
  3. resultMap – 是最复杂也是最强大的元素,用于实现数据库表列和Java Bean的属性名的映射配置;
  4. sql – 可被其他语句引用的可重用语句块。
  5. insert – 映射插入语句
  6. update – 映射更新语句
  7. delete – 映射删除语句
  8. select – 映射查询语句

正常情况下,我们很少使用Mybatis提供的cache机制而是使用外部的Redis等缓存,所以这里的1和2的cache配置几乎不会使用,主要也就是我们平时使用的6种配置。

以上就是Mybatis所有提供给我们配置的地方,改变Mybatis行为的有8个配置项,每个XML配置文件刚好也最多有8个配置项,总共有16个配置项。

Mybatis初始化的源码流程

阅读Mybatis源码最好的方式,就是从源码中的单测作为入口,然后DEBUG一步步的执行,在自己关注的地方多多停留一会仔细查看。

以下以代码的流程进行解析,只贴出主要的代码块:

Mybatis代码初始化入口

@BeforeClass
    public static void setup() throws Exception {
    createBlogDataSource();
    final String resource = "org/apache/ibatis/builder/MapperConfig.xml";
    final Reader reader = Resources.getResourceAsReader(resource);
    sqlMapper = new SqlSessionFactoryBuilder().build(reader);
}

这里看到,进入了new SqlSessionFactoryBuilder().build(reader)方法。

进入SqlSessionFactoryBuilder的build方法

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        return build(parser.parse());
    }
    catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    }
    finally {
        ErrorContext.instance().reset();
        try {
            reader.close();
        }
        catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}

主要两行在try块内,第一行的内容是调用XPathParser加载了Mybatis的主配置文件,而第二步包含两个步骤,parser.parse()方法返回的是一个Configuration对象,包裹它的build方法只有一行代码:

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

这就可以看出,其实初始化过程就是创建Configuration对象的过程,对照MapperConfig.xml的根元素是<configuration>,不难猜测到Configuration是一个非常重要的、包含了Mybatis所有数据配置的对象。

Mybatis核心对象Configuration的构建过程

接下来进入了XMLConfigBuilder.parse()方法,该方法解析XML文件的/configuration节点,然后挨个解析了上面配置文件中提到的9大配置:

public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
private void parseConfiguration(XNode root) {
    try {
        // issue #117 read properties first
        propertiesElement(root.evalNode("properties"));
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        mapperElement(root.evalNode("mappers"));
    }
    catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

我们挨个查看,这些配置项的解析,都产出了什么内容;

1、properties配置项的解析

进入propertiesElement方法,我们发现初始化了一个Properties对象,将XML中所有的子节点按照KEY-VALUE存入properties之后,和Configuration.variables变量进行了合并,而Configuration.variables本身,也是个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) {
            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配置解析后合并到Configuration.variables之后,后续的配置文件都可以使用这些变量。

2、setting的配置读取

setting配置的读取,包含两个步骤,第一步,将XML中所有的配置读取到properties对象:

private Properties settingsAsProperties(XNode context) {
    if (context == null) {
        return new Properties();
    }
    Properties props = context.getChildrenAsProperties();
    // Check that all settings are known to the configuration class
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    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的配置项,通过反射访问Configuration.class,如果不存在某个配置项的set方法则报错;

然后在settingsElement方法中,将这些读取的配置项存入了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")));
    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")));
}

因为setting变量直接改变的是Mybatis的行为,所以配置项直接存于Confirguration的属性中。

3、typeAliases配置的解析

进入typeAliasesElement方法,用于对typeAliases配置的解析:

private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String typeAliasPackage = child.getStringAttribute("name");
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
                String alias = child.getStringAttribute("alias");
                String type = child.getStringAttribute("type");
                try {
                    Class<?> clazz = Resources.classForName(type);
                    if (alias == null) {
                        typeAliasRegistry.registerAlias(clazz);
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                    }
                }
                catch (ClassNotFoundException e) {
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                }
            }
        }
    }
}

该方法将typeAliases的配置项提取之后,存入了typeAliasRegistry这个对象,该对象是在BaseBuilder中初始化的:

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类中,我们看到了该对象的声明:

protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

打开该类的代码,发现特别简单的,用一个MAP存储了别名和对应的类的映射:

public class TypeAliasRegistry {
    private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
    public TypeAliasRegistry() {
        registerAlias("string", String.class);
        registerAlias("byte", byte.class);
        registerAlias("long", long.class);
        registerAlias("short", short.class);
        registerAlias("int", Integer.class);
        registerAlias("integer", Integer.class);
        registerAlias("double", double.class);
        registerAlias("float", float.class);
        registerAlias("boolean", Boolean.class);

在构造函数中Mybatis已经默认注册了一些常用的别名和类的关系,所以我们可以在mappers的xml文件中使用这些短名字。

4、typeHandlers配置元素的解析

mybatis提供了大部分数据类型的typeHandlers,如果我们要定制自己的类型处理器比如实现数据库中0/1两个数字到中文男/女的映射,就可以自己实现typeHandler

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

在该方法中,通过反射得到了javaTypeClass、jdbcType、typeHandlerClass三个变量,这三个变量组成(javaType、jdbcType、typeHandler)三元组,当遇到javaType到jdbcType的转换,或者遇到jdbcType到javaType的转换时就会使用该typeHandler。

然后该方法调用了TypeHandlerRegistry.register进行注册,TypeHandlerRegistry对象是从BaseBuilder中的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();
    }

在TypeHandlerRegistry中,建立了几个Map映射:

public final class TypeHandlerRegistry {
    private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(
                JdbcType.class);
    private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<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<?>>();

第一个是JdbcType为key的map,第二个是JavaType为key的map,第三个是未知的处理器、最后一个是包含全部的处理器;

当执行SQL的时候,会将javaBean的JavaType转换到DB的jdbcType,而查询出来数据的时候,又需要将jdbcType转换成javaType,在TypeHandlerRegistry的构造函数中,已经注册好了很多默认的typeHandler,大部分情况下不需要我们添加:

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

要实现一个typeHandler,需要实现接口,该接口提供的就是从javaType到jdbcType的setParameter方法,以及从jdbcType到javaType转换的getResult方法:

public interface TypeHandler<T> {
    void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
    T getResult(ResultSet rs, String columnName) throws SQLException;
    T getResult(ResultSet rs, int columnIndex) throws SQLException;
    T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}

5、objectFactory配置项的解析

如果想自己控制查询数据库的结果到JavaBean映射的生成,则可以创建自己的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);
    }
}

可以看到,该配置项包含type属性,以及properties子节点,创建好ObjectFactory对象后,就会设置到configuration中:

// Configuration对象的objectFactory成员变量
protected ObjectFactory objectFactory = new DefaultObjectFactory();

要实现ObjectFactory,需要继承该接口:

public interface ObjectFactory {
    void setProperties(Properties properties);
    <T> T create(Class<T> type);
    <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);
    <T> boolean isCollection(Class<T> type);
}

该工厂接口提供了设置属性列表,还有创建对象的工厂方法。

6、plugin元素的解析

plugin,即mybatis的插件,可以让我们自己进行开发用于扩展mybatis。

进入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 interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

该段代码,首先获取intercepter元素作为拦截器,然后读取该节点的所有子节点作为配置项,最后调用configuration.addInterceptor方法添加到了configuration中的interceptorChain中,该对象是拦截器链的一个包装对象:

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            // target变量每次也在变化着
            target = interceptor.plugin(target);
        }
        return target;
    }
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }
    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }
}

该类中使用List<Interceptor>存储了所有配置的拦截器,并提供了addInterceptor用于添加拦截器,提供了getInterceptors用于获取当前所有添加的插件列表,提供了pluginAll接口调用所有的Interceptor.plugin(Object)方法进行插件的执行。

7、environments的配置解析

environments可以配置多个环境配置,每个配置包含了数据源和事务管理器两项,如下所示代码:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            if (isSpecifiedEnvironment(id)) {
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                DataSource dataSource = dsFactory.getDataSource();
                Environment.Builder environmentBuilder = new Environment.Builder(id).transactionFactory(txFactory)
                                            .dataSource(dataSource);
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

代码中通过isSpecifiedEnvironment方法判断当前的id是不是指定要读取的environment,如果是的话通过反射获取事务管理器和数据源,然后用Environment.Builder创建Enviroment对象并设置到Configuration中,在Configuration中可以看到Enviroment成员变量:

protected Environment environment;

而Enviroment对象也只包含了这三个属性:

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

8、databaseIdProvider配置项的解析

mybatis当然不只是支持mysql,也会支持oracle、sqlserver等不同的数据库,解析代码如下:

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

解析databaseIdProvider后里面的Properties中存储了各种数据库的映射,并且databaseIdProvider提供了一个根据dataSource获取对应的databseId的方法,以VendorDatabaseIdProvider为例,是通过connection.getMetaData().getDatabaseProductName()获取数据库的产品名称,然后从刚才databaseIdProvider中获取对应的databaseId:

public class VendorDatabaseIdProvider implements DatabaseIdProvider {
    private static final Log log = LogFactory.getLog(VendorDatabaseIdProvider.class);
    private Properties properties;
    @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;
    }
    @Override
        public void setProperties(Properties p) {
        this.properties = p;
    }
    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
                }
            }
        }
    }
}

在获取了databaseId之后,最后将databaseId设置到configuration,后续当执行SQL的时候会自动根据该databaseId来映射具体数据库的SQL。

9、mappers配置项的解析

mappers的解析最为复杂,我们假设mapper文件均是url指定的xml文件,来进行解析流程的查看:

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加载mapper.xml,然后进入parse()方法
                    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.");
                }
            }
        }
    }
}

加注释部分显示,读取每个mapper.xml资源文件的地址后,进入了XMLMapperBuilder.parse()方法:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
}
private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    }
    catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
}

然后进入了configurationElement(parser.evalNode(“/mapper”));方法后会读取所有xml中mapper下的子元素,在这里我们只查看buildStatementFromContext方法:

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant,
                            context, requiredDatabaseId);
        try {
            statementParser.parseStatementNode();
        }
        catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

该方法出现了一个XMLStatementBuilder用于select/insert/update/delete语句各自的解析,在XMLStatementBuilder.parseStatementNode方法中解析了各种语句的属性和参数以及动态SQL的处理,最后调用builderAssistant.addMappedStatement方法,所有的参数和内容被构建成MappedStatement,添加到了configuration中:

MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);

以下是configuration中的mappedStatements对象:

// Configuration的mappedStatements对象
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(
            "Mapped Statements collection");

这里的StrictMap就是一个HashMap,在addMappedStatement方法中可以看到该map的Key是各个SQL的ID:

public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
}

由此可以推测整个Mybatis执行SQL的过程:

  1. 业务代码调用SqlMapperInterface.method方法;
  2. Mybatis根据method的全限定名称作为ID,从mappedStatements找到构建好的MappedStatement对象;
  3. 使用MappedStatement中读取的SQL等各种配置,执行SQL;

Mybatis初始化的对象产出列表总结

以上就是对Mybatis初始化过程的详解,其最终产出了以下对象列表:

  1. 总产出:SqlSessionFactory,相当于ConnectionFactory用于生产SqlSession,而SqlSession相当于Connection用于实际的SQL查询;

  2. SQlSessionFactory的核心对象Configuration,所有的Mybatis配置项和Mapper配置列表,都会被解析并读取到该对象的属性中;

  3. Configuration中的各个对象:

    • Configuration.variables,类型为Properties,存储Mybatis配置的Properties对象;
    • Configuration.直接属性,setting的配置,因为直接影响Mybatis的行为,直接赋值到了Configuration对象的属性;
    • Configuration.TypeAliasRegistry对象,存储别名映射,KEY是别名,VALUE是别名对应的类;
    • Configuration.TypeHandlerRegistry对象,存储typeHandler映射,KEY是javaType或者jdbcType,VALUE是typeHandler类;
    • Configuration.ObjectFactory对象,存储单个的ObjectFactory对象,用于DB映射JAVABEAN对象的创建;
    • Configuration.InterceptorChain对象,存储Plugin对象列表,用户可以添加用于扩展Mybatis;
    • Configuration.Environment对象,指定开发/测试/线上环境,根据其dataSource也决定了databaseId对象的取值;
    • Configuration.databaseId,存储使用的DB的类型,Mybatis会根据不同的DB做SQL适配;
    • Configuration.mappedStatements,存储KEY为ID,VALUE为MappedStatement的MAP,执行SQL时从此处获取对象;

写在最后

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容