我要手撕mybatis源码

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 该文章深入分析了MyBatis框架的初始化和数据读写阶段的源码,详细阐述了MyBatis如何通过配置文件解析、建立数据库连接、映射接口绑定、动态代理、查询缓存和结果集处理等步骤实现ORM功能,以及与传统JDBC编程相比的优势。

传统的JDBC编程中的一般操作:

  • 1、注册数据库驱动类,指定数据库的URL地址、数据库用户名、密码等连接信息
  • 2、通过DriverManager打开数据库连接
  • 3、通过数据库连接创建Statement对象。
  • 4、通过State对象执行SQL语句,得到ResultSet对象。
  • 5、通过ResultSet读取数据,将数据转换成JavaBean对象
  • 6、关闭连接、释放资源

ORM(Object Relational Mapping) 对象—关系映射。用于实现面向对象编程语言里 不同类型系统的数据 之间的转换。
在这里插入图片描述

在这里插入图片描述

1、源码分析

1、先读取配置文件中的信息

在这里插入图片描述
经过多次追踪、找到这个getResourceAsStream核心方法,以下是该方法的源码
在这里插入图片描述
该方法的参数不仅有传入的文件路径还有类加载器。类加载的主要作用是将名称转化为文件名,读取外部资源。

作为debug的入口
在这里插入图片描述
跟踪到核心源码部分
在这里插入图片描述
这里完成两个操纵:

  • 在这里生成了一个XMLConfigBuilder的对象,并且调用了其parse()方法。返回解析好的configuration对象信息。
  • 调用了SqlSessionFactoryBuilder自身的build()方法,传入的参数为上一步得到的Configuration对象。

先追踪XMLConfigBuilder对象的parse()方法
在这里插入图片描述
这里的parsed保证只加载一次,这里的/configuration是整个配置文件的根节点、是解析整个配置文件的入口。然后调用parseConfiguration方法,继续追踪
在这里插入图片描述
查看root的传入的值,实际上就是configuration下的各个节点

<configuration>
<typeAliases>
<package name="com.github.yeecode.mybatisdemo"/>
</typeAliases>
<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://127.0.0.1:3306/mybatis?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/github/yeecode/mybatisdemo/UserMapper.xml"/>
</mappers>
</configuration>

查看environment下的信息、继续追踪,到environmentsElement方法,
在这里插入图片描述
查看传入的context的值,这里就是解析的信息。也就是url、username、password 、驱动的信息。和上一个的信息是不一样的

<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://127.0.0.1:3306/mybatis?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>

在这里插入图片描述
通过进入每个子方法可以看出来,Configuration类中保存了配置文件的所有配置信息。

通过调用SqlSessionFactoryBuilder自身的build()方法,返回了SqlSessionFactory对象。
在这里插入图片描述
在这里插入图片描述
小结
初始化阶段、MyBatis主要完成以下几项工作

  • 根据配值文件的位置,获取它的输入流InputStream
  • 从配置文件的根节点开始,逐层解析配置问价,也包括相应的映射文件。解析过程不断将解析结果放入Configuration对象
  • 以配置好的Configuration对象作为参数,获取一个SqlSessionFactory对象

2、数据读写阶段追踪

2.1 获得sqlsession

在这里插入图片描述
追踪openSession方法
在这里插入图片描述
进入openSessionFromDataSource方法内部,这里是生成SqlSession的核心源码
在这里插入图片描述
进入DefaultSqlSession类中,类里提供了查询、增加、更新、删除、提交、回滚等大量的方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
执行完new DefaultSqlSession(this.configuration, executor, autoCommit);后就返回了一个SqlSession对象
在这里插入图片描述

2.2 映射接口文件与映射文件的绑定

在这里插入图片描述
在这里插入图片描述
通过操作Configuration的getMapper方法最终进入MapperRegistry类的getMapper方法。
在这里插入图片描述

2.3 映射接口的代理

session.getMapper(UserMapper.class)得到的是mapperProxyFactory.newInstance(sqlSession)返回的对象,追踪此方法得到
在这里插入图片描述
返回的是基于反射的动态代理对象,找到MapperProxy类的invoke方法,被代理对象的方法会被代理对象的invoke方法拦截、直接到 MapperProxy类的invoke方法拦截,
在这里插入图片描述
在invoke打上断点,当执行 userMapper.queryUserBySchoolName(userParam);会自动进入断点invoke内
在这里插入图片描述
然后会触发MapperMethod对象的execute方法
在这里插入图片描述
进入该方法

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        Object param;
        switch(this.command.getType()) {
        case INSERT:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
            break;
        case UPDATE:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
            break;
        case DELETE:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
            break;
        case SELECT:
            if (this.method.returnsVoid() && this.method.hasResultHandler()) {
                this.executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (this.method.returnsMany()) {
                result = this.executeForMany(sqlSession, args);
            } else if (this.method.returnsMap()) {
                result = this.executeForMap(sqlSession, args);
            } else if (this.method.returnsCursor()) {
                result = this.executeForCursor(sqlSession, args);
            } else {
                param = this.method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(this.command.getName(), param);
                if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
                    result = Optional.ofNullable(result);
                }
            }
            break;
        case FLUSH:
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + this.command.getName());
        }

        if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
            throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
        } else {
            return result;
        }
    }

