Mybatis启动流程和执行流程源码解读

您所在的位置:网站首页 druid源码解读 Mybatis启动流程和执行流程源码解读

Mybatis启动流程和执行流程源码解读

2024-04-05 11:06| 来源: 网络整理| 查看: 265

目录

前言

mybatis是什么

Mybatis的架构

源码解读前的准备

正文

准备hello world代码

解读源码

启动源码解读

执行流程源码解读

Mybatis的执行流程图 

总结

个人推荐

前言 mybatis是什么

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

这段话来自于Mybatis中文网

我们知道在Javase阶段中,Java如果要将数据持久化的储存下来,要不就是io写到本地磁盘中,要不就是连接上mysql,将数据通过mysql存储到磁盘中。而JDBC的代码是非常的繁琐,哪里使用都需要写上一大段的代码,什么反射获取连接对象,statement传入参数然后执行sql,然后返回结果集进行遍历。

而Mybatis将繁琐的JDBC给进行封装,开发者只需要给一个mapper接口,如果使用注解编写sql语句xml都不需要,但是复杂的sql还是需要使用到xml然后映射到mapper接口,Mybatis帮你解析通过Java的动态代理生成接口的代理类底层帮你于mysql交互。对于返回值,Mybatis只需要你给定一个实体类于mysql表中的映射,Mybatis底层也帮你处理好返回值。

这种映射关系就叫做orm(Object Relational Mapping)框架。

Mybatis的架构

此帖子并不会直接拿源码一行一行的解读,更多的是一个思想的授予,一步一步的推导。从前面我们知道Mybatis是一个orm框架,对于orm框架我们只需要编写他的接口和xml的映射,返回值和实体类的映射。那么了解到这里,假如叫你来设计一个orm框架你会怎么来设计呢?

首先对于Java万物皆对象的语言来说肯定是要来解析Mybatis的配置文件,此时我们可以提供xml写配置文件和直接Java的new对象来写,因为xml其实最后也是解析生成Java对象。

解析完xml后,我们应该干什么呢?

在我们dao层都是接口,看到接口就能明白肯定是要生成代理类。最终干活的都是代理类。

与mysql交互后是不是要解析数据呢?

从整体来说其实就是解析xml,然后动态代理与mysql交互,然后解析数据。

 下面的源码环节会来证实这种猜想!

源码解读前的准备

那么我们追源码前需要准备一些什么呢?

有看过博主其他帖子的小伙伴可能知道,追源码共用的几个小步骤:

1. 对框架有一定了解(能推理出上面的架构图,因为这是一个推理过程,追源码一定要带着自己的想法来追,因为框架万变不离其宗,改变的只是代码,思想是改变不了的),对其他的api也有一定的使用了解。

2. 准备一段hello world代码(建议追任何框架源码都从一段hello world开始!)

3. 很关键的一个点就是熟悉使用idea的debug工具。

4. 通过idea生成的类关系图弄清楚类之间的关系。

5. 使用到截图工具将自己的接口和xml代码和测试类main方法代码截图钉在桌面,电脑比较大的可以使用到分屏。这样效率的确高很多。

正文 准备hello world代码

使用maven构建项目,pom文件如下

org.mybatis mybatis 3.4.6 mysql mysql-connector-java 8.0.23 com.alibaba druid 1.1.10 org.projectlombok lombok 1.18.20 src/main/java **/*.xml false

测试类的准备

public static void main(String[] args) { DataSource dataSource = getDataSource(); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(UserMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); System.out.println(mapper.selectAllUser()); // sqlSession.close(); } private static DataSource getDataSource() { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setUrl("jdbc:mysql://localhost:3306/dingcan?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8"); druidDataSource.setUsername("root"); druidDataSource.setPassword("123456"); return druidDataSource; }

dao层接口准备

public interface UserMapper { List selectAllUser(); }

对应的接口映射的xml文件准备(这里注意xml需要跟接口一个路径包下,因为博主使用的Java代码来代替xml配置文件的,xml配置文件可以配置扫描接口xml映射,但是Java代码我没找到对应的方法.... 不过不影响,因为mybatis就是默认扫描接口路径,后面可以证实)

select * from user

实体类的准备(你们可以根据我的main方法中数据源来建表,或者自定义,自定义的记得改变实体类的字段,不要犯低级错误来影响时间)

/** * @author liha * @version 1.0 * @date 2022/1/29 13:23 */ @Data @ToString public class User { private String id; private String userNmae; private String nickname; private String password; }

