MyBatis源码剖析

1. MyBatis架构原理

1.1. 架构设计

MyBatis源码剖析_第1张图片
我们把Mybatis的功能架构分为三层:

  1. API接口层:提供给外部使用的接口 API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

    MyBatis和数据库的交互有两种方式:

    (1)使用传统的MyBati s提供的API ;

    (2)使用Mapper代理的方式

  2. 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

  3. 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

1.2. 主要构件及其相互关系

构件 描述
SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
Executor MyBatis执行器,是MyBatis调度的核心,负责SQL语句的生成和查询缓 存的维护
StatementHandler 封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参数、将Statement结果集转换成List集合。
ParameterHandler 负责对用户传递的参数转换成JDBC Statement所需要的参数,
ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
MappedStatement MappedStatement维护了一条select、delete、update、insert节点的封装
SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息

MyBatis源码剖析_第2张图片

1.3. 总体流程

  1. 加载配置并初始化

    触发条件:加载配置文件
    配置来源于两个地方,一个是配置文件(主配置文件conf.xml,mapper文件*.xml),—个是java代码中的 注解,将主配置文件内容解析封装到Configuration,将sql的配置信息加载成为一个Mappedstatement 对象,存储在内存之中。

  2. 接收调用请求
    触发条件:调用Mybatis提供的API
    传入参数:为SQL的ID和传入参数对象
    处理过程:将请求传递给下层的请求处理层进行处理。

  3. 处理操作请求
    触发条件:API接口层传递请求过来
    传入参数:为SQL的ID和传入参数对象
    处理过程:
    ​ ① 根据SQL的ID查找对应的MappedStatement对象。
    ​ ② 根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。
    ​ ③ 获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。
    ​ ④ 根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理结果。
    ​ ⑤ 释放连接资源。

  4. 返回处理结果
    将最终的处理结果返回。

2. 传统方式源码剖析

public class MybatisTest {
     

    /**
     * 传统方式
     *
     * @throws IOException
     */
    public void test1() throws IOException {
     
        // 1. 读取配置文件,读成字节输入流,注意:现在还没解析
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        // 2. (1)解析配置文件,封装Configuration对象  (2)创建DefaultSqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        // 3. 生产了DefaultSqlsession实例对象   设置了事务不自动提交  完成了executor对象的创建
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 4.(1)根据statementid来从Configuration中map集合中获取到了指定的MappedStatement对象
        //(2)将查询任务委派了executor执行器
        List<Object> objects = sqlSession.selectList("namespace.id");

        // 5.释放资源
        sqlSession.close();
    }
}

2.1. 读取配置文件,读成字节输入流

首先调用Resources中getResourceAsStream()方法读取MyBatis核心配置文件,读成字节输入流

// 1. 读取配置文件,读成字节输入流,注意:现在还没解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

Resources类

/**
     * Returns a resource on the classpath as a Stream object
     *
     * @param resource The resource to find
     * @return The resource
     * @throws java.io.IOException If the resource cannot be found or read
     */
    public static InputStream getResourceAsStream(String resource) throws IOException {
     
        return getResourceAsStream(null, resource);
    }

    /**
     * Returns a resource on the classpath as a Stream object
     *
     * @param loader   The classloader used to fetch the resource
     * @param resource The resource to find
     * @return The resource
     * @throws java.io.IOException If the resource cannot be found or read
     */
    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;
    }

2.2. 解析配置文件

// 2. (1)解析配置文件,封装Configuration对象  (2)创建DefaultSqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

首先看下构建SqlSessionFactory的整个流程,1:解析核心配置文件;2:创建DefaultSqlSessionFactory对象

MyBatis源码剖析_第3张图片SqlSessionFactoryBuilder类

	// 1.我们最初调用的build
    public SqlSessionFactory build(InputStream inputStream) {
     
        //调用了重载方法
        return build(inputStream, null, null);
    }

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

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

    // 2.调用的重载方法
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
     
        try {
     
            // 创建 XMLConfigBuilder, XMLConfigBuilder是专门解析mybatis的配置文件的类
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            // 执行 XML 解析
            // 创建 DefaultSqlSessionFactory 对象
            return build(parser.parse());
        } catch (Exception e) {
     
            throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
     
            ErrorContext.instance().reset();
            try {
     
                inputStream.close();
            } catch (IOException e) {
     
                // Intentionally ignore. Prefer previous error.
            }
        }
    }

  MyBatis在初始化的时候,会将MyBatis的配置信息全部加载到内存中,使用Configuratio容器实例来维护

2.3. 下面进入对配置文件解析部分

2.3.1. Configuration