会根据不同的操作类型调用不同的处理方法、这里执行的是查询操作、所以就调用这个方法
在这里插入图片描述

进入executeForMany方法,在这个方法中,MyBatis已经开始通过SqlSession对象的selectList方法开展后续的查询工作。
在这里插入图片描述

2.4 sql语句的查找

继续追踪到DefaultSqlSession中的selectList方法,源码如下
在这里插入图片描述
每个MappedStatement对象对应设置的一个数据库操作节点,主要定义了数据库操作语句、输入/输出参数等信息。this.configuration.getMappedStatement(statement)语句将要执行的MappedStatement对象从Configuration对象存储的映射文件信息中找了出来。

2.5 查询缓存

其中的query方法是Executor中的抽象方法,实现有两个、一个是查询数据库、一个是查询缓存
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
直接在抽象方法上打一个断点、查看执行哪边、这里执行的是CachingExecutor中的方法、源码如下
在这里插入图片描述
其中的BoundSql是经过层层转化后去掉if、where、等标签的sql语句,CacheKey是为该此次查询操作计算出来的缓存建。

继续追踪代码流程到这里
在这里插入图片描述
查看当前的查询操作是否名中缓存,如果是从缓存中获取数据值、如果不是则调用delegate调用query方法。随后将此次查询的结果放入缓存中。

2.6 数据库查询

再次调用了executor接口中的抽象方法
在这里插入图片描述
继续追踪代码流向
在这里插入图片描述
最终进入了BaseExecutor中的query方法上,源码如下

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }

            List list;
            try {
                ++this.queryStack;
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }

            if (this.queryStack == 0) {
                Iterator var8 = this.deferredLoads.iterator();

                while(var8.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                    deferredLoad.load();
                }

                this.deferredLoads.clear();
                if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }

            return list;
        }
    }

其中的关键部分、开始调用数据库展开查询操作
在这里插入图片描述

queryFromDatabase的源代码如下
在这里插入图片描述
首先在缓存中放置一个占位符,然后调用doQuery方法实际执行,最后,将缓存中的占位符替换成真正的查询结果。

doQuery方法是BaseExecutor中的抽象方法,实际运行的最终实现代码
在这里插入图片描述

生成了Statement对象,是java.sql包中的类,Statement类能够执行静态SQL语句并返回结果。
在这里插入图片描述
获得了一个StatementHandler对象handler,然后将查询操作交给StatementHandler执行,StatementHandler是一个语句处理器,其中封装了很多的语句操作方法。,继续追踪代码流向。

handler.query(stmt,resultHandler)调用的是StatementHandler接口中的抽象方法
在这里插入图片描述
接续追踪代码走向、进入PreparedStatementHandler中,调用方法、源码如下
在这里插入图片描述
这里,ps.execute()真正执行了SQL语句,然后把执行结果交给ResulHandler对象处理。

流程梳理

  • 1、先查缓存,如果一定要查数据库、则查询数据库后需要将结果也放入缓存中
  • 2、SQL语句经过多次转化,经过了MappedStatement对象、Statement对象和PreparedStatement对象,最后才执行
  • 最后将查询的结果交给ResultHandler对象处理

2.7 处理结果集

查询到的结果集并没有直接返回,而是交给ResultHandler对象处理,ResultHandler是结果处理器,用来接受查询结果的方法是该接口中的抽象方法handleResultSets

在这里插入图片描述
继续代码追踪,最终的执行方法是DefaultResultSetHandler类中的handleResultSets方法

public List<Object> handleResultSets(Statement stmt) throws SQLException {
        ErrorContext.instance().activity("handling results").object(this.mappedStatement.getId());
        List<Object> multipleResults = new ArrayList();
        int resultSetCount = 0;
        ResultSetWrapper rsw = this.getFirstResultSet(stmt);
        List<ResultMap> resultMaps = this.mappedStatement.getResultMaps();
        int resultMapCount = resultMaps.size();
        this.validateResultMapsCount(rsw, resultMapCount);

        while(rsw != null && resultMapCount > resultSetCount) {
            ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount);
            this.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null);
            rsw = this.getNextResultSet(stmt);
            this.cleanUpAfterHandlingResultSet();
            ++resultSetCount;
        }

        String[] resultSets = this.mappedStatement.getResultSets();
        if (resultSets != null) {
            while(rsw != null && resultSetCount < resultSets.length) {
                ResultMapping parentMapping = (ResultMapping)this.nextResultMaps.get(resultSets[resultSetCount]);
                if (parentMapping != null) {
                    String nestedResultMapId = parentMapping.getNestedResultMapId();
                    ResultMap resultMap = this.configuration.getResultMap(nestedResultMapId);
                    this.handleResultSet(rsw, resultMap, (List)null, parentMapping);
                }

                rsw = this.getNextResultSet(stmt);
                this.cleanUpAfterHandlingResultSet();
                ++resultSetCount;
            }
        }

        return this.collapseSingleResultList(multipleResults);
    }