解读源码 启动源码解读

hello world的源码准备好了后,我们是不是应该找到追寻源码的入口呢?

入口?那不就是第一行开始?

并不是,我们来分析一下

DataSource dataSource = getDataSource(); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(UserMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); System.out.println(mapper.selectAllUser());

数据源的配置,事务工厂,然后环境的准备。

再看到Configuration,他的构造方法是不是把环境传进去,那么我们看看Configuration的构造方法

 一些初始化的注册操作,并不是我们的重点。

我们的架构图第一件事不应该是解析xml吗?而xml是跟接口做一个映射关系,所以往下走的addMapper(UserMapper.class)好像跟我们的想法是沾边了。

那么奖励这行一个断点,我们追进去

 我们看到Configuration类中维护了很多类,一眼就能明白它肯定是最核心的类。继续往下走

public void addMapper(Class type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { 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. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }

判断是否是接口,是否已经加载过不是我们的核心,我们的核心是什么?解析对不对!

在进入parse()方法之前,我们看一下try和finally代码块,可以学习到这种写法,先put到缓存,如果过程中出现问题,再通过finally代码块remove。

给parse()方法哪行一个断点,不过追进去之前,先看到MapperAnnotationBuilder的构造方法追进去。

 内部维护了一个MapperBuilderAssistant对象,翻译过来就是助手。其他就是类信息和共用一个Configuration对象。我们进入到parse()方法中。

public void parse() { // type是构造方法赋值的类对象。 String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { // 是否解析过 // 解析加载xml loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }

type是构造方法传进来的类对象,然后这里的configuration也是共用的。

public boolean isResourceLoaded(String resource) { return loadedResources.contains(resource); }

先是对判断是否加载过,使用的也是contains检查是否存在于集合中。继续往下走,给loadXmlResource来上一个断点,继续追进去。

private void loadXmlResource() { // Spring may not know the real resource name so we check a flag // to prevent loading again a resource twice // this flag is set at XMLMapperBuilder#bindMapperForNamespace if (!configuration.isResourceLoaded("namespace:" + type.getName())) { // 字符串的操作,其实从这里我们得出,Mybatis默认找的是接口的同级目录 String xmlResource = type.getName().replace('.', '/') + ".xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource); } catch (IOException e) { // ignore, resource is not required } if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); xmlParser.parse(); } } }

判断肯定是能过的,因为我们并没有解析过,解析过会存入set集合中。跟前面的那个判断共用一个方法。

其实从这里我们得出,Mybatis默认找的接口映射的xml文件是接口的同级目录,后面也是查找这个目录下是否存在这个文件,并且注意到catch代码块中的注释,其实说明了没有这个xml文件也不影响,因为可能使用的是注解写的xml,并且后面会有对这些判空操作,所以这里肯定是不能抛出异常的。

而我们的xml映射文件是跟接口一个目录所以能直接获取到io输出流来给下面的操作解析xml文件。

然后就是new了一个XMLMapperBuilder对象,我们来看看他的构造方法。

在XMLMapperBuilder类中维护了一个XPathParser对象,在构造方法中对XPathParser初始化,而XPathParser内部维护了一个Document,在XpathParser初始化的时候初始化了内部的document,根据io流解析XML(这里使用的是apache公司的xml解析技术,想要看具体的解析可以追进去)生成document。方便后面从document取出标签。

我们继续往后面的解析走。

public void parse() { if (!configuration.isResourceLoaded(resource)) { // 解析方法,parser是当前类维护的XPathParser类,XPathParser内部维护了一个document,根据io流解析XML(这里使用的是apache公司的xml解析技术,想要看具体的解析可以追进去)生成document。方便后面从document取出标签。 configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }

因为上面说过XMLMapperBuilder的构造方法,所以当前的parser对象是在XMLMapperBuilder的构造方法中创建的XPathParser,所以我们看到configurationElement(parser.evalNode("/mapper")); ,之前也说过在XPathParser类中维护了一个document,所以我们推测出parser.evalNode()方法就是从document中根据参数“/mapper”取出mapper标签的所有内容。我们给这行来上一个断点追进去

 然后生成XNode对象返回,我们再看看Xnode类中维护了一些什么对象

 可以看到我们从Document中取出的数据Node,给了类中的Properties对象attributes,那么我们后面是不是从attributes取值呢?

我们继续往configurationElement()方法里面走

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. The XML location is '" + resource + "'. Cause: " + e, e); } }

前面看的evalNode()方法的返回值就是XNode,也就是configurationElement()方法的参数,我们看到context.getStringAttribute("namespace")方法中。

这里的确就是从Properties对象中取值。然后进行判断您是否有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"));

这些方法跟之前也是一样,也就是通过document获取到参数标签的值,然后存到XNode中,然后再从XNode维护的Properties中获取到值,再把值存到builderAssistant助手中。这里我不细说,大家想追的可以自己细追一下,大致的操作都一样!

并且这里与缓存有关的我会另外写一篇帖子细讲Mybatis的缓存机制。

cache-ref:引用其它命名空间的缓存配置。

cache:该命名空间的缓存配置。

parameterMap:已经废弃,未来可能移除

resultMap:描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。

sql:抽取出公共的sql语句

                        Mybatis文档有中文翻译,建议大家养成一个看官方文档的习惯

来到buildStatementFromContext(context.evalNodes("select|insert|update|delete"))方法,给上一个断点,我们追进去。

private void buildStatementFromContext(List list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); }

DatabaseId:简单来说就是mybatis支持多种DB厂商的,通过databaseid来识别DB厂商。

Mybatis文档有中文翻译,建议大家养成一个看官方文档的习惯

我们这里没有设置,所以直接跳出if,给buildStatementFromContext()方法来上一个断点。追进去

private void buildStatementFromContext(List 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); } } }

因为一个XML文件可能存在多个增删改查语句,所以for循环遍历这没啥好说的,并且在mybatis中一个增删改查标签对应一个MappedStatement。给statementParser.parseStatementNode();来个断点我们继续往里面走。

public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String parameterType = context.getStringAttribute("parameterType"); Class parameterTypeClass = resolveClass(parameterType); String resultMap = context.getStringAttribute("resultMap"); String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); Class resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); String nodeName = context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // Include Fragments before parsing XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them. processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: and were parsed and removed) SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); String resultSets = context.getStringAttribute("resultSets"); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }

这里基本还是跟之前操作一样,从XNode的Properties中取值。

resultType、parameterType就生成他们的实体类。

并且看到那几个boolean值的操作,其实哪里就是缓存的一些操作,如果是查询就不flushCache,如果非查询就flushCache,并且看你是否能使用到缓存。对于缓存会另外写篇详细的帖子!

继续往下走

resultSets:在mybatis中执行存储过程,它会执行两个查询并返回多个结果集,所以需要规定多个返回结果集。 keyProperty:自增字段,后面可以通过自增字段获取到自增的值 keyColumn:自增字段数据库的字段

 Mybatis文档有中文翻译,建议大家养成一个看官方文档的习惯

解析完,就是添加到集合中的操作,进入到addMappedStatement()方法中,也就是生成MappedStatement然后添加到configuration对象中。

这里其实我觉得可以不用解释一句话,因为很明显的东西,小伙伴们可以打开Mybatis官方文档自行查看。

继续走,然后就回到了foreach循环的位置,因为我们就是一条语句所以继续返回。

回到了XMLMapperBuilder类中的parse()方法中

继续往后走, 添加到集合中,证明解析过了,再完后就是bingMapperForNamespace()方法。追进去看看吧其实也就是绑定一下xml中namespace和接口,但是我们已经绑定过了。

继续往下面三个方法看去

parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements();

parsePendingStatements();在之前buildStatementFromContext()方法中已经解析完毕并且添加到configuration中了。

大概就是前面解析的时候如果有ResultMap和cacheref就会对他进行一个处理。在前面解析的时候是把大致的信息添加到configuration中,这里就是把configuration大致的信息获取出来再对它进行一个处理并且添加configuration中,为后面的动态代理处理sql做铺垫。并且做了处理之后会把之前的大概信息给remove。

我们继续走,就回到了MapperAnnotationBuilder类中的parse()方法了

public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }

看到parseCache()和parseCacheRef()两个方法。