首先对Configuration对象进行介绍:

  1. Configuration对象的结构和xml配置文件的对象几乎相同。
  2. 回顾一下xml中的配置标签有哪些: properties (属性),settings (设置),typeAliases (类型别名),typeHandlers (类型处理器),objectFactory (对象工厂),mappers (映射器)等 Configuration也有对应的对象属性来封装它们
  3. 也就是说,初始化配置文件信息的本质就是创建Configuration对象,将解析的xml数据封装到Configuration内部属性中

2.3.2. MapperStatement

  作用:MappedStatement与Mapper配置文件中的一个select/update/insert/delete节点相对应。mapper中配置的标签都被封装到了此对象中,主要用途是描述一条SQL语句。

  初始化过程:回顾刚开 始介绍的加载配置文件的过程中,会对mybatis-config.xm l中的各个标签都进行解析,其中有mappers 标签用来引入mapper.xml文件或者配置mapper接口的目录。

	<select id="findAll" resultType="com.cyd.pojo.User">
        select * FROM user
    </select>

  像这样的一个select标签会在初始化配置文件时被解析封装成一个MappedStatement对象,然后存储在Configuration对象的mappedStatements属性中,mappedStatements 是一个HashMap,存储时key=全限定类名+方法名,value =对应的MappedStatement对象。

在configuration中对应的属性为

   /**
     * MappedStatement 映射
     *
     * KEY:`${namespace}.${id}`
     */
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection");

2.3.3. XMLConfigBuilder中parse()解析配置文件

MyBatis将核心配置文件交由XMLConfigBuilder的parse()方法来解析

   /**
     * 解析 XML 成 Configuration 对象。
     *
     * @return Configuration 对象
     */
    public Configuration parse() {
     
        // 若已解析,抛出 BuilderException 异常
        if (parsed) {
     
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        // 标记已解析
        parsed = true;
        // parser是XPathParser解析器对象,读取节点内数据,是MyBatis配置文件中的顶层标签
        // 解析 XML configuration 节点
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }

    /**
     * 解析 XML
     * 

* 具体 MyBatis 有哪些 XML 标签,参见 《XML 映射配置文件》http://www.mybatis.org/mybatis-3/zh/configuration.html * * @param root 根节点 */ private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 解析 标签 propertiesElement(root.evalNode("properties")); // 解析 标签 Properties settings = settingsAsProperties(root.evalNode("settings")); // 加载自定义的 VFS 实现类 loadCustomVfs(settings); // 解析 标签 typeAliasesElement(root.evalNode("typeAliases")); // 解析 标签 pluginElement(root.evalNode("plugins")); // 解析 标签 objectFactoryElement(root.evalNode("objectFactory")); // 解析 标签 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 解析 标签 reflectorFactoryElement(root.evalNode("reflectorFactory")); // 赋值 到 Configuration 属性 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); } }

2.3.3.1. 主要介绍一下如何解析mappers标签

一般我们在MyBatis核心配置文件中可以使用以下四种方式配置映射文件的位置

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

        <!-- 使用相对于类路径的资源引用 -->
        <mapper resource="UserMapper.xml"/>
        <!-- 使用完全限定资源定位符(URL) -->
        <mapper url="file:///xxx/UserMapper.xml"/>
    </mappers>
	/**
     * 解析  标签
     *
     * @param parent 标签
     * @throws Exception
     */
    private void mapperElement(XNode parent) throws Exception {
     
        if (parent != null) {
     
            // 遍历子节点
            for (XNode child : parent.getChildren()) {
     
                // 如果是 package 标签,则扫描该包
                if ("package".equals(child.getName())) {
     
                    // 获得包名
                    String mapperPackage = child.getStringAttribute("name");
                    // 添加到 configuration 中
                    configuration.addMappers(mapperPackage);
                    // 如果是 mapper 标签,
                } else {
     
                    // 获得 resource、url、class 属性
                    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);
                        // 获得 resource 的 InputStream 对象
                        InputStream inputStream = Resources.getResourceAsStream(resource);
                        // 创建 XMLMapperBuilder 对象
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                        // 执行解析
                        mapperParser.parse();
                        // 使用完全限定资源定位符(URL)
                    } else if (resource == null && url != null && mapperClass == null) {
     
                        ErrorContext.instance().resource(url);
                        // 获得 url 的 InputStream 对象
                        InputStream inputStream = Resources.getUrlAsStream(url);
                        // 创建 XMLMapperBuilder 对象
                        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 中
                        configuration.addMapper(mapperInterface);
                    } else {
     
                        throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                    }
                }
            }
        }
    }