查询出来的结果被遍历后放入list列表multipleResults中并返回。

继续代码追踪
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)方法:在自动属性映射功能开启的情况下,该方法将数据记录的值赋给输出结果对象。
    在这里插入图片描述
    追踪代码进入applyAutomaticMappings方法
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
        List<DefaultResultSetHandler.UnMappedColumnAutoMapping> autoMapping = this.createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
        boolean foundValues = false;
        if (!autoMapping.isEmpty()) {
            Iterator var7 = autoMapping.iterator();

            while(true) {
                DefaultResultSetHandler.UnMappedColumnAutoMapping mapping;
                Object value;
                do {
                    if (!var7.hasNext()) {
                        return foundValues;
                    }

                    mapping = (DefaultResultSetHandler.UnMappedColumnAutoMapping)var7.next();
                    value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
                    if (value != null) {
                        foundValues = true;
                    }
                } while(value == null && (!this.configuration.isCallSettersOnNulls() || mapping.primitive));

                metaObject.setValue(mapping.property, value);
            }
        } else {
            return foundValues;
        }
    }
  • applyAutomaticMappings方法:该方法将按照用户的映射设置,给输出结果对象的属性赋值

继续追踪代码、进入applyPropertyMappings方法
在这里插入图片描述

private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
        List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
        boolean foundValues = false;
        List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
        Iterator var9 = propertyMappings.iterator();

        while(true) {
            while(true) {
                Object value;
                String property;
                do {
                    ResultMapping propertyMapping;
                    String column;
                    do {
                        if (!var9.hasNext()) {
                            return foundValues;
                        }

                        propertyMapping = (ResultMapping)var9.next();
                        column = this.prependPrefix(propertyMapping.getColumn(), columnPrefix);
                        if (propertyMapping.getNestedResultMapId() != null) {
                            column = null;
                        }
                    } while(!propertyMapping.isCompositeResult() && (column == null || !mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) && propertyMapping.getResultSet() == null);

                    value = this.getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
                    property = propertyMapping.getProperty();
                } while(property == null);

                if (value == DEFERRED) {
                    foundValues = true;
                } else {
                    if (value != null) {
                        foundValues = true;
                    }

                    if (value != null || this.configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()) {
                        metaObject.setValue(property, value);
                    }
                }
            }
        }
    }
  • applyPropertyMappings 方法:该方法按照用户的映射设置,给输出结果对象的属性赋值

执行完之后,装载着multipleResults被返回给List<User> userList 变量

小结

  • 1、建立连接数据库的sqlsession
  • 2、查找当前映射接口中抽象方法对应的数据库操作节点,根据该节点生成接口的实现
  • 3、接口的实现拦截对映射接口中抽象方法的调用,并将其转化为数据查询操作
  • 4、对数据库操作节点中的数据库操作语句进行多次处理、最终得到标准的sql语句。
  • 5、尝试从缓存查询、找到返回;找不到继续找数据库
  • 6、从数据库中查询结果
  • 7、处理结果集(建立输出对象,根据输出结果对输出对象的属性赋值)
  • 在缓存中记录查询结果
  • 返回查询结果
相关文章
|
3月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
2月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
73 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
2月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
162 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
4月前
|
XML Java 数据库连接
mybatis源码研究、搭建mybatis源码运行的环境
这篇文章详细介绍了如何搭建MyBatis源码运行的环境,包括创建Maven项目、导入源码、添加代码、Debug运行研究源码,并提供了解决常见问题的方法和链接到搭建好的环境。
mybatis源码研究、搭建mybatis源码运行的环境
|
4月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
4月前
|
供应链 前端开发 Java
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
该博客文章介绍了一个使用Mybatis、Layui、MVC和JSP技术栈开发的服装库存管理系统,包括注册登录、权限管理、用户和货号管理、库存管理等功能,并提供了源码下载链接。
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
|
7月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——实体层(User.java)
mybatis简单案例源码详细【注释全面】——实体层(User.java)
|
7月前
|
SQL Java 数据库连接
深入源码:解密MyBatis数据源设计的精妙机制
深入源码:解密MyBatis数据源设计的精妙机制
116 1
深入源码:解密MyBatis数据源设计的精妙机制
|
7月前
|
SQL 缓存 Java
|
7月前
|
XML Java 数据库连接
探秘MyBatis:手写Mapper代理的源码解析与实现
探秘MyBatis:手写Mapper代理的源码解析与实现
93 1
下一篇
DataWorks