mybatis源码分析部分

1.mybatis原理及启动过程

这部分可以参考

​​​​​​mybatis的实现及mybatis在spring和springboot中的实现_流光亦流连的博客-CSDN博客_springboot中mybatis如何实现数据注入schema

为了方便后续的讲解,把mybatis启动过程的代码再拿出来写一遍:

public class testStart {
    @Test
    public  void testStart() throws IOException {
//1.mybatis 主配置文件,文件写在根目录下
        String config = "mybatis.xml";
//2. 读取配置文件,读取里面的数据库配置信息,及mapper接口中sql语句的所在的类
        InputStream in = Resources.getResourceAsStream(config);
//3. 创建 SqlSessionFactory 对象, , 目的是获取 Sql Session
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//4. 获取 SqlSession,SqlSession 能执行 sql 语句
        SqlSession session = factory.openSession();
//5. 执行 SqlSession 的 selectList() 参数中name space +id 匹配相应sql语句
        List<Student> studentList =session.selectList("com.cry.dao.StudentDao.selectStudents");
//6. 循环输出查询结果
        studentList.forEach( student -> System.out.println(student.toString()));
//7. 关闭 SqlSession ,释放资源
        session.close();
    }

2.源码解析

2.1 sqlSessionFactory部分

根据上面的7步,首先来看1-3步,从代码本身来看,大意是根据你写的mybatis.xml文件,将文件转换为输入流,再根据这个输入流构建sqlSessionFactory

那么首先来分析这3步的源码

找到SqlsessionFactory.build(Reader reader)方法,并找到其重载方法,下面对其重载方法进行分析。

public SqlSessionFactory build(Reader reader) {
        return this.build((Reader)reader, (String)null, (Properties)null);
    }

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    SqlSessionFactory var5;
    try {
        //根据mybatis.xml文件生成XMLConfiguration
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        //生成了一个SqlSessionFactory对象,注意参数中的parse
        var5 = this.build(parser.parse());
    } catch (Exception var14) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
    } finally {
        ErrorContext.instance().reset();

        try {
            inputStream.close();
        } catch (IOException var13) {
        }

    }

    return var5;
}

可以看到try块中,XMLConfigBuilder是一个用于对我们的XML配置文件进行解析的对象,其parse方法用于解析xml文件的属性。

在解析处xml文件中相关的属性后,将解析后的属性封装为Configuration对象用于构建sqlSessionFactory

那么我们来看看XMLConfigBuilder是怎么解析XML文件的

public Configuration parse() {
    //parsed的初始值为false,第二次调用此方法,进入if
    if (this.parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    } else {
        this.parsed = true;
        //核心方法
        this.parseConfiguration(this.parser.evalNode("/configuration"));
        return this.configuration;
    }
}

重点关注parseConfiguration方法,方法具体的代码如下:

private void parseConfiguration(XNode root) {
    //在root中已经包含了xml文件中配置的所有的属性值
    try {
        //以下内容用于解析configuration标签中配置的各项属性
        this.propertiesElement(root.evalNode("properties"));
        Properties settings = this.settingsAsProperties(root.evalNode("settings"));
        this.loadCustomVfs(settings);
        //别名
        this.typeAliasesElement(root.evalNode("typeAliases"));
        this.pluginElement(root.evalNode("plugins"));
        this.objectFactoryElement(root.evalNode("objectFactory"));
        this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
        this.settingsElement(settings);
        //数据库
        this.environmentsElement(root.evalNode("environments"));
        this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        this.typeHandlerElement(root.evalNode("typeHandlers"));
        //mapper接口映射文件
        this.mapperElement(root.evalNode("mappers"));
    } catch (Exception var3) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
    }
}

看到里面的字段是不是有些眼熟?再让我们来看看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="db.properties"/>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    <typeAliases>
        <typeAlias type="com.cry.domain.Student" alias="student"/>
    </typeAliases>
    <!-- 配置 mybatis 环境 -->
    <environments default="mysql">
        <!--id: 数据源的名称 -->
        <environment id="mysql">
            <!-- 配置事务类型:使用 JDBC 事务(使用 Connection 的提交和回滚) -->
            <transactionManager type="JDBC"/>
            <!-- 数据源 dataSource :创建数据库 Connection 对象
            type: POOLED 使用数据库的连接池-->
            <dataSource type="POOLED">
                <!-- 连接数据库的四个要素 -->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!-- 告诉 mybatis 要执行的 sql 语句的位置 -->
        <mapper resource="com/cry/dao/StudentDao.xml"/>
    </mappers>
</configuration>

是不是感觉出奇的一致?

其实parse方法的核心思想就是将configuration标签作为一个对象,configuration标签中子标签的内容作为对象的属性进行封装,也就是说将xml文件中的内容解析成一个configuration对象进行返回。有了这个概念,再读这部分代码就很轻松了。