private void parseCache() { CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class); if (cacheDomain != null) { Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size(); Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval(); Properties props = convertToProperties(cacheDomain.properties()); assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props); } } private void parseCacheRef() { CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class); if (cacheDomainRef != null) { Class refType = cacheDomainRef.value(); String refName = cacheDomainRef.name(); if (refType == void.class && refName.isEmpty()) { throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef"); } if (refType != void.class && !refName.isEmpty()) { throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef"); } String namespace = (refType != void.class) ? refType.getName() : refName; assistant.useCacheRef(namespace); } }

这里是看是否存在@CacheNamespace注解和@CacheNamespaceRef注解,我们之前XML解析过cache所以可以得出基于注解的cache优先级要大于xml配置。

对于缓存还是强调一遍,后面会有帖子从源码来讲解缓存。所以这里不多说继续往下走。

Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } }

获取到接口中的方法进行遍历,然后给parseStatement()方法给上一个断点我们追进去。

方法内的内容挺长的,我就不放出来了,但是是不是觉得特别的眼熟呢?

没错他跟前面解析xml标签的方法极其相似,并且我们知道我们的sql语句可以使用注解的形式,也可以使用到xml的形式。所以这里面是对注解的一个解析,并且最后也是通过助手的addMappedStatement()方法将生成MappedStatement对象添加到configuration对象中。

到这里大家伙有没有思考一个问题呢?要是我同时使用注解的形式和XML形式来写sql语句,那么它会是一个优先级的选择还是报错呢?

这里我又给大家做了一个实验,我同时使用到注解和xml,最后接口是抛出异常了。

从异常报告中我们可以看出他是使用到contains判断是否存在了,存在就抛出异常了,并把我们的方法告诉我们。而且从前面解析可以得出,先是解析XML再是解析注解,所以肯定是在解析注解的层面判断的。为了证实我的猜想。

所以我追了一遍,确实是在解析注解的时候进行了一个判断,是解析注解中对ResultMap注解解析中,因为基于注解形式我们一般不适用ResultMap注解,直接通过方法的返回值。所以这里对这个做了判断。在parseStatement()方法中的348行的parseResultMap()方法。这里想看详细的步骤的同学可以自己追一下。

因为这些比较成熟的源码套娃肯定少不了,所以很容易追的头疼,追到迷茫。所以在追源码很迷茫的时候,建议通过日志,一行一行的执行,直到日志出来。然后再debug运行一下,再慢慢思考!

至此启动的源码就已经全部追完,其实启动的源码也就是解析。后面就是mybatis如何动态代理生成代理类并且如何封装jdbc代码执行sql语句,并且如何解析mysql那边返回的数据。

执行流程源码解读

很多小伙伴才开始接触mybatis,就是SqlSessionFactory、SqlSession啥的。使用Sqlsession获取到接口的代理对象,然后执行被代理的方法。所以再追执行流程源码之前先讲解一下Sqlseesion。

使用 MyBatis 的主要 Java 接口就是 SqlSession。你可以通过这个接口来执行命令,获取映射器实例和管理事务。在介绍 SqlSession 接口之前,我们先来了解如何获取一个 SqlSession 实例。SqlSessions 是由 SqlSessionFactory 实例创建的。SqlSessionFactory 对象包含创建 SqlSession 实例的各种方法。而 SqlSessionFactory 本身是由 SqlSessionFactoryBuilder 创建的,它可以从 XML、注解或 Java 配置代码来创建 SqlSessionFactory。

简单来说就是SqlSession就是一次会话,也就是一次java与mysql的交互。而SqlSessionFactory就是生产SqlSession的。

Mybatis文档有中文翻译,建议大家养成一个看官方文档的习惯

这里使用了很明显的建造者模式来创建SqlSessionFactory对象,并且参数是Configuration,在前面的解析过程中,结果集都是存入到Configuration中的。

再来看看SqlSessionFactory如何创建的SqlSession对象

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }

Environment 、TransactionFactory 这里就不介绍了,我们来介绍一下Executor,在openSessionFromDataSource()方法的参数中ExecutorType,这里是一个枚举类,默认使用的是普通执行器,还有两种分别是重用和批量执行器。

然后就是创建DefaultSqlSession,我们继续往下走。

UserMapper mapper = sqlSession.getMapper(UserMapper.class);

核心来了,追进去。

public T getMapper(Class type, SqlSession sqlSession) { final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }

 knownMappers.get(type);在我们解析的时候有添加到这个map集合中。

 所以不为空,所以给mapperProxyFactory.newInstance(sqlSession);来上一个断点,并且追进去

public T newInstance(SqlSession sqlSession) { final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }

 MapperProxyFactory工厂创造出MapperProxy,然后继续往newInstance()方法追进去。

protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }

到这里就是使用JDK自带的动态代理来生成代理类并返回,并且不懂动态代理的同学建议先去通过博客或者课程去了解,我这里不过多讲。

生成了代理类后,看看Mybatis如何执行的,我们执行接口的方法。

 追进去。

@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }

if判断代理对象是否是原对象显然不是,再判断是否是default修饰的方法显然不是。所以接着往下走。

cachedMapperMethod()方法也就是从map集合缓存中去取,没有就创建,经典缓存思想。

看一下MapperMethod的构造方法把。MapperMethod内部维护了一个SqlCommand,和MethodSignature,也就是sql语句是增删改查和返回值的的类型的值。 

然后给execute()给上一个断点追进去。

public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }

也就是判断是增删改查,内部维护的command在构造方法对其初始化了。

看到switch代码块中的SELECT代码块。

this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();

这是内部的维护的MethodSignature构造方法的代码,也就是判断返回值是否是一个集合或者数组,显然我们的是一个集合,所以为true。所以来到executeForMany()方法给个断点追进去。 

private Object executeForMany(SqlSession sqlSession, Object[] args) { List result; Object param = method.convertArgsToSqlCommandParam(args); if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.selectList(command.getName(), param, rowBounds); } else { result = sqlSession.selectList(command.getName(), param); } // issue #510 Collections & arrays support if (!method.getReturnType().isAssignableFrom(result.getClass())) { if (method.getReturnType().isArray()) { return convertToArray(result); } else { return convertToDeclaredCollection(sqlSession.getConfiguration(), result); } } return result; }

判断是否实现了mybatis内部的分页,当然我们这里是没有实现的。mybatis实现的分页,我的猜想是获取到结果后对集合的处理达到分页效果。

所以我们进入到else代码块中给上一个断点继续追进去。

@Override public List selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }

 MappedStatement再熟悉不过了吧,从configuration中获取到MapperStatement对象。

继续往下走,追进query()方法中。

把MappedStatement的数据抽取出来生成其他对象给下面的操作做铺垫,继续往里面的query()走

@Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List list = (List) tcm.getObject(cache, key); if (list == null) { list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }

取缓存,有缓存就直接返回,没有缓存就继续创建。而且换成小伙伴们来设计查询的缓存,肯定是按照sql语句来缓存。这里不过多提。

继续往query()方法走。

打上断点继续走。

private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }

看到doQuery就知道要执行了(手动滑稽),所以打个断点,继续追进去。

生成StatementHandler来干活, 通过数据源来建立与mysql的连接,connection对象然后生成Statement对象,这些对于懂JDBC的小伙伴来说非常的熟悉不过了。

继续往下走。进入到query()方法中。

@Override public List query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler. handleResultSets(ps); }

 Statement和PreparedStatement的区别大致就是PreparedStatement会先与数据库进行预处理,再解析好参数再进行处理,所以比较耗性能,但是他会被缓存下来,所以有重复的操作就可以使用到,而Statement是每次从0到1的创建处理。

然后就是数据源执行,然后就是解析返回的数据。然后就是解析数据并且返回。解析的具体步骤感兴趣的小伙伴可以进给return哪行打上断点追进去。其实无非就是resultType实体类和返回结果中数据库字段的一个映射,如果有resultMap就按照resultMap的来。

后面就一路的返回并且添加到缓存中,就结束了整个的执行流程。

Mybatis的执行流程图 

总结

把一个复杂的东西具体化的拆分,然后再自我猜想,再抱着猜想开始追源码。

并且后面会写一篇关于Mybatis缓存的帖子和Spring整合Mybaits的帖子。

个人推荐

暂时没有非常好的课程推荐,但是如果时间比较多,可以去听这位老师的课,是真的学思想,但是得花时间因为课比较长(要有一定的底子才听得懂,要不然听天书)

非常仔细的Mybatis源码解读https://www.bilibili.com/video/BV1kT4y137hk?spm_id_from=333.999.0.0



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3