Mybatis插件
MyBatis允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
Mybatis的插件都需要实现Interceptor接口,通过对intercept方法的重写,来进行方法增强。接口实现类通过@Intercepts + @Signature注解来对可以增强的方法进行描述,将能进行方法增强的method,放入一个signatureMap,后续符合条件的method,则执行intercept方法。分页插件PageHelper相关的Interceptor接口实现类是PageInterceptor,它的增强点是Executor的4个参数和6个参数的query方法。
分页插件PageHelper的使用
代码准备
创建mybatis-config.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>
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor" />
</plugins>
<environments default="default">
<environment id="default">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/PluginMapper.xml" />
</mappers>
</configuration>
创建PluginMapper.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">
<mapper namespace="com.ys.mybatis.mapper.PluginMapper">
<select id="listAllEmployee" resultType="com.ys.mybatis.DO.EmployeeDO" >
select * from employee
</select>
<select id="listIPageEmployee" resultType="com.ys.mybatis.DO.EmployeeDO" >
select * from employee
</select>
<select id="listArgsEmployee" resultType="com.ys.mybatis.DO.EmployeeDO" >
select * from employee
</select>
</mapper>
创建PluginMapper
public interface PluginMapper {
List<EmployeeDO> listAllEmployee();
List<EmployeeDO> listIPageEmployee(IPage iPage);
List<EmployeeDO> listArgsEmployee(Map<String, Object> map);
}
创建EmployeeDO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDO implements Serializable {
private Integer id;
private String name;
private Integer age;
private String phone;
}
创建PluginTest
@Slf4j
public class PluginTest {
private SqlSessionFactory sqlSessionFactory;
@BeforeEach
public void before() {
InputStream inputStream = ConfigurationTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
}
1.使用PageHelper.startPage
@Test
public void testStartPage() {
// 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取PluginMapper对象
PluginMapper pluginMapper = sqlSession.getMapper(PluginMapper.class);
// 设置分页
PageHelper.startPage(1, 2);
// 分页查询
List<EmployeeDO> list = pluginMapper.listAllEmployee();
// 封装查询结果
PageInfo<EmployeeDO> pageInfo = new PageInfo<>(list);
System.out.println(pageInfo.getList());
System.out.println(pageInfo.getTotal());
}
2.参数为IPage
创建IPageRequest
@Data
@AllArgsConstructor
@NoArgsConstructor
public class IPageRequest implements IPage {
private Integer pageNum;
private Integer pageSize;
private String orderBy;
}
@Test
public void testIPage() {
// 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取PluginMapper对象
PluginMapper pluginMapper = sqlSession.getMapper(PluginMapper.class);
// 分页查询
List<EmployeeDO> list = pluginMapper.listIPageEmployee(new IPageRequest(1, 2, null));
// 封装查询结果
PageInfo<EmployeeDO> pageInfo = new PageInfo<>(list);
System.out.println(pageInfo.getList());
System.out.println(pageInfo.getTotal());
}
3.supportMethodsArguments属性为true
将属性SupportMethodsArguments设置为true
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor" >
<property name="supportMethodsArguments" value="true"/>
</plugin>
</plugins>
@Test
public void testSupportMethodsArguments() {
// 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取PluginMapper对象
PluginMapper pluginMapper = sqlSession.getMapper(PluginMapper.class);
// 封装查询参数
Map<String, Object> map = new HashMap<>();
map.put("pageNum", 1);
map.put("pageSize", 2);
// 分页查询
List<EmployeeDO> list = pluginMapper.listArgsEmployee(map);
// 封装查询结果
PageInfo<EmployeeDO> pageInfo = new PageInfo<>(list);
System.out.println(pageInfo.getList());
System.out.println(pageInfo.getTotal());
}
源码解析
解析配置文件
XMLConfigBuilder#pluginElement
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 获取interceptor全限定名
String interceptor = child.getStringAttribute("interceptor");
// 获取自定义的properties
Properties properties = child.getChildrenAsProperties();
// 实例化Interceptor对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 将自定义的properties设置到相关属性
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
PageInterceptor#setProperties
@Override
public void setProperties(Properties properties) {
// 缓存一些count查询
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
// 获取分页插件的实现类,默认是想是PageHelper
// 自定义分页插件,需要手动设置dialect
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = default_dialect_class;
}
try {
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
// 通过手动设置的参数,覆盖一些属性的默认值
dialect.setProperties(properties);
String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
}
PageHelper#setProperties
@Override
public void setProperties(Properties properties) {
setStaticProperties(properties);
pageParams = new PageParams();
autoDialect = new PageAutoDialect();
pageParams.setProperties(properties);
autoDialect.setProperties(properties);
CountSqlParser.addAggregateFunctions(properties.getProperty("aggregateFunctions"));
}
PageHelper存在两个属性PageParams,PageAutoDialect
- PageParams :分页相关设置
- PageAutoDialect : 方言自动推断
PageParams相关属性含义
- offsetAsPageNum : RowBounds参数offset作为PageNum使用
- rowBoundsWithCount:RowBounds是否进行count查询
- pageSizeZero:当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
- reasonable :分页合理化
- supportMethodsArguments:是否支持接口参数来传递分页参数,默认false
- countColumn :默认count(0)
动态代理
当我们新建一个SqlSession的时候,如果配置了相关插件,则对其绑定的Executor进行动态代理
执行PageInterceptor的intercept方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// 省略部分代码
List resultList;
// 重点关注这个skip方法,如果skip返回true,则不执行分页插件逻辑
if (!dialect.skip(ms, parameter, rowBounds)) {
// 是否执行count查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
// 查询总数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
// 处理查询总数,返回 true 时继续分页查询,false时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
// 当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
// 执行分页插件逻辑
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
dialect.afterAll();
}
}
Dialect#skip方法
@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
if (ms.getId().endsWith(MSUtils.COUNT)) {
throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
}
// 返回已经存在或者新构建的page对象
Page page = pageParams.getPage(parameterObject, rowBounds);
if (page == null) {
return true;
} else {
//设置默认的 count 列
if (StringUtil.isEmpty(page.getCountColumn())) {
page.setCountColumn(pageParams.getCountColumn());
}
// 根据url动态获取断言类型或者通过helperDialect属性,指定Dialect类型
autoDialect.initDelegateDialect(ms);
return false;
}
}
PageParam的getPage()方法很重要,它详细说明了如何定义参数,才能返回一个Page对象。如果不能成功返回一个Page对象,分页插件的功能就会失效。可以返回page对象的方式,分成了以下几种情况 :
- 1.Pape已经存在 (PageHelper.startPage)
- 2.Pape不存在
- 2.1 RowBounds不等于默认值
- 2.1.1 offsetAsPageNum属性为 true
- 2.1.2 offsetAsPageNum属性为 fasle
- 2.2 参数类型是IPage 或者 supportMethodsArguments 属性为 true
- 2.2.1 通过IPage构建page
- 2.2.2 通过javax.servlet.ServletRequest.getParameterMap方法构建
- 2.2.3 通过参数进行构建
- 2.3 返回null
- 2.1 RowBounds不等于默认值
上文的三个案例,分别对应1、2.2.1、2.2.3这三种情况。更多运用,可以查看getPage()方法明细。如果需要修改默认值,则需要手动配置,示例如下:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor" >
<property name="supportMethodsArguments" value="true"/>
<property name="offsetAsPageNum" value="true"/>
<property name="reasonable" value="true"/>
</plugin>
</plugins>
几种方式比较
第一优先级:1;PageHelper的startPage()方法会返回一个page对象,我们可以按需设置属性
第二优先级:2.2.2 、2.2.3 ; 可以手动设置pageNum、pageSize、orderBy、count、reasonable、pageSizeZero等属性
第三优先级:2.1.1 、2.1.2、2.2.1; 只能设置pageNum、pageSize、orderBy等属性。count、reasonable、pageSizeZero等属性,只能为全局配置的值或默认值,不用单独设置,灵活性较差
Dialect#beforeCount方法
通过skip方法,会构建出一个Page对象。如果其 orderByOnly 属性为 false ,count 属性为 true,则执行count方法
Dialect#count方法
根据Page对象countColumn属性,构建出类似于 select count(0) from ( 原始sql ) 这样的新Sql。我们也可以通过设置,将countColumn属性改成 id、*等。我们还可以自定义count查询,比如上文中的使用案例1(listAllEmployee),如果我们自定义了一个id为listAllEmployee_COUNT的查询,则最终将使用我们自定义的查询。演示如下:
<select id="listAllEmployee_COUNT" resultType="int" >
select count(*) from employee where 1 = 1
</select>
ExecutorUtil#pageQuery方法
主要有三个关注点,我们以MySqlDialect为例
- 关注点1:进入if条件 (Page对象的 orderByOnly 属性为 true 或 pageSize 大于0)
- 关注点2:参数处理
- 2.1 会将参数统一构建成Map对象
- 2.2 给Map对象put两个key(First_PageHelper、Second_PageHelper), 其值分别为Page对象的startRow和pageSize。
- 关注点3:拼接sql。如果Page对象的startRow为0,则拼接" LIMIT ? ",否则拼接" LIMIT ?, ? "。就是Map对象First_PageHelper、Second_PageHelper这两个key对应的值
MySqlDialect#getPageSql
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ? ");
} else {
sqlBuilder.append(" LIMIT ?, ? ");
}
return sqlBuilder.toString();
}
一些细节问题
1.如果reasonable的默认值被设置为true,且构建出的page的reasonable属性为null,page对象有一次优化分页的机会。但是上文中2.1.1这种情况不可以优化分页,因为构建出的page对象的reasonable属性被设置成了false
2.如果是单数据源,或者多数据源都是同类型的数据库产品,可以自定义helperDialect属性,避免默认情况下,每次分页查询都需要根据url来推断Dialect类型。以MySqlDialect为例,设置如下:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor" >
<property name="helperDialect" value="com.github.pagehelper.dialect.helper.MySqlDialect"/>
</plugin>
</plugins>
3.默认情况下,2.1.1 、2.1.2这两种构建page的方式,不会执行count查询