这里面比较重要的两个属性是environments(对应数据库配置)和mapper(对应mapper接口的路径),因此只对这两部分进行源码分析。

environments部分:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (this.environment == null) {
            this.environment = context.getStringAttribute("default");
        }

        Iterator var2 = context.getChildren().iterator();

        while(var2.hasNext()) {
            XNode child = (XNode)var2.next();
            //取出id对应的数据
            String id = child.getStringAttribute("id");
            //判断id对应的数据库是否为特定的数据库
            if (this.isSpecifiedEnvironment(id)) {
                //获取事务类型
                TransactionFactory txFactory = this.transactionManagerElement(child.evalNode("transactionManager"));
                //获取数据库的相关信息,详见dataSourceElement源码
                DataSourceFactory dsFactory = this.dataSourceElement(child.evalNode("dataSource"));
                //获取数据源
                 DataSource dataSource = dsFactory.getDataSource();
                 //根据数据库类型,事务类型,数据源生成builder
                Builder environmentBuilder = (new Builder(id)).transactionFactory(txFactory).dataSource(dataSource);
                //生成的builder注入到configuration的environment属性中去(对应父子结点)
                this.configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }

}

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
        //获取连接池的类型
        String type = context.getStringAttribute("type");
        //驱动类,url,username,password
        Properties props = context.getChildrenAsProperties();
        //通过连接池类型,用反射造出工厂类
        DataSourceFactory factory = (DataSourceFactory)this.resolveClass(type).newInstance();
        //将四要素注入到生成的工厂类中
        factory.setProperties(props);
        return factory;
    } else {
        throw new BuilderException("Environment declaration requires a DataSourceFactory.");
    }
}

指的注意的是,数据库连接的四要素,我写在了db.properties文件中,该文件在xml文件中的<properties>标签中进行引入,已经被解析为properties对象,因此这里可以直接注入。

可以看到在解析enviroments配置时,可以根据你的数据连接池,数据库类型,四要素,生成数据库的工厂类,依据此工厂类,生成数据源,并将其作为属性注入到configuration对象中

mapper部分:

在分析mapper部分的解析过程之前,我们首先要知道mapper接口在xml文件中有4种引入方式,分别是

 这里我是通过第一种方式引入的,就按第一种方式对应的代码来进行分析

if (resource != null && url == null && mapperClass == null) {
    ErrorContext.instance().resource(resource);
    inputStream = Resources.getResourceAsStream(resource);
    //根据resource路径生成对应的XMLMapperBuilder
    mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
    mapperParser.parse();
} 

 重点关注parse()方法

public void parse() {
    if (!this.configuration.isResourceLoaded(this.resource)) {
        //对应mapper标签中url路径对应的文件内容,此时已经可以拿到里面全部的内容了
        this.configurationElement(this.parser.evalNode("/mapper"));
        this.configuration.addLoadedResource(this.resource);
        this.bindMapperForNamespace();
    }

    this.parsePendingResultMaps();
    this.parsePendingCacheRefs();
    this.parsePendingStatements();
}

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace != null && !namespace.equals("")) {
            this.builderAssistant.setCurrentNamespace(namespace);
            this.cacheRefElement(context.evalNode("cache-ref"));
            this.cacheElement(context.evalNode("cache"));
            this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            this.resultMapElements(context.evalNodes("/mapper/resultMap"));
            this.sqlElement(context.evalNodes("/mapper/sql"));
            //重要:这里就是构建sql语句的地方,将带有s/i/u/d标签的标签中的内用封装为集合
            this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } else {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
    } catch (Exception var3) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);
    }
}

其实通过configurationElement已经可以看到,对mapper.xml解析的思路是跟configuration是一样的,即将mapper.xml中<mapper>标签作为一个对象,将其中所有的子标签作为mapper对象的属性进行填充。

至此,mapper对象中封装了我们写的mapper.xml文件中的全部内容,而mapper又将作为configuration中的一个依赖进行注入,因此configuration对象中也同样会存储着解析mapper.xml文件后得到的mapper对象。

而生成的sqlSessionFactory的方法中注入了configuration对象,因此,整个mybatis.xml文件中解析的配置将全部被sqlSessionFactory获取。

2.2 dao层与数据库的交互

在刚才解析mapper.xml文件的方法中有一个重要的方法,buildStatementFromContext

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    Iterator var3 = list.iterator();

    while(var3.hasNext()) {
        XNode context = (XNode)var3.next();
        //遍历每条sql标签生成对应的sql语句转换器对象
        XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);

        try {
            //解析标签中的相关内容,并将namespace+方法id生成key取出对应的sql语句
            statementParser.parseStatementNode();
        } catch (IncompleteElementException var7) {
            this.configuration.addIncompleteStatement(statementParser);
        }
    }

}



