MyBatis架构原理
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
一、架构设计
可以将MyBatis的功能架构分为三层
- API接口层:提供给外部使用的接口API,开发人员可以通过这些本地API来操纵数据库。接口层一旦接收到这些调用请求,那么就会调用数据处理层来完成具体的数据处理。
- MyBatis和数据库的交互有两种方式:
- 使用传统的MyBatis提供的API;例如sqlSession提供的selectLIst、selectOne等。
- 使用Mapper代理的方式(常用)
- MyBatis和数据库的交互有两种方式:
- 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。主要目的是根据调用的请求完成一次数据库操作。
- 基础支撑层:包括连接管理、事务管理、配置加载、缓存处理,都是作为公用的功能抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
二、主要构件及其相互关系
构件 | 描述 |
---|---|
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 update delete insert>节点的封装 |
SqlSource | 负责根据⽤户传递的parameterObject,动态地⽣成SQL语句,将信息封装到BoundSql对象中,并返回 |
BoundSql | 表示动态⽣成的SQL语句以及相应的参数信息 |
三、总体流程
1. 加载配置文件并初始化
触发条件:加载配置⽂件
配置来源于两个地⽅,⼀个是配置⽂件(主配置⽂件conf.xml,*mapper.xml),—个是java代码中的注解(例如使用注解开发的SQL信息@select),将主配置⽂件内容解析封装到Configuration,将sql的配置信息加载成为⼀个mappedstatement对象,存储在内存之中。
2. 接收调用请求
触发条件:调⽤Mybatis提供的API
传⼊参数:为SQL的StatementId和传⼊参数对象
处理过程:将请求传递给下层的请求处理层进⾏处理。
3. 处理操作请求
触发条件:API接⼝层传递请求过来
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:
(A) 根据SQL的StatementId查找对应的MappedStatement对象。
(B) 根据传⼊参数对象解析MappedStatement对象,得到最终要执⾏的SQL和执⾏传⼊参数。
© 获取数据库连接,根据得到的最终SQL语句和执⾏传⼊参数到数据库执⾏,并得到执⾏结果。
(D) 根据MappedStatement对象中的结果映射配置对得到的执⾏结果进⾏转换处理,并得到最终的处理结果。
(E) 释放连接资源。
4. 返回处理结果
将最终的处理结果返回
四、源码分析
使用传统模式(SqlSession)
1. 加载配置文件并初始化
① 加载配置文件
获取到对应的MyBatis核心配置文件地址,加载为字节输入流并返回
② 解析、初始化配置文件
解析配置文件
使用XPathParser解析核心配置文件的字节输入流
解析配置文件后封装
这里主要是看parser.parse()
方法,这里将解析出来的核心配置封装成configuration
对象
先判断该配置文件是否解析过,如果解析过那么抛出异常;没有解析那么先设置为true(意为已解析)后再解析XML配置文件中的configuration
节点并返回对应的configuration。
将configuration
标签中的各类子标签节点进行解析,然后封装到Configuration对象
这里以properties
标签为例:
获取其子标签节点,先判断子标签中的resource
和url
两属性不能共存,共存则抛出异常;解析完成后将对应的数据存放再configuration对象和XPathParser对象(parser)中。
再看看mappers
中的封装:
可以点进Configuration
对象中查看,很明显可以得知mappedStatements的底层就是map,其中key是${namespace}.${id}
组合而成,并找到对应的MappedStatement对象
每条语句只对应一个MappedStatement对象
最后,解析之后的Configuration对象会传入SqlSessionFactory.build()
方法中并获取对应的SqlSessionFactory对象,由源码可得默认的SqlSessionFactory对象为DefaultSqlSessionFactory。
注:这里使用了建造者模式
2. SQL执行流程
① 创建SqlSession
首先SqlSession中最重要的两个属性,一个是Configuration,一个是Executor。
点击sqlSessionFactory.openSession()
方法进入到创建SqlSession中,这里通过DefaultSqlSessionFactory创建的SqlSession,所以进入到DefaultSqlSession类中。发现创建SqlSession是由三个参数进行创建,首先第一个参数表明的是创建Executor的类型;第二个参数为事务的级别;第三个参数为事务是否自动开启,这里默认为false不开启。
创建的SqlSession中的Executor类型为SIMPLE。
执行器Executor共有三个类型:SIMPLE,REUSE,BATCH
使用传入的三个参数创建SqlSession,需要创建了事务,且为不自动提交;然后完成了Executor对象的创建。
② Executor源码
创建了Executor封装到对应的SqlSession对象中后,这时候可以使用sqlSession.selectOne
进行调用。
selectOne底层实际调用的是selectList方法。
进入SqlSession.selectList()
中重载了由三个参数的方法,其中第一个是statementId参数,第二个查询需要传递的参数对象,第三个参数和分页有关,这里不传参的时候为默认值。
进入到重载的selectList方法中,先根据statementId在Configuration对象中找到对应的MappedStatement对象,然后将对应传给Executor对象执行查询query操作。executor.query()
方法中四个参数,第一个为传入的MappedStatement对象,第二个为传入的参数对象(如果是collection集合需要转换为对应的map),第三个参数为默认的分页参数,第四个为ResultSetHandler(默认为null)
这里传入的ResultSetHandler对象默认为null
进入SimpleExecutor中的query方法,线程通过MappedStatement对象获取到对应的绑定SQL对象(BoundSql),并且创建一级缓存中的key。
绑定SQL对象(BoundSql)中包含了已经将占位符处理为?的SQL语句,参数映射数组,参数对象等信息。
将获取到的参数传入给重载方法query中。
- 先判断Executor对象是否关闭,如果关闭则抛出异常。
- 如果配置了isFlushCacheRequired参数且查询栈queryStack为0,那么清除本地的一级缓存。
- 根据缓存key从本地缓存中查询对应的数据,如果没有,那么从数据库查询获取对应的返回数据(此时的ResultSetHandler为null)
- 后续和延迟加载队列有关。
- 返回查询的结果。
从数据库中查询获取数据
- 设置本地缓存,使用占位符占位。
- 执行查询的操作
- 删除占位符,将查询结果put到本地缓存中。
- 返回查询结果
主要查询在doQuery方法中。获取到对应的configuration对象,初始化创建一个statementHandler对象,使用该对象初始化jdbc中的prepareStatement对象并进行读操作。
初始化jdbc中的prepareStatement中,先获取连接对象connection后,使用statementHandler创建 Statement 或 PrepareStatement 对象
获取连接对象connection会从连接池中获取。
最后statementHandler执行查询后返回。
上述的Executor.query()⽅法⼏经转折,最后会创建⼀个StatementHandler对象,然后将必要的参数传递给StatementHandler,使⽤StatementHandler来完成对数据库的查询,最终返回List结果集。
从上⾯的代码中我们可以看出,Executor的功能和作⽤是
- 根据传递的参数,完成SQL语句的动态解析,⽣成BoundSql对象,供StatementHandler使⽤;
- 为查询创建缓存,以提⾼性能
- 创建JDBC的Statement连接对象,传递给StatementHandler对象,返回List查询结果。
③ StatementHandler源码
StatementHandler主要完成两个工作:
- 对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使⽤的是SQL语句字符串会包含若⼲个?占位符,我们其后再对占位符进⾏设值。StatementHandler通过parameterize(statement)⽅法对 Statement 进⾏设值;
- StatementHandler 通过 List query(Statement statement, ResultHandler resultHandler)⽅法来完成执⾏Statement,和将Statement对象返回的resultSet封装成List;
进⼊到 StatementHandler 的 parameterize(statement)⽅法的实现:
进入parameterHandler.setParameters()
,设置statement中的参数。
对某一个statement进行设置参数。根据我们输⼊的参数,对statement对象的?占位符处进⾏赋值
进⼊到StatementHandler 的 query(Statement statement, ResultHandler resultHandler)⽅法的实现
ResultSetHandler 的 handleResultSets(Statement)⽅法会将 Statement 语句执⾏后⽣成的 resultSet结果集转换成List结果集
Mapper代理方式
1. 扫描mapper
要使用Mapper代理方式的方法调用。首先需要使用sqlSession.getMapper()
方法将需要代理类传入,并返回生成的代理对象。
MyBatis是如何扫描mapper的,通过我们核心配置文件xml中的mappers标签进行读取、扫描,mappers标签中提供了两种方式,一种是mapper标签(注入单个mapper),一种是package标签(将包名下的所有mapper进行注入)
在源码中org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
解析方法中可以找到mappers
标签的解析
首先会判断标签是package
还是mapper
,在根据标签获取mapper。我们以package
标签为例,Configuration类中有个添加mapper映射的方法,通过对应的包名获取。
这里将需要扫描包中所有mapper接口添加到mapperRegistry注册器中。整个mapper扫描注册过程到这里就结束。
2. sqlSession.getMapper()解析
由上面的mapper扫描过程中,可以发现多了一个MapperRegistry类,主要包含了两个属性,一个是Configuration,一个是Map类,Map类中的key是我们传入的需要代理的mapper接口,value值根据词义来看是mapper代理工厂。
从sqlSession.getMapper()
方法出发,底层是有Configuration类提供的getMapper方法。除了传入需要代理类外,还传入了调用getMapper方法的sqlSession。
Configuration类中的getMapper方法实际上调用的是MapperRegistry类中的getMapper方法。
根据上文描述可以这里使用了mapper接口的字节码对象作为key获取对应的MapperProxyFactory对象,然后通过动态代理工厂生成实例。
动态代理工厂生成实例最终调用的是JDK中的Proxy生成的代理对象,所以可以表明MyBatis中是使用JDK动态代理对mapper接口产生代理对象。
代理对象是调用第三个参数InvocationHandler中的invoke方法。
进入该方法确实发现有个invoke方法,这里就是对需要代理的对象方法执行。
执行方法中除了select方法外,其他例如insert、update、delete方法都会发现执行了sqlSession提供的API。
其实select中执行的也是sqlSession提供的api,只是由于返回的结果不同,需要进行不同的操作。
总结:
MyBatis初始化时对接⼝的处理:MapperRegistry是Configuration中的⼀个属性,它内部维护⼀个HashMap⽤于存放mapper接⼝的⼯⼚类,每个接⼝对应⼀个⼯⼚类。
mappers中可以配置接⼝的包路径,或者某个具体的接⼝类。当解析mappers标签时,它会判断解析到的是mapper配置⽂件时,会再将对应配置⽂件中的增删改查标签封装成MappedStatement对象,存⼊mappedStatements中。
当判断解析到接⼝时,会建此接⼝对应的MapperProxyFactory对象,存⼊HashMap中,key =接⼝的字节码对象,value =此接⼝对应的MapperProxyFactory对象
在动态代理返回了实例后,我们就可以直接调⽤mapper类中的⽅法了,但代理对象调⽤⽅法,执⾏是在MapperProxy中的invoke⽅法中