首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

MyBatis 分頁攔截器自研實戰:物理分頁、多方言支援與 Spring 整合

     編輯:Java 相關知識

設計目標

  • 零侵入:Mapper XML 無需修改
  • 多資料庫支援:MySQL/Oracle/PostgreSQL/SQL Server
  • 高效能:物理分頁 (Limit/Offset/Rownum) 避免全表掃描
  • 易擴充:方言策略模式

核心攔截器實作

@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class PaginationInterceptor implements Interceptor {
    
    private static final String PAGE_PARAM = "page";  // 分頁參數鍵名
    private Dialect dialect;  // 方言策略
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof StatementHandler) {
            StatementHandler stmtHandler = (StatementHandler) invocation.getTarget();
            MetaObject meta = SystemMetaObject.forObject(stmtHandler);
            MappedStatement ms = (MappedStatement) meta.getValue("delegate.mappedStatement");
            
            // 1. 判斷是否需要分頁 (Mapper 方法名包含 page)
            String msId = ms.getId();
            String methodName = msId.substring(msId.lastIndexOf('.') + 1);
            if (!methodName.toLowerCase().contains(PAGE_PARAM)) {
                return invocation.proceed();  // 不攔截,直接執行
            }
            
            // 2. 取得原始 SQL 與參數
            BoundSql boundSql = (BoundSql) meta.getValue("delegate.boundSql");
            String originalSql = boundSql.getSql();
            Object paramObj = boundSql.getParameterObject();
            
            // 3. 解析 Page 物件 (支援 Map/POJO/單一參數)
            Page page = extractPage(paramObj);
            if (page == null) return invocation.proceed();
            
            // 4. 查詢總筆數 (必要時)
            if (page.isCountTotal()) {
                long total = queryTotalCount(originalSql, boundSql, invocation);
                page.setTotal(total);
            }
            
            // 5. 改寫分頁 SQL
            String pageSql = dialect.buildPageSql(originalSql, page.getOffset(), page.getLimit());
            meta.setValue("delegate.boundSql.sql", pageSql);
        }
        return invocation.proceed();
    }
    
    private Page extractPage(Object paramObj) {
        if (paramObj instanceof Page) return (Page) paramObj;
        if (paramObj instanceof Map) {
            Map map = (Map) paramObj;
            return (Page) map.get(PAGE_PARAM);
        }
        return null;
    }
    
    private long queryTotalCount(String sql, BoundSql boundSql, Invocation invocation) {
        String countSql = dialect.buildCountSql(sql);
        Connection conn = (Connection) invocation.getArgs()[0];
        try (PreparedStatement ps = conn.prepareStatement(countSql)) {
            // 參數綁定 (複製原 BoundSql 參數)
            ParameterHandler handler = (ParameterHandler) 
                SystemMetaObject.forObject(boundSql).getValue("parameterHandler");
            handler.setParameters(ps);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? rs.getLong(1) : 0;
            }
        }
    }
    
    @Override public Object plugin(Object target) { return Plugin.wrap(target, this); }
    @Override public void setProperties(Properties properties) { /* 方言配置解析 */ }
}

方言策略介面與實作

public interface Dialect {
    String buildPageSql(String originalSql, int offset, int limit);
    String buildCountSql(String originalSql);
    boolean supportsOffsetFetch();  // SQL:2008 標準
}

// MySQL / MariaDB / PostgreSQL / H2 / SQLite
public class LimitOffsetDialect implements Dialect {
    @Override
    public String buildPageSql(String sql, int offset, int limit) {
        return sql + " LIMIT " + limit + " OFFSET " + offset;
    }
    @Override public String buildCountSql(String sql) {
        return "SELECT COUNT(*) FROM (" + sql + ") AS tmp_count";
    }
    @Override public boolean supportsOffsetFetch() { return false; }
}

// Oracle 12c+ (OFFSET FETCH)
public class Oracle12cDialect implements Dialect {
    @Override
    public String buildPageSql(String sql, int offset, int limit) {
        return sql + " OFFSET " + offset + " ROWS FETCH NEXT " + limit + " ROWS ONLY";
    }
    @Override public String buildCountSql(String sql) {
        return "SELECT COUNT(*) FROM (" + sql + ")";
    }
    @Override public boolean supportsOffsetFetch() { return true; }
}