1、解析package标签

	// 如果是 package 标签,则扫描该包
    if ("package".equals(child.getName())) {
     
    	// 获得包名
      	String mapperPackage = child.getStringAttribute("name");
      	// 添加到 configuration 中
       	configuration.addMappers(mapperPackage);
	}

configuration.addMappers(mapperPackage)

	public void addMappers(String packageName) {
     
        // 扫描该包下所有的 Mapper 接口,并添加到 mapperRegistry 中
        mapperRegistry.addMappers(packageName);
    }

mapperRegistry.addMappers(packageName),扫描指定包,并将符合的类,添加到MapperRegistry的属性knownMappers中

	//这个类中维护一个HashMap存放MapperProxyFactory
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    /**
     * @since 3.2.2
     */
    public void addMappers(String packageName) {
     
        addMappers(packageName, Object.class);
    }
    
    /**
     * 扫描指定包,并将符合的类,添加到 {@link #knownMappers} 中
     *
     * @since 3.2.2
     */
    public void addMappers(String packageName, Class<?> superType) {
     
        // 扫描指定包下的指定类
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        // 遍历,添加到 knownMappers 中
        for (Class<?> mapperClass : mapperSet) {
     
            addMapper(mapperClass);
        }
    }
    
    public <T> void addMapper(Class<T> type) {
     
        // 判断,必须是接口。
        if (type.isInterface()) {
     
            // 已经添加过,则抛出 BindingException 异常
            if (hasMapper(type)) {
     
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
     
                // 添加到 knownMappers 中
                knownMappers.put(type, new MapperProxyFactory<>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                // 解析 Mapper 的注解配置
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                // 标记加载完成
                loadCompleted = true;
            } finally {
     
                // 若加载未完成,从 knownMappers 中移除
                if (!loadCompleted) {
     
                    knownMappers.remove(type);
                }
            }
        }
    }    

  上面的knowMappers.put(type, new MapperProxyFactory<>(type)),为该类生成一个动态代理工厂放入到Map集合中,key为该类Class,value为该类动态代理工厂。

2、解析mapper标签(分别有resource、class、url三种属性)

	// 如果是 package 标签,则扫描该包
    if ("package".equals(child.getName())) {
     
        // 获得包名
        String mapperPackage = child.getStringAttribute("name");
        // 添加到 configuration 中
        configuration.addMappers(mapperPackage);
    // 如果是 mapper 标签,
    } else {
     
        // 获得 resource、url、class 属性
        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);
            // 获得 resource 的 InputStream 对象
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 创建 XMLMapperBuilder 对象
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 执行解析
            mapperParser.parse();
            // 使用完全限定资源定位符(URL)
        } else if (resource == null && url != null && mapperClass == null) {
     
            ErrorContext.instance().resource(url);
            // 获得 url 的 InputStream 对象
            InputStream inputStream = Resources.getUrlAsStream(url);
            // 创建 XMLMapperBuilder 对象
            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 中
            configuration.addMapper(mapperInterface);
        } else {
     
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
    }

(1)如果是使用映射器接口实现类的完全限定类名,即使用class的话,和package标签类似,调用configuration.addMapper()接口将该类添加到MapperRegistry的knownMappers中。

(2)如果是/使用相对于类路径的资源引用(resource)或使用完全限定资源定位符(URL),则交由XMLMapperBuilder解析Mapper配置文件,调用的是XMLMapperBuilder的parse()方法。

在 XMLMapperBuilder 中的处理:

	/**
     * 解析***Mapper.xml配置文件
     */
    public void parse() {
     
        // 判断当前 Mapper 是否已经加载过
        if (!configuration.isResourceLoaded(resource)) {
     
            // 解析 `` 节点
            configurationElement(parser.evalNode("/mapper"));
            // 标记该 Mapper 已经加载过
            configuration.addLoadedResource(resource);
            // 绑定 Mapper
            bindMapperForNamespace();
        }

        // 解析待定的  节点
        parsePendingResultMaps();
        // 解析待定的  节点
        parsePendingCacheRefs();
        // 解析待定的 SQL 语句的节点
        parsePendingStatements();
    }

	// 解析 `` 节点
    private void configurationElement(XNode context) {
     
        try {
     
            // 获得 namespace 属性
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
     
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            // 设置 namespace 属性
            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"));
            // 解析     节点们
    private void buildStatementFromContext(List<XNode> list) {
     
        if (configuration.getDatabaseId() != null) {
     
            buildStatementFromContext(list, configuration.getDatabaseId());
        }
        buildStatementFromContext(list, null);
        // 上面两块代码,可以简写成 buildStatementFromContext(list, configuration.getDatabaseId());
    }

    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
     
        //遍历