设计一个分页插件之二【基于mybatis实现分页插件】

在之前的文章中,介绍过一个最简单的分页插件的实现。
设计一个分页插件

本篇,主要介绍如何实现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 - 打包发布

相关推荐
czlczl200209259 小时前
MyBatis-Plus SQL自动填充字段
sql·tomcat·mybatis
独断万古他化9 小时前
【MyBatis-Plus 进阶】注解配置、条件构造器与自定义 SQL的复杂操作详解
sql·mybatis·mybatis-plus·条件构造器
马猴烧酒.1 天前
【JAVA数据传输】Java 数据传输与转换详解笔记
java·数据库·笔记·tomcat·mybatis
962464i1 天前
mybatis-plus生成代码
java·开发语言·mybatis
小信丶1 天前
@MappedJdbcTypes 注解详解:应用场景与实战示例
java·数据库·spring boot·后端·mybatis
palomua1 天前
MyBatis-Plus实体类新增字段导致存量接口报错问题
数据库·mybatis
椎4951 天前
MybatisPlus插件-简化代码开发
mybatis
root666/1 天前
【Java-后端-Mybatis】DISTINCT 作用
数据库·sql·mybatis
墨雨晨曦881 天前
MyBatis框架篇
java·开发语言·mybatis