//applyCurrentNamespace()将当前的namespace+.+id拼成key值
keyStatementId = this.builderAssistant.applyCurrentNamespace(keyStatementId, true);
Object keyGenerator;
if (this.configuration.hasKeyGenerator(keyStatementId)) {
    //取出对应的sql语句
    keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
} else {
    keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//根据xml文件中各部分解析的值,以及对应的sql语句,调用addMappedStatement进行statement的创建
this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

以下面这个mapper.xml文件为例

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
namespace :必须有值,自定义的唯一字符串
推荐使用: dao 接口的全限定名称
namespace 与 id组合成唯一的key作为 Map <String, MapperStatement> 的 key使用的
-->
<mapper namespace="com.cry.dao.StudentDao">
    <!--
<select>: 查询数据, 标签中必须是 select 语句
id: sql 语句的自定义名称,推荐使用 dao 接口中方法名称,
使用名称表示要执行的 sql 语句
resultType: 查询语句的返回结果数据类型,使用全限定类名
-->
    <select id="selectStudents" resultType="com.cry.domain.Student">
<!-- 要执行的 sql 语句 -->
        select id,name,email,age from student
    </select>
    <insert id="insert" >
        insert into student(id,name,email,age) values(#{id},#{name},#{email},#{age})
    </insert>
</mapper>

它的原理是将select/insert/update/delete标签中配置的全部内容解析为一个mapperStatement对象

将mapper中的id与namespace一起拼成一个字符串,与解析出来的mapperStatement对象进行一一映射,存储在mybatis中的一个map对象中。

这也就是为什么mapper。xml文件中的id和namespace组成的值必须唯一的原因,因为key重复时,无法完成dao层接口的方法与mapperStatement的一一映射。

也就是说有了这个一一映射的关系,我们在执行dao层相应的方法时,代理类拦截此方法后,就可以找到这个方法对应的mapperStatement对象来决定执行的sql语句,输入的参数值,返回值类型等。

由于这个mapperStatement对象最终将存储在configuration中,所以sqlSession是可以获取mapperStament的,而通过openSession()方法生成的sqlSession对象同样也可以拿到这个mapperStatement配置。

至此,第4步生成的sqlSession已经获取你在mybatis.xml和mapper.xml中的所有配置,这也就是后面dao层方法执行以及数据库与java直接交互的基础。

3.sql语句的执行与方法的返回值

这部分重点对应第5步,首先来看看selectList的代码

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    List var5;
    try {
        //对应前面的addMapperStatement,在这里拿到对应的statement对象
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        //调用query方法拿到对应的sql语句
        var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception var9) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
    } finally {
        ErrorContext.instance().reset();
    }

    return var5;
}

可以看到首先sqlSession获取了刚才解析的mapperStatement,并调用executor的query方法完成了查询功能。

那么再来看query方法

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //ms中包含着之前解析select/insert/delete/update标签生成的所有内容,此处取出其中的sql语句,同时注入参数
    BoundSql boundSql = ms.getBoundSql(parameter);
    //生成缓存
    CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
    return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;

    List var9;
    try {
        //获取confinguration中解析的所有配置
        Configuration configuration = ms.getConfiguration();
        //根据ms中的内容生成statementHandler
        StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        //这里生成了preparedStatement
        stmt = this.prepareStatement(handler, ms.getStatementLog());
        var9 = handler.query(stmt, resultHandler);
    } finally {
        this.closeStatement(stmt);
    }

    return var9;
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    //connection连接数据库
    Connection connection = this.getConnection(statementLog);
    //调用prepare()方法拿到statement对象
    Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}

可以看到,在查询过程中,首先将参数注入到sql语句中生成了boundSql对象,这个对象对应着我们查询时输入的sql语句。

在查询时首先检查缓存中是否存在之前的查询结果,如果存在,直接返回结果,如果不存在,则继续调用query方法进行查询,query方法中核心方法为doQuery

可以看到在doQuery方法中,sqlsession获取了mybatis.xml中配置的configuration,然后根据configuration生成了StatementHandler,进而生成了prepareStatement,在生成prepareStatement的是否已经建立了与数据库的连接,由于之前数据的配置已经解析为enviroments对象注入到configuration中,因此是可以与本地配置的数据库建立连接的。

最后只需要调用handler对象的query方法,将生成的prepareStatement对象和处理结果映射的resultHandler对象一起传入,就可以完成对数据库的查询和结果的返回了。

var9 = handler.query(stmt, resultHandler);

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement)statement;
    //执行对应的sql语句
    ps.execute();
    //封装返回的结果
    return this.resultSetHandler.handleResultSets(ps);
}

至此查询过程完毕,且结果映射为java对象。

第6步为对结果的输出,用于查看返回的结果。

第7步为关闭连接,节省资源。

至此mybatis源码部分的分析完毕。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值