// Oracle 11g 及以下 (ROWNUM 子查詢)
public class Oracle11gDialect implements Dialect {
    @Override
    public String buildPageSql(String sql, int offset, int limit) {
        int end = offset + limit;
        return "SELECT * FROM (SELECT tmp.*, ROWNUM rn FROM (" + sql + ") tmp WHERE ROWNUM <= " + end + ") WHERE rn > " + offset;
    }
    @Override public String buildCountSql(String sql) {
        return "SELECT COUNT(*) FROM (" + sql + ")";
    }
    @Override public boolean supportsOffsetFetch() { return false; }
}

// SQL Server 2012+ (OFFSET FETCH)
public class SqlServer2012Dialect implements Dialect {
    @Override
    public String buildPageSql(String sql, int offset, int limit) {
        return sql + " ORDER BY (SELECT 0) OFFSET " + offset + " ROWS FETCH NEXT " + limit + " ROWS ONLY";
    }
    @Override public String buildCountSql(String sql) {
        return "SELECT COUNT(*) FROM (" + sql + ") AS tmp";
    }
    @Override public boolean supportsOffsetFetch() { return true; }
}

分頁參數物件

public class Page implements Serializable {
    private int pageNum = 1;      // 當前頁 (1-based)
    private int pageSize = 10;    // 每頁筆數
    private long total = 0;       // 總筆數
    private boolean countTotal = true;  // 是否查總數
    private String orderBy;       // 排序欄位
    private boolean asc = true;   // 升序/降序
    
    // 計算屬性
    public int getOffset() { return (pageNum - 1) * pageSize; }
    public int getLimit() { return pageSize; }
    public int getPages() { return total == 0 ? 0 : (int)Math.ceil((double)total / pageSize); }
    
    // 靜態工廠
    public static Page of(int pageNum, int pageSize) {
        Page p = new Page();
        p.pageNum = Math.max(1, pageNum);
        p.pageSize = Math.max(1, Math.min(pageSize, 500));  // 限制最大頁面
        return p;
    }
    
    public static Page from(Map params) {
        Page p = new Page();
        p.pageNum = parseInt(params.get("pageNum"), 1);
        p.pageSize = parseInt(params.get("pageSize"), 10);
        p.orderBy = (String) params.get("orderBy");
        p.asc = parseBoolean(params.get("asc"), true);
        return p;
    }
}

Controller 使用範例

@RestController
@RequestMapping("/employees")
public class EmployeeController {
    
    @Autowired EmployeeService employeeService;
    
    @GetMapping
    public Result> list(
            @RequestParam(defaultValue = "1") int pageNum,
            @RequestParam(defaultValue = "10") int pageSize,
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) String orderBy,
            @RequestParam(defaultValue = "true") boolean asc) {
        
        // 1. 封裝查詢參數
        Map params = new HashMap<>();
        params.put("keyword", keyword);
        params.put("page", Page.of(pageNum, pageSize));
        params.put("orderBy", orderBy);
        params.put("asc", asc);
        
        // 2. 呼叫 Service (Mapper 方法名需包含 "page")
        List list = employeeService.selectPage(params);
        Page page = (Page) params.get("page");
        
        // 3. 封裝回傳
        return Result.success(new PageResult<>(list, page.getTotal(), page.getPageNum(), page.getPageSize()));
    }
}

Mapper XML 定義


Spring 整合配置

@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
    
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor interceptor = new PaginationInterceptor();
        Properties props = new Properties();
        props.setProperty("dialect", "mysql");  // mysql/oracle12c/oracle11g/sqlserver2012
        interceptor.setProperties(props);
        return interceptor;
    }
    
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setPlugins(paginationInterceptor());  // 註冊攔截器
        factory.setMapperLocations(new PathMatchingResourcePatternResolver()
            .getResources("classpath:mapper/*.xml"));
        return factory.getObject();
    }
}

進階優化技巧

優化點實作策略
Count 查詢優化簡化 Count SQL:移除 ORDER BY、JOIN、SELECT * 只保留 WHERE
總數快取Redis 快取總筆數,TTL 30-60 秒,減輕 DB 壓力
游標分頁大數據量 (百萬+) 改用 Cursor/Seek 分頁:WHERE id > lastId LIMIT n
合理化參數限制 pageSize 最大值 (如 500)、pageNum 不得超過總頁數
SQL 注入防護${orderBy} 白名單驗證、禁止用戶直接控制排序欄位
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved