Mybatis整体架构
执行流程图
主要构件及其相互关系
构件 | 描述 |
---|---|
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代码中的 注解,将主配置⽂件内容解析封装到Configuration,将sql的配置信息加载成为⼀个mappedstatement 对象,存储在内存之中
(2) 接收调⽤请求
触发条件:调⽤Mybatis提供的API
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:将请求传递给下层的请求处理层进⾏处理。
(3) 处理操作请求
触发条件:API接⼝层传递请求过来
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:
(A) 根据SQL的ID查找对应的MappedStatement对象。
(B) 根据传⼊参数对象解析MappedStatement对象,得到最终要执⾏的SQL和执⾏传⼊参数。
© 获取数据库连接,根据得到的最终SQL语句和执⾏传⼊参数到数据库执⾏,并得到执⾏结果。
(D) 根据MappedStatement对象中的结果映射配置对得到的执⾏结果进⾏转换处理,并得到最终的处理 结果。
(E) 释放连接资源。
(4) 返回处理结果
将最终的处理结果返回。
各个文件的作用 和 要存储哪些信息
sqlMapConfig.xml:因为如果使用jdbc,那么数据库配置信息就需要硬编码在代码中,解决这个问题的方案就是,将数据库配置信息 写在配置文件中 。sqlMapConfig.xml中存储的就是数据库配置信息,包括数据驱动类,数据库地址,用户名和密码
userMapper.xml:如果使用jdbc,那么sql语句就需要硬编码在代码中,解决这个问题的方案就是,将sql语句写在配置文件中,userMapper.xml中存储的就是sql语句,包括sql语句的参数、结果集提取出的javaBean类型等
Configuration:包含dataSource和MappedStatement。这个Configuration最终会传递jdbc代码中,在jdbc代码中被用于 得到Connection 和 PreparedStatement。
MappedStatement:对userMapper.xml解析的结果存储在这里,包含id、要设置的参数、返回类型、sql语句
XmlConfigParser用于解析sqlMapConfig.xml,得到一个dataSource(连接池dataSource的实现类)
XmlMapperParser 用于解析userMapper.xml,得到MappedStatement
GenericTokenParser用于将#{}解析成?,且取出#{}中的参数名称
SqlSessionFactoryBuilder:启动类,将xml解析成configuration,然后将configuration传递到SqlSessionFactory,实现类是DefaultSqlSessionFactory
SqlSessionFactory:用于构建SqlSession,将configuration传递到SqlSession中
SqlSession:与数据库交互的会话,写一些select/updte/delete/insert代码,DefaultSqlSession
SqlExecutor:这个是写一些jdbc代码,包含获取连接,预编译,执行sql,返回结果集等。SqlSession中的增删改查,就是调用SqlExecutor中的方法来完成的。实现类是SimpleSqlExecutor
SqlSession中有一个getMapper,这个方法的作用是,给指定接口生成jdk动态代理类
动态sql的解析流程
SqlSessionFactoryBuilder(build) -----> XMLConfigBuilder(parseConfiguration
--->mapperElement------>mapperParser.parse) ---->XMLMapperBuilder(parse--->configurationElement-
---->buildStatementFromContext)----->XMLStatementBuilder(parseStatementNode)----->XMLLanguageDriver(createSqlSource)
----->XMLScriptBuilder(parseScriptNode--->parseDynamicTags)----->IfHandler(handleNode)
sql的执行流程
1.创建sqlSessionFactory的时候,会将sql映射文件读到内存中,解析得到MappedStatement,同时会将mybatis的配置文件解析存储到Configuration中
2.当调用sqlSession.getMapper的时候,会先创建mapperProxyFactory,然后通过mapperProxyFactory.newInstance创建动态代理类。
3.当调用动态代理类的方法时,会先执行MapperProxy的invoke,接着调用DefaultSqlSession的selectList,接着调用SimpleExecutor的doQuery方法,接着调用PreparedStatementHandler的parameterize方法,接着调用
DefaultParameterHandler的setParameters方法,接着调用TypeHandler的setParameter,然后执行数据库查询,得到resultSet,将这个resultSet交给DefaultResultSetHandler的handleResultSets方法,得到对象。
具体调用关系如下:
当调用动态代理类的方法时,会先执行MapperProxy(invoke)--->MapperMethod(execute)--->DefaultSqlSession(selectList)---->BaseExecutor(query---->queryFromDatabase)----->SimpleExecutor(doQuery)---->PreparedStatementHandler(parameterize)---->DefaultParameterHandler(setParameters)---->TypeHandler(setParameter)---->PreparedStatementHandler(query)---->执行PreparedStatement(execute)---->DefaultResultSetHandler(handleResultSets)
插件
⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅ 的,⼀是增加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯ 作。
以MyBatis为例,我们可基于MyBati s插件机制实现分⻚、分表,监控等功能。由于插件和业务 ⽆关,业务也⽆法感知插件的存在。
因此可以⽆感植⼊插件,在⽆形中增强功能
插件原理
Mybati s作为⼀个应⽤⼴泛的优秀的ORM开源框架,这个框架具有强⼤的灵活性,在四⼤组件
(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易⽤的插 件扩展机制。
Mybatis对持久层的操作就是借助于四⼤核⼼对象。
MyBatis⽀持⽤插件对四⼤核⼼对象进 ⾏拦截,对mybatis来说插件就是拦截器,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,MyBatis中的四⼤对象都是代理对象
MyBatis所允许拦截的⽅法如下:
- 执⾏器Executor (update、query、commit、rollback等⽅法);
- SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等⽅ 法);
- 参数处理器ParameterHandler (getParameterObject、setParameters⽅法);
- 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等⽅法);
在四⼤对象创建的时候
- 1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);
- 2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返回 target 包
- 装后的对象
- 3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可 以为四⼤对象
- 创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;
拦截
插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object object, BoundSql sql, InterceptorChain interceptorChain){
ParameterHandler parameterHandler =
mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
parameterHandler = (ParameterHandler)
interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调⽤拦截器链 中的拦截器
依次的对⽬标进⾏拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis 中的四⼤对象。返回
的target是被重重代理后的对象
如果我们想要拦截Executor的query⽅法,那么可以这样定义插件:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
//省略逻辑
}
在mybatis-config.xml中,将MyPlugin配置到plugins标签中
<configuration>
....
<plugins>
<plugin interceptor="com.wp.persist.plugin.ExeunplePlugin">
<property name="address" value="nanjing"/>
</plugin>
</plugins>
</configuration>
⾃定义插件
插件接⼝
Mybatis 插件接⼝-Interceptor
• Intercept⽅法,插件的核⼼⽅法
• plugin⽅法,⽣成target的代理对象
• setProperties⽅法,传递插件所需参数
Intercepts ({//注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤这个拦截器
@Signature (type = StatementHandler .class , //这是指拦截哪个接⼝
method = "prepare",//这个接⼝内的哪个⽅法名,不要拼错了
args = { Connection.class, Integer .class}),//// 这是拦截的⽅法的⼊参,按顺序写到
这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的
})
public class MyPlugin implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// //这⾥是每次执⾏操作的时候,都会进⾏这个拦截器的⽅法内
Override
public Object intercept(Invocation invocation) throws Throwable {
//增强逻辑
System.out.println("对⽅法进⾏了增强....");
return invocation.proceed(); //执⾏原⽅法
}
/**
* //主要是为了把这个拦截器⽣成⼀个代理放到拦截器链中
* ^Description包装⽬标对象 为⽬标对象创建代理对象
* @Param target为要拦截的对象
* @Return代理对象
*/
Override
public Object plugin(Object target) {
System.out.println("将要包装的⽬标对象:"+target);
return Plugin.wrap(target,this);
}
/**获取配置⽂件的属性**/
//插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来
Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:"+properties );
}
}
在mybatis-config.xml中,将MyPlugin配置到plugins标签中
<configuration>
....
<plugins>
<plugin interceptor="com.wp.persist.plugin.MyPlugin">
<property name="address" value="nanjing"/>
</plugin>
</plugins>
</configuration>
源码分析
Plugin实现了 InvocationHandler接⼝,因此它的invoke⽅法会拦截所有的⽅法调⽤。
invoke⽅法会 对所拦截的⽅法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:
// -Plugin
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
/*
*获取被拦截⽅法列表,⽐如:
* signatureMap.get(Executor.class), 可能返回 [query, update, commit]
*/
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//检测⽅法列表是否包含被拦截的⽅法
if (methods != null && methods.contains(method)) {
//执⾏插件逻辑
return interceptor.intercept(new Invocation(target, method, args));
//执⾏被拦截的⽅法
return method.invoke(target, args);
} catch(Exception e){
}
}
invoke⽅法的代码⽐较少,逻辑不难理解。
⾸先,invoke⽅法会检测被拦截⽅法是否配置在插件的 @Signature注解中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。
插件逻辑封装在intercept中,该 ⽅法的参数类型为Invocationo Invocation主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看 ⼀下该类的定义
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object targetf Method method, Object[] args) {
this.target = target;
this.method = method;
//省略部分代码
public Object proceed() throws InvocationTargetException, IllegalAccessException { //调
⽤被拦截的⽅法
>> —
}
PageHelper
参考文档:https://pagehelper.github.io/docs/howtouse/
分页插件—拦截器
分页插件,本质是一个拦截器。
新版拦截器是 com.github.pagehelper.PageInterceptor。 com.github.pagehelper.PageHelper 现在是一个特殊的 dialect 实现类,是分页插件的默认实现类,提供了和以前相同的用法。
分页插件参数介绍
helperDialect:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。
你可以配置helperDialect属性来指定分页插件使用哪种方言。
配置时,可以使用下面的缩写值:oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby
特别注意:使用 SqlServer2012 数据库时,需要手动指定为 sqlserver2012,否则会使用 SqlServer2005 的方式进行分页。
你也可以实现 AbstractHelperDialect,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。
pageSizeZero:默认值为 false,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)。
reasonable:分页合理化参数,默认值为false。当该参数设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。
params:为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。
supportMethodsArguments:支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest。
如何在代码中使用
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);
//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
//其他fields
//下面两个参数名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<Country> list = countryMapper.selectByPageNumSize(user);
PageHelper.startPage 静态方法调用
除了 PageHelper.startPage 方法外,还提供了类似用法的 PageHelper.offsetPage 方法。
在你需要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage 静态方法即可,紧跟在这个方法后的第一个MyBatis 查询方法会被进行分页。
例一:
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page
assertEquals(182, ((Page) list).getTotal());
例二:
//request: url?pageNum=1&pageSize=10
//支持 ServletRequest,Map,POJO 对象,需要配合 params 参数
PageHelper.startPage(request);
//紧跟着的第一个select方法会被分页
List
//后面的不会被分页,除非再次调用PageHelper.startPage
List
//list1
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page
//或者使用PageInfo类(下面的例子有介绍)
assertEquals(182, ((Page) list).getTotal());
//list2
assertEquals(1, list2.get(0).getId());
assertEquals(182, list2.size());
例三,使用PageInfo的用法:
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List
//用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
//测试PageInfo全部属性
//PageInfo包含了非常全面的分页属性
assertEquals(1, page.getPageNum());
assertEquals(10, page.getPageSize());
assertEquals(1, page.getStartRow());
assertEquals(10, page.getEndRow());
assertEquals(183, page.getTotal());
assertEquals(19, page.getPages());
assertEquals(1, page.getFirstPage());
assertEquals(8, page.getLastPage());
assertEquals(true, page.isFirstPage());
assertEquals(false, page.isLastPage());
assertEquals(false, page.isHasPreviousPage());
assertEquals(true, page.isHasNextPage());
3). 使用参数方式
想要使用参数方式,需要配置 supportMethodsArguments 参数为 true,同时要配置 params 参数。 例如下面的配置:
在 MyBatis 方法中:
List
@Param(“user”) User user,
@Param(“pageNumKey”) int pageNum,
@Param(“pageSizeKey”) int pageSize);
当调用这个方法时,由于同时发现了 pageNumKey 和 pageSizeKey 参数,这个方法就会被分页。params 提供的几个参数都可以这样使用。
除了上面这种方式外,如果 User 对象中包含这两个参数值,也可以有下面的方法:
List
当从 User 中同时发现了 pageNumKey 和 pageSizeKey 参数,这个方法就会被分页。
注意:pageNum 和 pageSize 两个属性同时存在才会触发分页操作,在这个前提下,其他的分页参数才会生效。
分页查询中total是怎么处理的?PageInfo和 Page的关系是什么?
查询sql语句执行,获取到totalCount,将这个totalCount放到ThreadLocal中的page的total属性中,
然后将返回的list封装成page对象,这个page 是 extends ArrayList的
PageInfo是一个对象,能获取到的数据比Page多;
即PageInfo对page对象的信息,进行了重新封装。PageInfo中有很多属性,其中protected List
封装后的格式更方便与前端进行交互
DTO BO PO转换,引起分页参数中的total不正确
当我们想要查询用户列表,因为用户过多,往往会有分页操作。当我们使用pagehelper,执行分页查询时,首先执行select sql,得到的一个List,这个List不是一个简单的List,而是一个Page对象。Page extends ArrayList
,page对象除了具有List的功能,还额外持有了分页信息,包括pageSize
,pageNum
,total
等。
数据库返回的是PO对象的List,如果我们想要将PO转换成BO,那么得到的BO对象的List是不含分页参数的,也即不含pageSize
,pageNum
,total
等。因为前端页面,是需要返回分页信息,特别是需要返回total值。
所以,我们需要做额外的分页参数处理,如下:
public static <E, T> List<T> getPageList(List<E> originList, List<T> targetList) {
if (originList instanceof Page) {
Page<T> result = new Page<>();
Page originPage = (Page) originList;
result.setPageSize(originPage.getPageSize());
result.setPageNum(originPage.getPageNum());
result.setTotal(originPage.getTotal());
result.addAll(targetList);
return result;
} else {
return targetList;
}
}
调用代码如下:
List<DashboardComponentConfigPO> dashboardComponentConfigPOs = dashboardComponentConfigMapper.selectComponentConfigs(dashboardComponentConfigPO);
if (CollectionUtils.isNotEmpty(dashboardComponentConfigPOs)) {
List<DashboardComponentConfigBO> configBOList = dashboardComponentConfigPOs.stream().map(DashboardComponentConfigBO::po2Bo).collect(Collectors.toList());
return PageUtils.getPageList(dashboardComponentConfigPOs, configBOList);
} else {
return new ArrayList<>();
}
PageHelper原理
PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。
如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。
但是如果你写出下面这样的代码,就是不安全的用法:
PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子:
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
这种写法就能保证安全。
如果你对此不放心,你可以手动清理 ThreadLocal 存储的分页参数,可以像下面这样使用:
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
try{
list = countryMapper.selectAll();
} finally {
PageHelper.clearPage();
}
} else {
list = new ArrayList<Country>();
}
这么写很不好看,而且没有必要。
源码分析
// com.github.pagehelper.page.PageMethod#startPage(int, int, boolean, java.lang.Boolean, java.lang.Boolean)
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
* @param reasonable 分页合理化,null时用默认配置
* @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page); // 这里是关键代码,将分页参数,保存到ThreadLocal中
return page;
}
我们继续追踪,看下这个setLocalPage方法
public abstract class PageMethod {
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static boolean DEFAULT_COUNT = true;
/**
* 设置 Page 参数
*
* @param page
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page); // 这个地方,就是将page,保存到ThreadLocal中
}
// 省略代码...
}
上面说了,我们将分页参数,保存到ThreadLocal中。下面,我们看下,什么时候使用了这个分页参数了呢?
关键就在于PageInterceptor
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
private static final Log log = LogFactory.getLog(PageInterceptor.class);
// com.github.pagehelper.PageInterceptor#intercept
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if (args.length == 4) {
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
// 这一步,是关键代码,在这一步中,我们调用了拦截器,是获取threadLocal中的分页参数,生成一个带分页功能的boundSql
boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
// 省略代码...
}
上面,我们找到了关键代码,下面,我们看下,具体的调用栈
com.github.pagehelper.PageInterceptor#intercept
先进行 count 查询
---->com.github.pagehelper.PageInterceptor#count
---->com.github.pagehelper.util.ExecutorUtil#executeAutoCount
接下来,再进行分页查询,这里,先获取分页参数
---->com.github.pagehelper.util.ExecutorUtil#pageQuery
----->com.github.pagehelper.PageHelper#processParameterObject
---->com.github.pagehelper.dialect.AbstractHelperDialect#processParameterObject
---->com.github.pagehelper.dialect.helper.MySqlDialect#processPageParameter
接下来,再拼接limit参数
---->com.github.pagehelper.PageHelper#getPageSql
---->com.github.pagehelper.dialect.AbstractHelperDialect#getPageSql
---->com.github.pagehelper.dialect.helper.MySqlDialect#getPageSql
最后,再讲分页参数 和 sql 语句,封装成一个BoundSql对象,去执行sql查询
下面,看下, 获取分页参数的代码:
// com.github.pagehelper.dialect.AbstractHelperDialect#processParameterObject
public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
//处理参数
Page page = getLocalPage();
// 省略代码...
return processPageParameter(ms, paramMap, page, boundSql, pageKey);
}
// com.github.pagehelper.dialect.helper.MySqlDialect#processPageParameter
@Override
public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
//处理pageKey
pageKey.update(page.getStartRow());
pageKey.update(page.getPageSize());
//处理参数配置
if (boundSql.getParameterMappings() != null) {
List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(boundSql.getParameterMappings());
if (page.getStartRow() == 0) {
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
} else {
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, long.class).build());
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
}
MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
metaObject.setValue("parameterMappings", newParameterMappings);
}
return paramMap;
}
最后,当分页查询,执行完成后,会清除,保存在ThreadLocal中的page参数
// com.github.pagehelper.PageHelper#afterAll
public void afterAll() {
//这个方法即使不分页也会被执行,所以要判断 null
AbstractHelperDialect delegate = autoDialect.getDelegate();
if (delegate != null) {
delegate.afterAll();
autoDialect.clearDelegate();
}
clearPage();
}
/**
* 移除本地变量
*/
public static void clearPage() {
LOCAL_PAGE.remove();
}
LocalDateTime日期处理,以及对应的typeHandler
java中,定义一个属性的类型,是LocalDateTime,而在数据库中,定义这个字段是DATETIME。
mybatis,是如何完成,这2个类型的映射的呢?
首先,我们看下,最基本的prepareStatement的设值方法
Connection con;
PreparedStatement pstmt;
try {
con = ds.getConnection(username, password);
con.setAutoCommit(false);
pstmt = con.prepareStatement("UPDATE COFFEES " +
"SET PRICE = ? " +
"WHERE COF_NAME = ?");
pstmt.setFloat(1, price);
pstmt.setString(2, cofName);
pstmt.executeUpdate();
con.commit();
pstmt.close();
} finally {
if (con != null) con.close();
}
上面中设值方法中,有setFloat setString,但是,其实prepareStatement,还定义了一个setObject方法
// java.sql.PreparedStatement#setObject(int, java.lang.Object, java.sql.SQLType, int)
default void setObject(int parameterIndex, Object x, SQLType targetSqlType,
int scaleOrLength) throws SQLException {
throw new SQLFeatureNotSupportedException("setObject not implemented");
}
**这个java.sql.PreparedStatement的setObject方法很重要 **
这里的SQLType,是一个接口,jdk的rt.jar中,提供了一个实现类,就是JDBCType。而mysql-connector-java-8,也提供了一个实现类,就是MysqlType。
我们这里,要使用的就是 MysqlType
public enum JDBCType implements SQLType {
public enum MysqlType implements SQLType {
也就是说,我们在调用jdbc的prepareStatement的setObject方法时,需要指定某个值对应的数据库类型。
那么,mybatis是在哪里调用这个 prepareStatement的setObject方法的呢?另外,mybatis是怎么知道,应该传入哪一种数据库类型的呢?
1. 保存每种java类型,以及对应的类型处理器到TypeHandlerRegistry
首先,mybatis有一个TypeHandlerRegistry,这个类的构造方法中,我们指定了,每一个java类型,以及所使用的的TypeHandler
// org.apache.ibatis.type.TypeHandlerRegistry#TypeHandlerRegistry(org.apache.ibatis.session.Configuration)
public TypeHandlerRegistry(Configuration configuration) {
this.unknownTypeHandler = new UnknownTypeHandler(configuration);
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
register(Byte.class, new ByteTypeHandler());
register(byte.class, new ByteTypeHandler());
register(JdbcType.TINYINT, new ByteTypeHandler());
register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
register(JdbcType.INTEGER, new IntegerTypeHandler());
register(Long.class, new LongTypeHandler());
register(long.class, new LongTypeHandler());
register(Float.class, new FloatTypeHandler());
register(float.class, new FloatTypeHandler());
register(JdbcType.FLOAT, new FloatTypeHandler());
register(Double.class, new DoubleTypeHandler());
register(double.class, new DoubleTypeHandler());
register(JdbcType.DOUBLE, new DoubleTypeHandler());
register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
register(JdbcType.CHAR, new StringTypeHandler());
register(JdbcType.VARCHAR, new StringTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
register(JdbcType.LONGVARCHAR, new StringTypeHandler());
register(Date.class, new DateTypeHandler());
register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
register(JdbcType.TIMESTAMP, new DateTypeHandler());
register(JdbcType.DATE, new DateOnlyTypeHandler());
register(JdbcType.TIME, new TimeOnlyTypeHandler());
register(java.sql.Date.class, new SqlDateTypeHandler());
register(java.sql.Time.class, new SqlTimeTypeHandler());
register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());
register(String.class, JdbcType.SQLXML, new SqlxmlTypeHandler());
// 省略代码
register(Instant.class, new InstantTypeHandler());
// 这个是 关键代码
register(LocalDateTime.class, new LocalDateTimeTypeHandler());
register(LocalDate.class, new LocalDateTypeHandler());
register(LocalTime.class, new LocalTimeTypeHandler());
// 省略代码
}
上面的构造方法,最终,会调用下面这个register方法
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
map.put(jdbcType, handler);
typeHandlerMap.put(javaType, map);
}
allTypeHandlersMap.put(handler.getClass(), handler);
}
也就是说,会将javaType jdbcType 以及对应的typeHandler对象,保存到map和 typeHandlerMap中。
比如,会将javaType为LocalDateTime,以及对应的LocalDateTimeTypeHandler,保存到这个typeHandlerMap中。
2. 解析xml中编写sql语句时,根据java类型,找到对应的typeHandler,保存到parameterMapping中
// org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler#handleToken
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
上面的ParameterMappingTokenHandler,就是解析xml中编写的sql语句,得到真正可用的prepareStatement。调用了buildParameterMapping方法
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
// 这个地方,如果写了jdbcType,那么就根据这个jdbcType,找到对应的typeHandler
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
// 正常情况下,都是在这一步,根据javaType,找到对应的typeHandler
return builder.build();
}
上面的builder.build()
,调用到了下面的方法
// org.apache.ibatis.mapping.ParameterMapping.Builder#build
public ParameterMapping build() {
resolveTypeHandler();
validate();
return parameterMapping;
}
上面的resolveTypeHandler
方法,如下:
// org.apache.ibatis.mapping.ParameterMapping.Builder#resolveTypeHandler
private void resolveTypeHandler() {
if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) {
Configuration configuration = parameterMapping.configuration;
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 这一步是关键,可以看到,就是根据javaType,去typeHandlerRegistry中找到对应的typeHandler,并且将结果,保存到parameterMapping中。
parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
}
}
3.根据parameterMapping,调用jdbc的prepareStatement的setObject方法,进行设值
上面,我们已经找到LocalDateTime对应的typeHandler—LocalDateTimeTypeHandler,并且将结果,保存到了parameterMapping中了。
接下来,我们来看下,是怎么调用jdbc的prepareStatement的setObject方法,进行设值的
我们看下DefaultParameterHandler#setParameters方法:
// org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 这一步是关键,拿到 parameterMapping中的typeHandler,然后调用typeHandler.setParameter
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
这个 typeHandler.setParameter,会调用到LocalDateTimeTypeHandler的setNonNullParameter方法:
// org.apache.ibatis.type.LocalDateTimeTypeHandler#setNonNullParameter
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType)
throws SQLException {
ps.setObject(i, parameter);
}
这个ps.setObject,会调用到ClientPreparedStatement的setObject方法,而这个ClientPreparedStatement其实就是对jdbc的prepareStatement方法的实现。
public class ClientPreparedStatement extends com.mysql.cj.jdbc.StatementImpl implements JdbcPreparedStatement {
@Override
public void setObject(int parameterIndex, Object parameterObj) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
((PreparedQuery) this.query).getQueryBindings().setObject(getCoreParameterIndex(parameterIndex), parameterObj);
}
}
}
看到这里,应该要告一段落了。总结下,就是,最终会使用LocalDateTimeTypeHandler的setNonNullParameter,进而调用jdbc的prepareStatement的setObject方法。
这里,再多说一些题外话。上面,我们知道ClientPreparedStatement是jdbc的prepareStatement的实现类,这里,我们看下,具体是怎么实现的呢?
ClientPreparedStatement的setObject,会调用到NativeQueryBindings的setObject方法:
// com.mysql.cj.NativeQueryBindings#setObject(int, java.lang.Object)
public void setObject(int parameterIndex, Object parameterObj) {
if (parameterObj == null) {
setNull(parameterIndex);
return;
}
// 这一步是关键,这个DEFAULT_MYSQL_TYPES ,保存了,所有javaType,以及对应的jdbcType
MysqlType defaultMysqlType = DEFAULT_MYSQL_TYPES.get(parameterObj.getClass());
if (defaultMysqlType == null) {
Optional<MysqlType> mysqlType = DEFAULT_MYSQL_TYPES.entrySet().stream().filter(m -> m.getKey().isAssignableFrom(parameterObj.getClass()))
.map(m -> m.getValue()).findFirst();
if (mysqlType.isPresent()) {
defaultMysqlType = mysqlType.get();
}
}
// 找到了jdbcType,然后会调用setObject方法,将jdbcType设置进去
setObject(parameterIndex, parameterObj, defaultMysqlType, -1);
}
后面的代码,我们暂时就不看了。我们可以看到的是,com.mysql.cj.NativeQueryBindings#DEFAULT_MYSQL_TYPES,保存了所有javaType,以及对应的jdbcType。 如下:
// com.mysql.cj.NativeQueryBindings#DEFAULT_MYSQL_TYPES
static Map<Class<?>, MysqlType> DEFAULT_MYSQL_TYPES = new HashMap<>();
static {
DEFAULT_MYSQL_TYPES.put(BigDecimal.class, MysqlType.DECIMAL);
DEFAULT_MYSQL_TYPES.put(BigInteger.class, MysqlType.BIGINT);
DEFAULT_MYSQL_TYPES.put(Blob.class, MysqlType.BLOB);
DEFAULT_MYSQL_TYPES.put(Boolean.class, MysqlType.BOOLEAN);
DEFAULT_MYSQL_TYPES.put(Byte.class, MysqlType.TINYINT);
DEFAULT_MYSQL_TYPES.put(byte[].class, MysqlType.BINARY);
DEFAULT_MYSQL_TYPES.put(Calendar.class, MysqlType.TIMESTAMP);
DEFAULT_MYSQL_TYPES.put(Clob.class, MysqlType.TEXT);
DEFAULT_MYSQL_TYPES.put(Date.class, MysqlType.DATE);
DEFAULT_MYSQL_TYPES.put(java.util.Date.class, MysqlType.TIMESTAMP);
DEFAULT_MYSQL_TYPES.put(Double.class, MysqlType.DOUBLE);
DEFAULT_MYSQL_TYPES.put(Duration.class, MysqlType.TIME);
DEFAULT_MYSQL_TYPES.put(Float.class, MysqlType.FLOAT);
DEFAULT_MYSQL_TYPES.put(InputStream.class, MysqlType.BLOB);
DEFAULT_MYSQL_TYPES.put(Instant.class, MysqlType.TIMESTAMP);
DEFAULT_MYSQL_TYPES.put(Integer.class, MysqlType.INT);
DEFAULT_MYSQL_TYPES.put(LocalDate.class, MysqlType.DATE);
// 这个就是,我们切入话题的LocalDateTime
DEFAULT_MYSQL_TYPES.put(LocalDateTime.class, MysqlType.DATETIME); // default JDBC mapping is TIMESTAMP, see B-4
DEFAULT_MYSQL_TYPES.put(LocalTime.class, MysqlType.TIME);
DEFAULT_MYSQL_TYPES.put(Long.class, MysqlType.BIGINT);
DEFAULT_MYSQL_TYPES.put(OffsetDateTime.class, MysqlType.TIMESTAMP); // default JDBC mapping is TIMESTAMP_WITH_TIMEZONE, see B-4
DEFAULT_MYSQL_TYPES.put(OffsetTime.class, MysqlType.TIME); // default JDBC mapping is TIME_WITH_TIMEZONE, see B-4
DEFAULT_MYSQL_TYPES.put(Reader.class, MysqlType.TEXT);
DEFAULT_MYSQL_TYPES.put(Short.class, MysqlType.SMALLINT);
DEFAULT_MYSQL_TYPES.put(String.class, MysqlType.VARCHAR);
DEFAULT_MYSQL_TYPES.put(Time.class, MysqlType.TIME);
DEFAULT_MYSQL_TYPES.put(Timestamp.class, MysqlType.TIMESTAMP);
DEFAULT_MYSQL_TYPES.put(ZonedDateTime.class, MysqlType.TIMESTAMP); // no JDBC mapping is defined
}
java.util.Date日期处理 以及对应的typeHandler
上面,我们已经看了LocalDateTime日期的处理,下面,我们再看看 java.util.Date日期。
我们首先,明确一点: java.util.Date 包含日期和时间 ,而 java.sql.Date只包含日期,不包含time
所以,单纯的从理论上来说, java.util.Date 变成 mysql中的datetime是没有逻辑上的问题,他们都包含日期 和 time
我们,先看下TypeHandlerRegistry的构造方法:
register(Date.class, new DateTypeHandler());
也就是说,由DateTypeHandler来处理Date类型的属性
DateTypeHandler的setNonNullParameter如下:
public class DateTypeHandler extends BaseTypeHandler<Date> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType)
throws SQLException {
ps.setTimestamp(i, new Timestamp(parameter.getTime()));
}
这里,可以看到,是调用了prepareStatement的setTimestamp方法,并且将Date转为了java.sql.Timestamp,最后设值的。
后面的调用代码,暂时就不看了,反正最后,设置的SQLType类型,是 MysqlType.TIMESTAMP