在之前的文章中,介绍过一个最简单的分页插件的实现。
设计一个分页插件
本篇,主要介绍如何实现mybatis的插件,比如自己实现一个mybatis的分页插件。
1. 分页注解定义
java
/**
* 分页注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PageQuery {
/**
* 页码参数名,默认为"pageNum"
*/
String pageParam() default "pageNum";
/**
* 每页大小参数名,默认为"pageSize"
*/
String sizeParam() default "pageSize";
/**
* 是否需要查询总记录数
*/
boolean needTotal() default true;
/**
* 最大每页记录数
*/
int maxPageSize() default 1000;
}
2. 分页参数类
java
/**
* 分页参数
*/
public class PageParam<T> {
// 页码
private Integer pageNum;
// 每页大小
private Integer pageSize;
// 排序字段
private String orderBy;
// 查询参数
private T params;
// 省略getter/setter
}
/**
* 分页结果
*/
public class PageResult<T> {
// 当前页数据
private List<T> records;
// 总记录数
private Long total;
// 总页数
private Integer pages;
// 当前页
private Integer pageNum;
// 每页大小
private Integer pageSize;
// 省略getter/setter
}
3. MyBatis分页拦截器(核心实现)
java
/**
* MyBatis分页拦截器
*/
@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 ThreadLocal<PageParam<?>> PAGE_PARAM_THREAD_LOCAL = new ThreadLocal<>();
// 数据库方言
private String dialect = "mysql";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 获取分页参数
PageParam<?> pageParam = getPageParam(parameter);
if (pageParam == null || !needPage(ms, parameter)) {
return invocation.proceed();
}
// 设置线程变量
PAGE_PARAM_THREAD_LOCAL.set(pageParam);
try {
// 查询总数
Long total = queryTotal(ms, parameter, invocation);
// 查询分页数据
List<?> records = queryPageData(ms, parameter, invocation, pageParam);
// 构建分页结果
return buildPageResult(records, total, pageParam);
} finally {
PAGE_PARAM_THREAD_LOCAL.remove();
}
}
/**
* 查询总记录数
*/
private Long queryTotal(MappedStatement ms, Object parameter, Invocation invocation) throws SQLException {
// 获取原始SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
// 构建统计SQL
String countSql = "SELECT COUNT(*) FROM (" + sql + ") temp_count";
// 创建新的MappedStatement
MappedStatement countMs = createCountMappedStatement(ms, countSql);
// 执行查询
Executor executor = (Executor) invocation.getTarget();
List<Object> countResult = executor.query(
countMs,
parameter,
RowBounds.DEFAULT,
(ResultHandler) args[3]
);
return ((Number) countResult.get(0)).longValue();
}
/**
* 查询分页数据
*/
private List<?> queryPageData(MappedStatement ms, Object parameter,
Invocation invocation, PageParam<?> pageParam) throws SQLException {
// 获取原始SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
// 构建分页SQL
String pageSql = buildPageSql(sql, pageParam);
// 创建新的MappedStatement
MappedStatement pageMs = createPageMappedStatement(ms, pageSql);
// 执行查询
Executor executor = (Executor) invocation.getTarget();
return executor.query(
pageMs,
parameter,
RowBounds.DEFAULT,
(ResultHandler) args[3]
);
}
/**
* 构建分页SQL
*/
private String buildPageSql(String sql, PageParam<?> pageParam) {
int offset = (pageParam.getPageNum() - 1) * pageParam.getPageSize();
switch (dialect.toLowerCase()) {
case "mysql":
return sql + " LIMIT " + offset + ", " + pageParam.getPageSize();
case "oracle":
return "SELECT * FROM (SELECT ROWNUM RN, T.* FROM (" + sql +
") T WHERE ROWNUM <= " + (offset + pageParam.getPageSize()) +
") WHERE RN > " + offset;
case "postgresql":
return sql + " LIMIT " + pageParam.getPageSize() + " OFFSET " + offset;
default:
return sql + " LIMIT " + offset + ", " + pageParam.getPageSize();
}
}
/**
* 判断是否需要分页
*/
private boolean needPage(MappedStatement ms, Object parameter) {
try {
// 通过方法签名获取注解
String id = ms.getId();
Class<?> mapperClass = Class.forName(id.substring(0, id.lastIndexOf(".")));
String methodName = id.substring(id.lastIndexOf(".") + 1);
Method method = Arrays.stream(mapperClass.getMethods())
.filter(m -> m.getName().equals(methodName))
.findFirst()
.orElse(null);
return method != null && method.isAnnotationPresent(PageQuery.class);
} catch (Exception e) {
return false;
}
}
/**
* 获取分页参数
*/
private PageParam<?> getPageParam(Object parameter) {
if (parameter instanceof PageParam) {
return (PageParam<?>) parameter;
} else if (parameter instanceof Map) {
// 从Map参数中查找PageParam
Map<?, ?> paramMap = (Map<?, ?>) parameter;
for (Object value : paramMap.values()) {
if (value instanceof PageParam) {
return (PageParam<?>) value;
}
}
}
return null;
}
// 创建统计和分页的MappedStatement
private MappedStatement createCountMappedStatement(MappedStatement ms, String countSql) {
return copyMappedStatement(ms, countSql, ms.getId() + "_COUNT");
}
private MappedStatement createPageMappedStatement(MappedStatement ms, String pageSql) {
return copyMappedStatement(ms, pageSql, ms.getId() + "_PAGE");
}
private MappedStatement copyMappedStatement(MappedStatement ms, String sql, String id) {
// 实现MappedStatement的复制逻辑
// 这里简化实现,实际需要完整复制配置
MappedStatement.Builder builder = new MappedStatement.Builder(
ms.getConfiguration(), id, new StaticSqlSource(ms.getConfiguration(), sql),
ms.getSqlCommandType()
);
// 复制其他属性...
return builder.build();
}
private PageResult<Object> buildPageResult(List<?> records, Long total, PageParam<?> pageParam) {
PageResult<Object> result = new PageResult<>();
result.setRecords((List<Object>) records);
result.setTotal(total);
result.setPageNum(pageParam.getPageNum());
result.setPageSize(pageParam.getPageSize());
result.setPages((int) Math.ceil((double) total / pageParam.getPageSize()));
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.dialect = properties.getProperty("dialect", "mysql");
}
}
4. Spring Boot配置
java
@Configuration
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PagePluginConfig {
@Bean
public PageInterceptor pageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("dialect", "mysql");
interceptor.setProperties(properties);
return interceptor;
}
}
5. 使用示例
java
@Mapper
public interface UserMapper {
@PageQuery
PageResult<User> selectByPage(PageParam<UserQuery> pageParam);
// 或者使用注解参数
@PageQuery(pageParam = "current", sizeParam = "size", needTotal = false)
PageResult<User> selectUsers(@Param("params") UserQuery query,
@Param("current") Integer current,
@Param("size") Integer size);
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public PageResult<User> getUsers(Integer pageNum, Integer pageSize) {
PageParam<UserQuery> pageParam = new PageParam<>();
pageParam.setPageNum(pageNum);
pageParam.setPageSize(pageSize);
pageParam.setParams(new UserQuery());
return userMapper.selectByPage(pageParam);
}
}
至此,一个简单的基于mybatis的分页插件也就可以使用了。
还是很简单的吧。
但是有1个问题,以上这个插件只能是这个进程使用,别的进程用不了,这不有点扯嘛,继续。
6. Spring Boot自动配置类
java
package com.tzb.mybatis.page;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* Spring Boot自动配置类
* 这是最关键的一步,让其他Spring Boot项目能自动加载插件
*/
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, Interceptor.class})
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageAutoConfiguration {
@Autowired(required = false)
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addPageInterceptor() {
if (sqlSessionFactoryList == null || sqlSessionFactoryList.isEmpty()) {
return;
}
// 创建分页拦截器
PageInterceptor pageInterceptor = new PageInterceptor();
// 为每个SqlSessionFactory添加拦截器
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration =
sqlSessionFactory.getConfiguration();
// 检查是否已添加相同的拦截器
boolean alreadyAdded = configuration.getInterceptors().stream()
.anyMatch(interceptor -> interceptor instanceof PageInterceptor);
if (!alreadyAdded) {
configuration.addInterceptor(pageInterceptor);
}
}
}
}
7. spring.factories文件
在 src/main/resources/META-INF/ 目录下创建 spring.factories
properties
# Spring Boot自动配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tzb.mybatis.page.PageAutoConfiguration
8. mvn install安装
9. 其他项目引用
xml
<dependency>
<groupId>com.tzb.mybatis</groupId>
<artifactId>mybatis-page-plugin</artifactId>
<version>1.0.0</version>
</dependency>
java
@Mapper
public interface UserMapper {
@PageQuery
PageResult<User> selectUsers(@Param("query") UserQuery query,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public PageResult<User> getUsers(int pageNum, int pageSize) {
UserQuery query = new UserQuery();
// 设置查询条件...
return userMapper.selectUsers(query, pageNum, pageSize);
}
}
直接使用就可以了。
项目结构
mybatis-page-plugin/
├── src/main/java/com/tzb/mybatis/page/
│ ├── config/
│ │ └── PageAutoConfiguration.java
│ ├── annotation/
│ │ └── PageQuery.java
│ ├── PageParam.java
│ ├── PageResult.java
│ ├── PageInterceptor.java
│ └── PageUtil.java
├── src/main/resources/META-INF/
│ └── spring.factories
└── pom.xml
10.总结
以上可以看出,定义一个自己的mybatis插件其实就4步。
4步实现MyBatis插件打包
✅ 实现Interceptor - 核心功能
✅ 自动配置类 - Spring Boot支持
✅ spring.factories - 自动发现
✅ mvn install - 打包发布