├── src ├── test │ ├── resources │ │ ├── mybatis-config.xml │ │ ├── application.yaml │ │ ├── mapper │ │ │ └── simplePagingXMLMapper.xml │ │ └── test2.sql │ └── java │ │ └── com │ │ └── xiwh │ │ └── paginator │ │ ├── TestApplication.java │ │ └── demo │ │ ├── simplePaging │ │ ├── SimplePagingXMLMapper.java │ │ ├── SimplePagingXMLTests.java │ │ ├── SimplePagingMapper.java │ │ └── SimplePagingTests.java │ │ ├── common │ │ ├── ATablePO.java │ │ ├── BTablePO.java │ │ └── PagingController.java │ │ ├── nplusonePaging │ │ ├── NPlusonePagingMapper.java │ │ └── NPlusOnePagingTests.java │ │ ├── sql │ │ └── SQLCountTests.java │ │ └── cache │ │ └── CountCacheTests.java └── main │ └── java │ └── com │ └── xiwh │ └── paginator │ ├── wrappers │ ├── NormalPageWrapper.java │ ├── NPlusOnePageWrapper.java │ ├── PageParamsWrapper.java │ ├── NormalPageWrapperBase.java │ ├── NPlusOnePage.java │ ├── SimplePage.java │ └── PagingRowBounds.java │ ├── cache │ ├── CountResultCache.java │ ├── MybatisMethodCache.java │ └── impl │ │ ├── CountResultCacheImpl.java │ │ └── MybatisMethodCacheImpl.java │ ├── annotations │ ├── NPlusOnePaginator.java │ └── NormalPaginator.java │ ├── utils │ └── StringUtils.java │ ├── sqlGenerator │ ├── DataBaseType.java │ └── PaginatorSqlGenerator.java │ ├── Paginator.java │ └── interceptors │ ├── PaginatorLimitHandler.java │ ├── PaginatorExecuteHandler.java │ ├── BaseInterceptor.java │ └── PaginatorResultHandler.java ├── .gitignore ├── LICENSE ├── pom.xml └── README.md /src/test/resources/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/wrappers/NormalPageWrapper.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.wrappers; 2 | 3 | import java.util.List; 4 | 5 | public interface NormalPageWrapper extends Iterable{ 6 | void init(List list, int count, int startOffset, int physicalPage, int size); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/wrappers/NPlusOnePageWrapper.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.wrappers; 2 | 3 | import java.util.List; 4 | 5 | public interface NPlusOnePageWrapper extends Iterable{ 6 | void init(List list, boolean hasNext, int pageOffset, int physicalPage, int size); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/cache/CountResultCache.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.cache; 2 | 3 | import org.apache.ibatis.cache.CacheKey; 4 | import java.util.concurrent.Callable; 5 | 6 | public interface CountResultCache { 7 | 8 | public int size(); 9 | 10 | public void startClear(); 11 | 12 | public int getCachedCount(CacheKey cacheKey); 13 | 14 | public boolean removeCache(CacheKey cacheKey); 15 | 16 | public int getCacheCount(CacheKey key, Callable countCallable, int expireTime, boolean forceUpdate) throws Exception; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | mvnw 7 | mvnw.cmd 8 | springboot-mybatis-paginator.iml 9 | .mvn 10 | 11 | ### STS ### 12 | .apt_generated 13 | .classpath 14 | .factorypath 15 | .project 16 | .settings 17 | .springBeans 18 | .sts4-cache 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | 26 | ### NetBeans ### 27 | /nbproject/private/ 28 | /nbbuild/ 29 | /dist/ 30 | /nbdist/ 31 | /.nb-gradle/ 32 | build/ 33 | !**/src/main/**/build/ 34 | !**/src/test/**/build/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/annotations/NPlusOnePaginator.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target(ElementType.METHOD) 8 | public @interface NPlusOnePaginator { 9 | /** 10 | * Automatically load parameters through get request 11 | */ 12 | boolean auto() default false; 13 | 14 | /** 15 | * Page offset default 0 16 | */ 17 | int startOffset() default 0; 18 | 19 | /** 20 | * Custom limit statement 21 | */ 22 | boolean customLimit() default false; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/wrappers/PageParamsWrapper.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.wrappers; 2 | 3 | import org.apache.ibatis.session.RowBounds; 4 | 5 | public class PageParamsWrapper { 6 | private int page; 7 | private int limit; 8 | private boolean forceCounting; 9 | public PageParamsWrapper(int page, int limit, boolean forceCounting){ 10 | this.page = page; 11 | this.limit = limit; 12 | this.forceCounting = forceCounting; 13 | } 14 | 15 | public PageParamsWrapper(int page, int limit){ 16 | this(page, limit, false); 17 | } 18 | 19 | public boolean isForceCounting() { 20 | return forceCounting; 21 | } 22 | 23 | public int getPage() { 24 | return page; 25 | } 26 | 27 | public int getLimit() { 28 | return limit; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.utils; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class StringUtils { 6 | 7 | public static boolean isEmpty(String str){ 8 | return str == null || str.isEmpty(); 9 | } 10 | 11 | public static int safeToInt(String str,int defaultVal){ 12 | if(isEmpty(str)){ 13 | return defaultVal; 14 | } 15 | if(isNumeric(str)){ 16 | int temp = Integer.parseInt(str); 17 | if(temp>=0){ 18 | defaultVal = temp; 19 | } 20 | } 21 | return defaultVal; 22 | } 23 | 24 | public static boolean isNumeric(String string){ 25 | if(isEmpty(string)){ 26 | return false; 27 | } 28 | Pattern pattern = Pattern.compile("[0-9]*"); 29 | return pattern.matcher(string).matches(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.http.converter.HttpMessageConverter; 10 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | @SpringBootApplication 17 | @MapperScan("com.xiwh.paginator") 18 | public class TestApplication { 19 | public static void main(String[] args) throws Exception { 20 | SpringApplication.run(TestApplication.class, args); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/wrappers/NormalPageWrapperBase.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.wrappers; 2 | 3 | import java.util.*; 4 | import java.util.function.Consumer; 5 | 6 | public abstract class NormalPageWrapperBase implements NormalPageWrapper { 7 | 8 | private transient List list; 9 | 10 | protected abstract void onInit(List list, int count, int startOffset, int physicalPage, int size); 11 | 12 | @Override 13 | public void init(List list, int count, int startOffset, int physicalPage, int size) { 14 | this.list = list; 15 | onInit(list, count, startOffset, physicalPage, size); 16 | } 17 | 18 | @Override 19 | public Iterator iterator() { 20 | return list.iterator(); 21 | } 22 | 23 | @Override 24 | public void forEach(Consumer action) { 25 | list.forEach(action); 26 | } 27 | 28 | @Override 29 | public Spliterator spliterator() { 30 | return list.spliterator(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/cache/MybatisMethodCache.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.cache; 2 | 3 | import com.xiwh.paginator.annotations.NPlusOnePaginator; 4 | import com.xiwh.paginator.annotations.NormalPaginator; 5 | import org.apache.ibatis.mapping.MappedStatement; 6 | import java.lang.reflect.Method; 7 | 8 | public interface MybatisMethodCache { 9 | 10 | public static int TYPE_NONE = 0; 11 | public static int TYPE_NORMAL = 1; 12 | public static int TYPE_N_PLUS_ONE = 2; 13 | 14 | public MethodInfo getValidMethod(MappedStatement mappedStatement); 15 | 16 | public static interface MethodInfo{ 17 | 18 | public Method getMethod(); 19 | 20 | public int getPaginatorType(); 21 | 22 | public NPlusOnePaginator getNPlusOnePaginator(); 23 | 24 | public NormalPaginator getNormalPaginator(); 25 | 26 | public Class getReturnClass(); 27 | 28 | public Class getGenericReturnClass(); 29 | 30 | public String getCustomCountMapperId(); 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/simplePaging/SimplePagingXMLMapper.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.simplePaging; 2 | 3 | import com.xiwh.paginator.annotations.NormalPaginator; 4 | import com.xiwh.paginator.demo.common.ATablePO; 5 | import com.xiwh.paginator.demo.common.BTablePO; 6 | import com.xiwh.paginator.wrappers.PagingRowBounds; 7 | import com.xiwh.paginator.wrappers.SimplePage; 8 | import org.apache.ibatis.annotations.Mapper; 9 | import org.apache.ibatis.session.RowBounds; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.List; 13 | 14 | @Mapper 15 | @Component 16 | public interface SimplePagingXMLMapper { 17 | @NormalPaginator 18 | List customRowBoundsSelect(String aa, Integer bb, RowBounds rowBounds); 19 | 20 | @NormalPaginator 21 | List customPagingRowBoundsSelect(String aa, Integer bb, PagingRowBounds rowBounds); 22 | 23 | @NormalPaginator(startOffset = 1, cache = true) 24 | SimplePage select(String aa, Integer bb); 25 | } 26 | -------------------------------------------------------------------------------- /src/test/resources/application.yaml: -------------------------------------------------------------------------------- 1 | #全局日志输出等级 2 | logging.level.com.xiwh.paginator: debug 3 | spring: 4 | #数据库配置 5 | datasource: 6 | # mysql 7 | url: jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT&useSSL=false 8 | username: root 9 | password: 12345678 10 | driverClassName: com.mysql.jdbc.Driver 11 | # sqlserver 12 | # url: jdbc:sqlserver://124.70.7.48:1433;DatabaseName=test2 13 | # username: rdsuser 14 | # password: xxxx 15 | # driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver 16 | #posgresql 17 | # url: jdbc:postgresql://127.0.0.1:5432/postgres?useUnicode=true&characterEncoding=utf-8 18 | # username: postgres 19 | # password: 12345678 20 | # driverClassName: org.postgresql.Driver 21 | # type: com.alibaba.druid.pool.DruidDataSource 22 | 23 | mybatis: 24 | config-location: classpath:/mybatis-config.xml 25 | mapper-locations: classpath*:/mapper/*Mapper.xml 26 | 27 | paginator: 28 | size-key: size 29 | page-key: page 30 | default-size: 10 31 | 32 | server: 33 | port: 8080 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 xiwh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/common/ATablePO.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.common; 2 | 3 | import java.io.Serializable; 4 | 5 | public class ATablePO implements Serializable { 6 | private int id; 7 | private String name; 8 | private String value; 9 | 10 | public int getId() { 11 | return id; 12 | } 13 | 14 | public String getName() { 15 | return name; 16 | } 17 | 18 | public String getValue() { 19 | return value; 20 | } 21 | 22 | public void setId(int id) { 23 | this.id = id; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | 30 | public void setValue(String value) { 31 | this.value = value; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | final StringBuffer sb = new StringBuffer("ATableDO{"); 37 | sb.append("id=").append(id); 38 | sb.append(", name='").append(name).append('\''); 39 | sb.append(", value='").append(value).append('\''); 40 | sb.append('}'); 41 | return sb.toString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/annotations/NormalPaginator.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target(ElementType.METHOD) 8 | public @interface NormalPaginator { 9 | 10 | /** 11 | * Automatically load parameters through get request 12 | */ 13 | boolean auto() default false; 14 | 15 | /** 16 | * Cache count results 17 | */ 18 | boolean cache() default false; 19 | 20 | /** 21 | * Count SQL optimization 22 | */ 23 | boolean countOptimization() default true; 24 | 25 | /** 26 | * Count cache expiry time, in seconds 27 | */ 28 | int cacheExpiryTime() default 3600; 29 | 30 | /** 31 | * Page offset default 0 32 | */ 33 | int startOffset() default 0; 34 | 35 | /** 36 | * Custom count statement, which is automatically counted by default 37 | */ 38 | String countMethod() default ""; 39 | 40 | /** 41 | * Custom limit statement 42 | * When customizing the limit, you must also customize the count 43 | */ 44 | boolean customLimit() default false; 45 | } 46 | -------------------------------------------------------------------------------- /src/test/resources/mapper/simplePagingXMLMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/nplusonePaging/NPlusonePagingMapper.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.nplusonePaging; 2 | 3 | import com.xiwh.paginator.annotations.NPlusOnePaginator; 4 | import com.xiwh.paginator.demo.common.ATablePO; 5 | import com.xiwh.paginator.wrappers.NPlusOnePage; 6 | import com.xiwh.paginator.wrappers.PagingRowBounds; 7 | import org.apache.ibatis.annotations.Mapper; 8 | import org.apache.ibatis.annotations.Select; 9 | import org.apache.ibatis.session.RowBounds; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.List; 13 | 14 | @Mapper 15 | @Component 16 | public interface NPlusonePagingMapper { 17 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa}") 18 | @NPlusOnePaginator 19 | List customRowBoundsSelect(String aa, Integer bb, RowBounds rowBounds); 20 | 21 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa}") 22 | @NPlusOnePaginator 23 | List customPagingRowBoundsSelect(String aa, Integer bb, PagingRowBounds rowBounds); 24 | 25 | @Select("SELECT * FROM a") 26 | @NPlusOnePaginator(startOffset = 1) 27 | NPlusOnePage select(); 28 | 29 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa}") 30 | @NPlusOnePaginator(auto = true, startOffset = 1) 31 | NPlusOnePage requestPaging(Integer aa, Integer bb); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/common/BTablePO.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.common; 2 | 3 | import java.io.Serializable; 4 | 5 | public class BTablePO implements Serializable { 6 | private int id; 7 | private String name; 8 | private String value; 9 | private String value2; 10 | 11 | public int getId() { 12 | return id; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public String getValue() { 20 | return value; 21 | } 22 | 23 | public void setId(int id) { 24 | this.id = id; 25 | } 26 | 27 | public void setName(String name) { 28 | this.name = name; 29 | } 30 | 31 | public void setValue(String value) { 32 | this.value = value; 33 | } 34 | 35 | public String getValue2() { 36 | return value2; 37 | } 38 | 39 | public void setValue2(String value2) { 40 | this.value2 = value2; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | final StringBuffer sb = new StringBuffer("BTableDO{"); 46 | sb.append("id=").append(id); 47 | sb.append(", name='").append(name).append('\''); 48 | sb.append(", value='").append(value).append('\''); 49 | sb.append(", value2='").append(value2).append('\''); 50 | sb.append('}'); 51 | return sb.toString(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/sqlGenerator/DataBaseType.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.sqlGenerator; 2 | 3 | public enum DataBaseType { 4 | SQLITE, 5 | MYSQL, 6 | ORACLE, 7 | POSGRESQL, 8 | SQLSERVER, 9 | MARIADB; 10 | 11 | public String toDruidDbType(){ 12 | switch (this){ 13 | case POSGRESQL: 14 | return "postgresql"; 15 | case MYSQL: 16 | return "mysql"; 17 | case ORACLE: 18 | return "oracle"; 19 | case SQLSERVER: 20 | return "sqlserver"; 21 | case MARIADB: 22 | return "mariadb"; 23 | case SQLITE: 24 | return "sqlite"; 25 | } 26 | return null; 27 | } 28 | 29 | public static DataBaseType findByURL(String str){ 30 | str = str.toLowerCase(); 31 | if(str.contains(":mysql:")){ 32 | return MYSQL; 33 | }else if(str.contains(":mariadb:")){ 34 | return MARIADB; 35 | }else if(str.contains(":postgresql:")){ 36 | return POSGRESQL; 37 | }else if(str.contains(":oracle:")){ 38 | return ORACLE; 39 | }else if(str.contains(":sqlserver:")){ 40 | return SQLSERVER; 41 | }else if(str.contains(":sqlite:")){ 42 | return SQLITE; 43 | } 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/simplePaging/SimplePagingXMLTests.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.simplePaging; 2 | 3 | import com.xiwh.paginator.Paginator; 4 | import com.xiwh.paginator.TestApplication; 5 | import com.xiwh.paginator.wrappers.PagingRowBounds; 6 | import org.apache.ibatis.session.RowBounds; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.test.context.ContextConfiguration; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | @SuppressWarnings("ALL") 15 | @SpringBootTest 16 | @RunWith(SpringRunner.class) 17 | @ContextConfiguration(classes = TestApplication.class) 18 | class SimplePagingXMLTests { 19 | 20 | @Autowired 21 | SimplePagingXMLMapper mapper; 22 | 23 | @Test 24 | void testOriginalPaging() { 25 | RowBounds rowBounds = new RowBounds(10,10); 26 | Object list = mapper.customRowBoundsSelect("\"AA'",0, rowBounds); 27 | System.out.println(list); 28 | } 29 | 30 | @Test 31 | void testOriginalPaging2() { 32 | PagingRowBounds rowBounds = new PagingRowBounds(1,10,1,false); 33 | Object list = mapper.customPagingRowBoundsSelect("\"AA'",0, rowBounds); 34 | System.out.println(rowBounds); 35 | System.out.println(list); 36 | } 37 | 38 | @Test 39 | void testBasicPaging() { 40 | Paginator.paginate(2,10); 41 | Object list = mapper.select("\"AA'",0); 42 | System.out.println(list); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/nplusonePaging/NPlusOnePagingTests.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.nplusonePaging; 2 | 3 | import com.xiwh.paginator.Paginator; 4 | import com.xiwh.paginator.TestApplication; 5 | import com.xiwh.paginator.demo.simplePaging.SimplePagingMapper; 6 | import com.xiwh.paginator.wrappers.PagingRowBounds; 7 | import org.apache.ibatis.session.RowBounds; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | @SuppressWarnings("ALL") 16 | @SpringBootTest 17 | @RunWith(SpringRunner.class) 18 | @ContextConfiguration(classes = TestApplication.class) 19 | class NPlusOnePagingTests { 20 | 21 | @Autowired 22 | NPlusonePagingMapper mapper; 23 | 24 | @Test 25 | void testOriginalPaging() { 26 | RowBounds rowBounds = new RowBounds(10,10); 27 | Object list = mapper.customRowBoundsSelect("\"AA'",0, rowBounds); 28 | System.out.println(list); 29 | } 30 | 31 | @Test 32 | void testOriginalPaging2() { 33 | PagingRowBounds rowBounds = new PagingRowBounds(1,10,1,false); 34 | Object list = mapper.customPagingRowBoundsSelect("\"AA'",0, rowBounds); 35 | System.out.println(rowBounds); 36 | System.out.println(list); 37 | } 38 | 39 | @Test 40 | void testBasicPaging() { 41 | Paginator.paginate(1,10); 42 | Object list = mapper.select(); 43 | System.out.println(list); 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/simplePaging/SimplePagingMapper.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.simplePaging; 2 | 3 | import com.xiwh.paginator.annotations.NormalPaginator; 4 | import com.xiwh.paginator.demo.common.ATablePO; 5 | import com.xiwh.paginator.wrappers.PagingRowBounds; 6 | import com.xiwh.paginator.wrappers.SimplePage; 7 | import org.apache.ibatis.annotations.Mapper; 8 | import org.apache.ibatis.annotations.Select; 9 | import org.apache.ibatis.session.RowBounds; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.List; 13 | 14 | @Mapper 15 | public interface SimplePagingMapper { 16 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa} order by id") 17 | @NormalPaginator 18 | List customRowBoundsSelect(Integer aa, Integer bb, RowBounds rowBounds); 19 | 20 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa} order by id") 21 | @NormalPaginator 22 | List customPagingRowBoundsSelect(Integer aa, Integer bb, PagingRowBounds rowBounds); 23 | 24 | @Select("SELECT * FROM a order by id") 25 | @NormalPaginator() 26 | SimplePage select(Integer aa, Integer bb); 27 | 28 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa} order by id") 29 | @NormalPaginator(auto = true, startOffset = 1, cacheExpiryTime = 300) 30 | SimplePage requestPaging(Integer aa, Integer bb); 31 | 32 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa} order by id") 33 | @NormalPaginator(countMethod = "customCount") 34 | SimplePage customCountSelect(Integer aa, Integer bb); 35 | 36 | @Select("SELECT count(1) FROM a where id != ${bb} or id != #{aa}") 37 | Integer customCount(Integer aa, Integer bb); 38 | 39 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa} limit :offset,:limit") 40 | @NormalPaginator(customLimit = true, countMethod = "customCount") 41 | SimplePage customLimitSelect(Integer aa, Integer bb); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/wrappers/NPlusOnePage.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.wrappers; 2 | 3 | import java.util.*; 4 | import java.util.function.Consumer; 5 | 6 | public class NPlusOnePage implements NPlusOnePageWrapper { 7 | 8 | private List list; 9 | private int page; 10 | private int size; 11 | private boolean hasNext; 12 | private int pageOffset; 13 | 14 | private Map cachedMap = null; 15 | 16 | @Override 17 | public void init(List list, boolean hasNext, int pageOffset, int physicalPage, int size) { 18 | this.list = list; 19 | this.page = physicalPage+pageOffset; 20 | this.size = size; 21 | this.pageOffset = pageOffset; 22 | this.hasNext = hasNext; 23 | } 24 | 25 | public List list() { 26 | return list; 27 | } 28 | 29 | public int getSize() { 30 | return size; 31 | } 32 | 33 | public int getPage() { 34 | return page; 35 | } 36 | 37 | public boolean hasNext() { 38 | return hasNext; 39 | } 40 | 41 | @Override 42 | public Iterator iterator() { 43 | return list.iterator(); 44 | } 45 | 46 | @Override 47 | public void forEach(Consumer action) { 48 | list.forEach(action); 49 | } 50 | 51 | @Override 52 | public Spliterator spliterator() { 53 | return list.spliterator(); 54 | } 55 | 56 | /** 57 | * If modifiable = false, the map will be cached 58 | * @param modifiable Allow modification? 59 | * @return 60 | */ 61 | public Map toMap(boolean modifiable){ 62 | if(!modifiable && cachedMap!=null) { 63 | return cachedMap; 64 | }else{ 65 | Map map = new HashMap(8); 66 | map.put("list", this.list()); 67 | map.put("page", this.page); 68 | map.put("size", this.size); 69 | map.put("has_next", this.hasNext()); 70 | if(!modifiable){ 71 | map = Collections.unmodifiableMap(map); 72 | cachedMap = map; 73 | } 74 | return map; 75 | } 76 | } 77 | 78 | /** 79 | * toMap == toMap(false) 80 | * @return Unmodifiable map 81 | */ 82 | public Map toMap(){ 83 | return toMap(false); 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | final StringBuffer sb = new StringBuffer("NPlusOnePage{"); 89 | sb.append("list=").append(list); 90 | sb.append(", page=").append(page); 91 | sb.append(", size=").append(size); 92 | sb.append(", hasNext=").append(hasNext); 93 | sb.append('}'); 94 | return sb.toString(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/Paginator.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator; 2 | 3 | import com.xiwh.paginator.interceptors.PaginatorLimitHandler; 4 | import com.xiwh.paginator.utils.StringUtils; 5 | import com.xiwh.paginator.wrappers.PageParamsWrapper; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.ApplicationContext; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.annotation.PostConstruct; 12 | 13 | @Component("Springboot-Mybatis-Paginator") 14 | public class Paginator { 15 | 16 | protected static Paginator instance; 17 | 18 | ThreadLocal pageParamsThreadLocal = new ThreadLocal<>(); 19 | 20 | @Autowired 21 | PaginatorLimitHandler paginatorInterceptor; 22 | @Autowired 23 | private Environment environment; 24 | @Autowired 25 | private ApplicationContext context; 26 | String queryPageKey; 27 | String querySizeKey; 28 | Integer defaultSize; 29 | 30 | @PostConstruct 31 | private void init(){ 32 | querySizeKey = environment.getProperty("paginator.size-key", "size"); 33 | queryPageKey = environment.getProperty("paginator.page-key", "page"); 34 | defaultSize = environment.getProperty("paginator.default-size", Integer.class, 10); 35 | instance = this; 36 | } 37 | 38 | private void _startPaginate(int page, int size, boolean forceCounting){ 39 | pageParamsThreadLocal.set(new PageParamsWrapper(page, size, forceCounting)); 40 | } 41 | 42 | protected void _autoInject() throws ClassNotFoundException { 43 | org.springframework.web.context.request.ServletRequestAttributes servletRequestAttributes = 44 | (org.springframework.web.context.request.ServletRequestAttributes) 45 | org.springframework.web.context.request.RequestContextHolder.getRequestAttributes(); 46 | javax.servlet.http.HttpServletRequest request = servletRequestAttributes.getRequest(); 47 | int page = StringUtils.safeToInt(request.getParameter(this.queryPageKey),0); 48 | int size = StringUtils.safeToInt(request.getParameter(this.querySizeKey),defaultSize); 49 | _startPaginate(page, size, false); 50 | } 51 | 52 | public static PageParamsWrapper currentPageParams(){ 53 | return instance.pageParamsThreadLocal.get(); 54 | } 55 | 56 | public static PageParamsWrapper readPageParams(){ 57 | PageParamsWrapper pagingRowBounds = instance.pageParamsThreadLocal.get(); 58 | instance.pageParamsThreadLocal.remove(); 59 | return pagingRowBounds; 60 | } 61 | 62 | public static void paginate(int page, int size){ 63 | instance._startPaginate(page, size, false); 64 | } 65 | 66 | public static void paginate(int page, int size, boolean forceCounting){ 67 | instance._startPaginate(page, size, forceCounting); 68 | } 69 | 70 | 71 | public static void autoInjectFromRequest(){ 72 | try { 73 | instance._autoInject(); 74 | }catch (Exception e){ 75 | throw new RuntimeException(e); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/sql/SQLCountTests.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.sql; 2 | 3 | import com.xiwh.paginator.TestApplication; 4 | import com.xiwh.paginator.sqlGenerator.DataBaseType; 5 | import com.xiwh.paginator.sqlGenerator.PaginatorSqlGenerator; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | 12 | @SpringBootTest 13 | @RunWith(SpringRunner.class) 14 | @ContextConfiguration(classes = TestApplication.class) 15 | class SQLCountTests { 16 | 17 | @Test 18 | void countSql() { 19 | PaginatorSqlGenerator sqlGenerator = new PaginatorSqlGenerator("SELECT a.id FROM a GROUP BY a.id HAVING id != 3 ORDER BY a.id", DataBaseType.MYSQL); 20 | System.out.println("countSql1:\n"+sqlGenerator.toCountSql()); 21 | System.out.println(); 22 | sqlGenerator = new PaginatorSqlGenerator("SELECT a.id FROM a where id != 3 ORDER BY a.id", DataBaseType.MYSQL); 23 | System.out.println("countSql2:\r\n"+sqlGenerator.toCountSql()); 24 | System.out.println(); 25 | sqlGenerator = new PaginatorSqlGenerator("SELECT\n" + 26 | "\ta.id AS aid \n" + 27 | "FROM\n" + 28 | "\ta\n" + 29 | "LEFT JOIN b on a.id = b.id\n" + 30 | "WHERE b.id in (select max(id) FROM a)", DataBaseType.MYSQL); 31 | System.out.println("countSql3:\r\n"+sqlGenerator.toCountSql()); 32 | } 33 | 34 | @Test 35 | void optimizedCountSql() { 36 | PaginatorSqlGenerator sqlGenerator = new PaginatorSqlGenerator( 37 | "select a.id,b.name,c.value FROM a\n" + 38 | "left join b on b.id = a.id\n" + 39 | "left join c on c.id = b.id\n" + 40 | "group by b.name,c.value\n", 41 | DataBaseType.POSGRESQL 42 | ); 43 | System.out.println("countSql1:\n"+sqlGenerator.toOptimizedCountSql()); 44 | System.out.println(); 45 | System.out.println("limitSql1:\n"+sqlGenerator.toLimitSql(0,100)); 46 | System.out.println("---------------------"); 47 | sqlGenerator = new PaginatorSqlGenerator( 48 | "select a.id,b.name,c.value FROM a\n " + 49 | "left join b on b.id = a.id\n " + 50 | "left join c on c.id = b.id\n " + 51 | "group by a.id\n" + 52 | "having id != 0", 53 | DataBaseType.MYSQL 54 | ); 55 | System.out.println("countSql2:\n"+sqlGenerator.toOptimizedCountSql()); 56 | System.out.println(); 57 | System.out.println("limitSql2:\n"+sqlGenerator.toLimitSql(0,100)); 58 | System.out.println("---------------------"); 59 | sqlGenerator = new PaginatorSqlGenerator( 60 | "SELECT a.a1, a.a2, a.a3 FROM TABLE1 a, TABLE2 b WHERE a.a=t2.a", 61 | DataBaseType.ORACLE); 62 | System.out.println("countSql3:\n"+sqlGenerator.toOptimizedCountSql()); 63 | System.out.println(); 64 | System.out.println("limitSql3:\n"+sqlGenerator.toLimitSql(0,100)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/wrappers/SimplePage.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.wrappers; 2 | 3 | import java.util.*; 4 | 5 | public class SimplePage extends NormalPageWrapperBase { 6 | 7 | private int total; 8 | private transient int startOffset; 9 | private transient int physicalPage; 10 | private int page; 11 | private int size; 12 | private int totalPage; 13 | private List list; 14 | 15 | private Map cachedMap = null; 16 | 17 | public boolean hasNext(){ 18 | return list.size()>=size; 19 | } 20 | 21 | public boolean hasLast(){ 22 | return physicalPage>0; 23 | } 24 | 25 | public int size(){ 26 | return size; 27 | } 28 | 29 | public int page(){ 30 | return page; 31 | } 32 | 33 | public int total(){ 34 | return total; 35 | } 36 | 37 | public int totalPage(){ 38 | return totalPage; 39 | } 40 | 41 | public List list() { 42 | return list; 43 | } 44 | 45 | /** 46 | * Paged callback 47 | * @param list 48 | * @param count 49 | * @param startOffset 50 | * @param physicalPage 51 | * @param size 52 | */ 53 | @Override 54 | public void onInit(List list, int count, int startOffset, int physicalPage, int size) { 55 | this.list = list; 56 | this.total = count; 57 | this.physicalPage = physicalPage; 58 | this.startOffset = startOffset; 59 | this.page = physicalPage + startOffset; 60 | this.size = size; 61 | this.totalPage = total/size+(total%size==0?0:1); 62 | } 63 | 64 | /** 65 | * If modifiable = false, the map will be cached 66 | * @param modifiable Allow modification? 67 | * @return 68 | */ 69 | public Map toMap(boolean modifiable){ 70 | if(!modifiable && cachedMap!=null) { 71 | return cachedMap; 72 | }else{ 73 | Map map = new HashMap(8); 74 | map.put("list", this.list()); 75 | map.put("page", this.page); 76 | map.put("size", this.size); 77 | map.put("total", this.total); 78 | map.put("total_page", totalPage); 79 | map.put("has_last", this.hasLast()); 80 | map.put("has_next", this.hasNext()); 81 | if(!modifiable){ 82 | map = Collections.unmodifiableMap(map); 83 | cachedMap = map; 84 | } 85 | return map; 86 | } 87 | } 88 | 89 | /** 90 | * toMap == toMap(false) 91 | * @return Unmodifiable map 92 | */ 93 | public Map toMap(){ 94 | return toMap(false); 95 | } 96 | 97 | @Override 98 | public String toString() { 99 | final StringBuffer sb = new StringBuffer("SimplePage{"); 100 | sb.append("total=").append(total); 101 | sb.append(", startOffset=").append(startOffset); 102 | sb.append(", physicalPage=").append(physicalPage); 103 | sb.append(", page=").append(page); 104 | sb.append(", size=").append(size); 105 | sb.append(", hasLast=").append(hasLast()); 106 | sb.append(", hasNext=").append(hasNext()); 107 | sb.append(", totalPage=").append(totalPage); 108 | sb.append(", list=").append(list); 109 | sb.append('}'); 110 | return sb.toString(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/wrappers/PagingRowBounds.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.wrappers; 2 | 3 | import org.apache.ibatis.session.RowBounds; 4 | 5 | import java.util.List; 6 | 7 | public class PagingRowBounds extends RowBounds { 8 | private boolean forceCounting = false; 9 | private int end = -1; 10 | private int page = -1; 11 | private int pageOffset = -1; 12 | private int count = -1; 13 | private boolean hasNext; 14 | public PagingRowBounds(int page, int size, int pageOffset, boolean forceCounting){ 15 | super((page-pageOffset)*size, size); 16 | this.page = page; 17 | this.pageOffset = pageOffset; 18 | this.end = getOffset() + getLimit(); 19 | this.forceCounting = forceCounting; 20 | } 21 | 22 | public PagingRowBounds(int page, int size){ 23 | super(page*size, size); 24 | this.page = page; 25 | this.pageOffset = 0; 26 | this.forceCounting = false; 27 | this.end = getOffset() + getLimit(); 28 | } 29 | 30 | public PagingRowBounds(PageParamsWrapper pageParams, int pageOffset){ 31 | super((pageParams.getPage()-pageOffset) * pageParams.getLimit(), pageParams.getLimit()); 32 | this.forceCounting = pageParams.isForceCounting(); 33 | this.end = getOffset() + getLimit(); 34 | this.page = pageParams.getPage() - pageOffset; 35 | this.pageOffset = pageOffset; 36 | } 37 | 38 | public PagingRowBounds(RowBounds rowBounds){ 39 | super(rowBounds.getOffset(), rowBounds.getLimit()); 40 | this.forceCounting = false; 41 | this.page = getOffset()/getLimit(); 42 | this.pageOffset = 0; 43 | this.end = getOffset() + getLimit(); 44 | } 45 | 46 | public boolean isForceCounting() { 47 | return forceCounting; 48 | } 49 | 50 | public int getEnd(){ 51 | return end; 52 | } 53 | 54 | public int getPage() { 55 | return page; 56 | } 57 | 58 | public int getPageOffset() { 59 | return pageOffset; 60 | } 61 | 62 | public void setCount(int count) { 63 | this.count = count; 64 | } 65 | 66 | public int getCount() { 67 | return count; 68 | } 69 | 70 | public void setHasNext(boolean hasNext) { 71 | this.hasNext = hasNext; 72 | } 73 | 74 | public boolean hasNext() { 75 | return hasNext; 76 | } 77 | 78 | public T toNormalPage(Class clazz, List list){ 79 | try { 80 | T obj = clazz.newInstance(); 81 | obj.init(list, getCount(), pageOffset, getPage()-pageOffset,getLimit()); 82 | return obj; 83 | } catch (Exception e) { 84 | throw new RuntimeException("toNormalPage Error!", e); 85 | } 86 | } 87 | 88 | public T toNPlushOnePage(Class clazz, List list){ 89 | try { 90 | T obj = clazz.newInstance(); 91 | obj.init(list, hasNext, pageOffset, getPage()-pageOffset, getLimit()); 92 | return obj; 93 | } catch (Exception e) { 94 | throw new RuntimeException("toNormalPage Error!", e); 95 | } 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | final StringBuffer sb = new StringBuffer("PagingRowBounds{"); 101 | sb.append("forceCounting=").append(forceCounting); 102 | sb.append(", offset=").append(getOffset()); 103 | sb.append(", limit=").append(getLimit()); 104 | sb.append(", end=").append(end); 105 | sb.append(", page=").append(page); 106 | sb.append(", pageOffset=").append(pageOffset); 107 | sb.append(", count=").append(count); 108 | sb.append(", hasNext=").append(hasNext); 109 | sb.append('}'); 110 | return sb.toString(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/simplePaging/SimplePagingTests.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.simplePaging; 2 | 3 | import com.xiwh.paginator.Paginator; 4 | import com.xiwh.paginator.TestApplication; 5 | import com.xiwh.paginator.demo.common.ATablePO; 6 | import com.xiwh.paginator.wrappers.PagingRowBounds; 7 | import com.xiwh.paginator.wrappers.SimplePage; 8 | import org.apache.ibatis.session.RowBounds; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.test.context.ContextConfiguration; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 18 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 19 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 20 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 21 | import org.springframework.web.context.WebApplicationContext; 22 | 23 | import javax.annotation.PostConstruct; 24 | import java.util.List; 25 | 26 | @SuppressWarnings("ALL") 27 | @SpringBootTest 28 | @RunWith(SpringRunner.class) 29 | @ContextConfiguration(classes = TestApplication.class) 30 | class SimplePagingTests { 31 | 32 | @Autowired 33 | private WebApplicationContext wac ; 34 | @Autowired 35 | SimplePagingMapper mapper; 36 | 37 | private MockMvc mockMvc; 38 | 39 | @PostConstruct 40 | public void setUp(){ 41 | mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); 42 | } 43 | 44 | @Test 45 | void testBasicPaging() { 46 | Paginator.paginate(0,10); 47 | SimplePage page = mapper.select(321,0); 48 | System.out.println(page); 49 | System.out.println(page.hasLast()); 50 | System.out.println(page.hasNext()); 51 | System.out.println(page.totalPage()); 52 | System.out.println(page.total()); 53 | System.out.println(page.size()); 54 | System.out.println(page.list()); 55 | System.out.println(page.toMap()); 56 | for (ATablePO item:page) { 57 | item.setName(item.getId()+":"+item.getName()); 58 | } 59 | 60 | } 61 | 62 | @Test 63 | void testOriginalPaging() { 64 | RowBounds rowBounds = new RowBounds(10,10); 65 | Object list = mapper.customRowBoundsSelect(321,0, rowBounds); 66 | System.out.println(list); 67 | } 68 | 69 | @Test 70 | void testAdvancedPaging() { 71 | PagingRowBounds rowBounds = new PagingRowBounds(3,10,1,false); 72 | List list = mapper.customPagingRowBoundsSelect(321,0, rowBounds); 73 | System.out.println(rowBounds); 74 | System.out.println(rowBounds.toNormalPage(SimplePage.class, list)); 75 | } 76 | 77 | @Test 78 | void testCustomCountPaging() { 79 | Paginator.paginate(2,10); 80 | Object list = mapper.customCountSelect(321,0); 81 | System.out.println(list); 82 | } 83 | 84 | @Test 85 | void testCustomLimitPaging() { 86 | Paginator.paginate(0,10); 87 | Object list = mapper.customCountSelect(123,0); 88 | System.out.println(list); 89 | } 90 | 91 | @Test 92 | void testGetRequestPaging() throws Exception { 93 | System.out.println(mockMvc); 94 | mockMvc.perform(MockMvcRequestBuilders.get("/test1?page=3&size=10") 95 | .contentType(MediaType.APPLICATION_FORM_URLENCODED)) 96 | .andExpect(MockMvcResultMatchers.status().isOk()) 97 | .andDo(MockMvcResultHandlers.print());; 98 | // System.out.println(mvcResult.getAsyncResult()); 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/interceptors/PaginatorLimitHandler.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.interceptors; 2 | 3 | import com.xiwh.paginator.Paginator; 4 | import com.xiwh.paginator.cache.MybatisMethodCache; 5 | import com.xiwh.paginator.cache.impl.MybatisMethodCacheImpl; 6 | import com.xiwh.paginator.sqlGenerator.PaginatorSqlGenerator; 7 | import com.xiwh.paginator.wrappers.PagingRowBounds; 8 | import org.apache.ibatis.executor.statement.StatementHandler; 9 | import org.apache.ibatis.mapping.*; 10 | import org.apache.ibatis.plugin.*; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Qualifier; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.lang.reflect.Field; 16 | import java.sql.*; 17 | 18 | 19 | @Intercepts({ 20 | @Signature( 21 | type = StatementHandler.class, 22 | method = "prepare", 23 | args = {Connection.class, Integer.class} 24 | ) 25 | }) 26 | @Component 27 | public class PaginatorLimitHandler extends BaseInterceptor { 28 | @Autowired 29 | Paginator paginator; 30 | @Autowired 31 | @Qualifier("mybatisMethodCacheImpl") 32 | MybatisMethodCache methodCache; 33 | 34 | 35 | @Override 36 | public Object intercept(Invocation invocation) throws Throwable { 37 | StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); 38 | PagingInfoWrapper pagingInfoWrapper = getPagingInfoWrapper(); 39 | if(pagingInfoWrapper==null){ 40 | return invocation.proceed(); 41 | }else if(pagingInfoWrapper.counting){ 42 | return invocation.proceed(); 43 | } 44 | BoundSql boundSql = statementHandler.getBoundSql(); 45 | pagingInfoWrapper.boundSql = boundSql; 46 | pagingInfoWrapper.statementHandler = statementHandler; 47 | //Get raw SQL 48 | String sql = boundSql.getSql(); 49 | //Generate paging statement 50 | PaginatorSqlGenerator sqlGenerator = new PaginatorSqlGenerator(sql, pagingInfoWrapper.dataBaseType); 51 | Field field = boundSql.getClass().getDeclaredField("sql"); 52 | field.setAccessible(true); 53 | PagingRowBounds pagingRowBounds = pagingInfoWrapper.pagingRowBounds; 54 | pagingInfoWrapper.sqlGenerator = sqlGenerator; 55 | MybatisMethodCache.MethodInfo methodInfo = pagingInfoWrapper.methodInfo; 56 | 57 | if (methodInfo.getPaginatorType() == MybatisMethodCache.TYPE_NORMAL) { 58 | if(!methodInfo.getNormalPaginator().customLimit()) { 59 | //Modify limit sql 60 | String limitSql = sqlGenerator.toLimitSql(pagingRowBounds.getOffset(), pagingRowBounds.getLimit()); 61 | field.set(boundSql, limitSql); 62 | }else{ 63 | //Custom limit 64 | sql = sql.replaceAll( 65 | ":limit", String.valueOf(pagingRowBounds.getLimit()) 66 | ).replaceAll(":offset", String.valueOf(pagingRowBounds.getOffset()) 67 | ).replaceAll(":end", String.valueOf(pagingRowBounds.getEnd())); 68 | field.set(boundSql, sql); 69 | } 70 | 71 | }else if(methodInfo.getPaginatorType() == MybatisMethodCache.TYPE_N_PLUS_ONE) { 72 | if (!methodInfo.getNPlusOnePaginator().customLimit()) { 73 | String limitSql = sqlGenerator.toLimitSql(pagingRowBounds.getOffset(), pagingRowBounds.getLimit()+1); 74 | field.set(boundSql, limitSql); 75 | }else{ 76 | //Custom limit 77 | sql = sql.replaceAll( 78 | ":limit", String.valueOf(pagingRowBounds.getLimit()+1) 79 | ).replaceAll(":offset", String.valueOf(pagingRowBounds.getOffset()) 80 | ).replaceAll(":end", String.valueOf(pagingRowBounds.getEnd()+1)); 81 | field.set(boundSql, sql); 82 | } 83 | } 84 | return invocation.proceed(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/common/PagingController.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.common; 2 | 3 | import com.xiwh.paginator.demo.nplusonePaging.NPlusonePagingMapper; 4 | import com.xiwh.paginator.demo.simplePaging.SimplePagingMapper; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.util.Map; 11 | 12 | @SuppressWarnings("ALL") 13 | @RestController 14 | class PagingController { 15 | 16 | @Autowired 17 | SimplePagingMapper simplePagingMapper; 18 | @Autowired 19 | NPlusonePagingMapper nPlusonePagingMapper; 20 | 21 | /** 22 | * http://127.0.0.1:8080/test1?page=1&size=20 23 | * { 24 | * "total": 16, 25 | * "size": 10, 26 | * "total_page": 2, 27 | * "has_last": false, 28 | * "has_next": true, 29 | * "page": 1, 30 | * "list": [{ 31 | * "id": 1, 32 | * "name": "a", 33 | * "value": "v1" 34 | * }, { 35 | * "id": 2, 36 | * "name": "a", 37 | * "value": "v1" 38 | * }, { 39 | * "id": 3, 40 | * "name": "a", 41 | * "value": "v1" 42 | * }, { 43 | * "id": 4, 44 | * "name": "d", 45 | * "value": "v4" 46 | * }, { 47 | * "id": 5, 48 | * "name": "e", 49 | * "value": "v5" 50 | * }, { 51 | * "id": 6, 52 | * "name": "f", 53 | * "value": "v6" 54 | * }, { 55 | * "id": 7, 56 | * "name": "g", 57 | * "value": "v7" 58 | * }, { 59 | * "id": 8, 60 | * "name": "h", 61 | * "value": "v8" 62 | * }, { 63 | * "id": 9, 64 | * "name": "i", 65 | * "value": "v9" 66 | * }, { 67 | * "id": 10, 68 | * "name": "j", 69 | * "value": "v10" 70 | * }] 71 | * } 72 | */ 73 | @RequestMapping("/test1") 74 | public Map requestPaging(HttpServletRequest request){ 75 | System.out.println("[[["+Thread.currentThread().getName()); 76 | Map map = simplePagingMapper.requestPaging(321,123).toMap(); 77 | System.out.println("]]]"); 78 | return map; 79 | } 80 | 81 | /** 82 | * http://127.0.0.1:8080/test2?page=1&size=20 83 | * { 84 | * "has_next": true, 85 | * "page": 1, 86 | * "list": [{ 87 | * "id": 1, 88 | * "name": "a", 89 | * "value": "v1" 90 | * }, { 91 | * "id": 2, 92 | * "name": "a", 93 | * "value": "v1" 94 | * }, { 95 | * "id": 3, 96 | * "name": "a", 97 | * "value": "v1" 98 | * }, { 99 | * "id": 4, 100 | * "name": "d", 101 | * "value": "v4" 102 | * }, { 103 | * "id": 5, 104 | * "name": "e", 105 | * "value": "v5" 106 | * }, { 107 | * "id": 6, 108 | * "name": "f", 109 | * "value": "v6" 110 | * }, { 111 | * "id": 7, 112 | * "name": "g", 113 | * "value": "v7" 114 | * }, { 115 | * "id": 8, 116 | * "name": "h", 117 | * "value": "v8" 118 | * }, { 119 | * "id": 9, 120 | * "name": "i", 121 | * "value": "v9" 122 | * }, { 123 | * "id": 10, 124 | * "name": "j", 125 | * "value": "v10" 126 | * }, { 127 | * "id": 11, 128 | * "name": "k", 129 | * "value": "v11" 130 | * }, { 131 | * "id": 12, 132 | * "name": "l", 133 | * "value": "v12" 134 | * }, { 135 | * "id": 13, 136 | * "name": "m", 137 | * "value": "v13" 138 | * }, { 139 | * "id": 14, 140 | * "name": "n", 141 | * "value": "v14" 142 | * }, { 143 | * "id": 15, 144 | * "name": "o", 145 | * "value": "v15" 146 | * }, { 147 | * "id": 16, 148 | * "name": "p", 149 | * "value": "v16" 150 | * }, { 151 | * "id": 17, 152 | * "name": "q", 153 | * "value": "v17" 154 | * }, { 155 | * "id": 18, 156 | * "name": "r", 157 | * "value": "v18" 158 | * }, { 159 | * "id": 19, 160 | * "name": "s", 161 | * "value": "v19" 162 | * }, { 163 | * "id": 20, 164 | * "name": "t", 165 | * "value": "v20" 166 | * }], 167 | * "size": 20 168 | * } 169 | */ 170 | @RequestMapping("/test2") 171 | public Map requestNplusOnePaging(HttpServletRequest request){ 172 | System.out.println("[[["+Thread.currentThread().getId()); 173 | Map map = nPlusonePagingMapper.requestPaging(321,123).toMap(); 174 | System.out.println("]]]"); 175 | return map; 176 | } 177 | 178 | } -------------------------------------------------------------------------------- /src/test/resources/test2.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : loc5.6 5 | Source Server Type : MySQL 6 | Source Server Version : 50728 7 | Source Host : localhost:3306 8 | Source Schema : test2 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50728 12 | File Encoding : 65001 13 | 14 | Date: 25/09/2020 14:22:46 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for a 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `a`; 24 | CREATE TABLE `a` ( 25 | `id` int(11) NOT NULL AUTO_INCREMENT, 26 | `name` varchar(255) NOT NULL, 27 | `value` varchar(255) DEFAULT NULL, 28 | PRIMARY KEY (`id`) USING BTREE 29 | ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=latin1; 30 | 31 | -- ---------------------------- 32 | -- Records of a 33 | -- ---------------------------- 34 | BEGIN; 35 | INSERT INTO `a` VALUES (1, 'a', 'v1'); 36 | INSERT INTO `a` VALUES (2, 'a', 'v1'); 37 | INSERT INTO `a` VALUES (3, 'a', 'v1'); 38 | INSERT INTO `a` VALUES (4, 'd', 'v4'); 39 | INSERT INTO `a` VALUES (5, 'e', 'v5'); 40 | INSERT INTO `a` VALUES (6, 'f', 'v6'); 41 | INSERT INTO `a` VALUES (7, 'g', 'v7'); 42 | INSERT INTO `a` VALUES (8, 'h', 'v8'); 43 | INSERT INTO `a` VALUES (9, 'i', 'v9'); 44 | INSERT INTO `a` VALUES (10, 'j', 'v10'); 45 | INSERT INTO `a` VALUES (11, 'k', 'v11'); 46 | INSERT INTO `a` VALUES (12, 'l', 'v12'); 47 | INSERT INTO `a` VALUES (13, 'm', 'v13'); 48 | INSERT INTO `a` VALUES (14, 'n', 'v14'); 49 | INSERT INTO `a` VALUES (15, 'o', 'v15'); 50 | INSERT INTO `a` VALUES (16, 'p', 'v16'); 51 | INSERT INTO `a` VALUES (17, 'q', 'v17'); 52 | INSERT INTO `a` VALUES (18, 'r', 'v18'); 53 | INSERT INTO `a` VALUES (19, 's', 'v19'); 54 | INSERT INTO `a` VALUES (20, 't', 'v20'); 55 | INSERT INTO `a` VALUES (21, 'u', 'v21'); 56 | INSERT INTO `a` VALUES (22, 'v', 'v22'); 57 | INSERT INTO `a` VALUES (23, 'w', 'v23'); 58 | INSERT INTO `a` VALUES (24, 'x', 'v24'); 59 | INSERT INTO `a` VALUES (25, 'y', 'v25'); 60 | INSERT INTO `a` VALUES (26, 'z', 'v26'); 61 | COMMIT; 62 | 63 | -- ---------------------------- 64 | -- Table structure for b 65 | -- ---------------------------- 66 | DROP TABLE IF EXISTS `b`; 67 | CREATE TABLE `b` ( 68 | `id` int(11) NOT NULL AUTO_INCREMENT, 69 | `name` varchar(255) NOT NULL, 70 | `value` varchar(255) DEFAULT NULL, 71 | PRIMARY KEY (`id`) USING BTREE 72 | ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=latin1; 73 | 74 | -- ---------------------------- 75 | -- Records of b 76 | -- ---------------------------- 77 | BEGIN; 78 | INSERT INTO `b` VALUES (1, 'a', 'v1'); 79 | INSERT INTO `b` VALUES (2, 'a', 'v1'); 80 | INSERT INTO `b` VALUES (3, 'a', 'v1'); 81 | INSERT INTO `b` VALUES (4, 'd', 'v4'); 82 | INSERT INTO `b` VALUES (5, 'e', 'v5'); 83 | INSERT INTO `b` VALUES (6, 'f', 'v6'); 84 | INSERT INTO `b` VALUES (7, 'g', 'v7'); 85 | INSERT INTO `b` VALUES (8, 'h', 'v8'); 86 | INSERT INTO `b` VALUES (9, 'i', 'v9'); 87 | INSERT INTO `b` VALUES (10, 'j', 'v10'); 88 | INSERT INTO `b` VALUES (11, 'k', 'v11'); 89 | INSERT INTO `b` VALUES (12, 'l', 'v12'); 90 | INSERT INTO `b` VALUES (13, 'm', 'v13'); 91 | INSERT INTO `b` VALUES (14, 'n', 'v14'); 92 | INSERT INTO `b` VALUES (15, 'o', 'v15'); 93 | INSERT INTO `b` VALUES (16, 'p', 'v16'); 94 | INSERT INTO `b` VALUES (17, 'q', 'v17'); 95 | INSERT INTO `b` VALUES (18, 'r', 'v18'); 96 | INSERT INTO `b` VALUES (19, 's', 'v19'); 97 | INSERT INTO `b` VALUES (20, 't', 'v20'); 98 | INSERT INTO `b` VALUES (21, 'u', 'v21'); 99 | INSERT INTO `b` VALUES (22, 'v', 'v22'); 100 | INSERT INTO `b` VALUES (23, 'w', 'v23'); 101 | INSERT INTO `b` VALUES (24, 'x', 'v24'); 102 | INSERT INTO `b` VALUES (25, 'y', 'v25'); 103 | INSERT INTO `b` VALUES (26, 'z', 'v26'); 104 | COMMIT; 105 | 106 | -- ---------------------------- 107 | -- Table structure for c 108 | -- ---------------------------- 109 | DROP TABLE IF EXISTS `c`; 110 | CREATE TABLE `c` ( 111 | `id` int(11) NOT NULL AUTO_INCREMENT, 112 | `name` varchar(255) NOT NULL, 113 | `value` varchar(255) DEFAULT NULL, 114 | PRIMARY KEY (`id`) USING BTREE 115 | ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=latin1; 116 | 117 | -- ---------------------------- 118 | -- Records of c 119 | -- ---------------------------- 120 | BEGIN; 121 | INSERT INTO `c` VALUES (1, 'a', 'v1'); 122 | INSERT INTO `c` VALUES (2, 'a', 'v1'); 123 | INSERT INTO `c` VALUES (3, 'a', 'v1'); 124 | INSERT INTO `c` VALUES (4, 'd', 'v4'); 125 | INSERT INTO `c` VALUES (5, 'e', 'v5'); 126 | INSERT INTO `c` VALUES (6, 'f', 'v6'); 127 | INSERT INTO `c` VALUES (7, 'g', 'v7'); 128 | INSERT INTO `c` VALUES (8, 'h', 'v8'); 129 | INSERT INTO `c` VALUES (9, 'i', 'v9'); 130 | INSERT INTO `c` VALUES (10, 'j', 'v10'); 131 | INSERT INTO `c` VALUES (11, 'k', 'v11'); 132 | INSERT INTO `c` VALUES (12, 'l', 'v12'); 133 | INSERT INTO `c` VALUES (13, 'm', 'v13'); 134 | INSERT INTO `c` VALUES (14, 'n', 'v14'); 135 | INSERT INTO `c` VALUES (15, 'o', 'v15'); 136 | INSERT INTO `c` VALUES (16, 'p', 'v16'); 137 | INSERT INTO `c` VALUES (17, 'q', 'v17'); 138 | INSERT INTO `c` VALUES (18, 'r', 'v18'); 139 | INSERT INTO `c` VALUES (19, 's', 'v19'); 140 | INSERT INTO `c` VALUES (20, 't', 'v20'); 141 | INSERT INTO `c` VALUES (21, 'u', 'v21'); 142 | INSERT INTO `c` VALUES (22, 'v', 'v22'); 143 | INSERT INTO `c` VALUES (23, 'w', 'v23'); 144 | INSERT INTO `c` VALUES (24, 'x', 'v24'); 145 | INSERT INTO `c` VALUES (25, 'y', 'v25'); 146 | INSERT INTO `c` VALUES (26, 'z', 'v26'); 147 | COMMIT; 148 | 149 | SET FOREIGN_KEY_CHECKS = 1; 150 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/interceptors/PaginatorExecuteHandler.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.interceptors; 2 | 3 | import com.xiwh.paginator.Paginator; 4 | import com.xiwh.paginator.cache.MybatisMethodCache; 5 | import com.xiwh.paginator.sqlGenerator.DataBaseType; 6 | import com.xiwh.paginator.wrappers.NPlusOnePageWrapper; 7 | import com.xiwh.paginator.wrappers.NormalPageWrapper; 8 | import com.xiwh.paginator.wrappers.PageParamsWrapper; 9 | import com.xiwh.paginator.wrappers.PagingRowBounds; 10 | import org.apache.ibatis.cache.CacheKey; 11 | import org.apache.ibatis.executor.Executor; 12 | import org.apache.ibatis.mapping.BoundSql; 13 | import org.apache.ibatis.mapping.MappedStatement; 14 | import org.apache.ibatis.mapping.ResultMap; 15 | import org.apache.ibatis.mapping.SqlCommandType; 16 | import org.apache.ibatis.plugin.*; 17 | import org.apache.ibatis.session.ResultHandler; 18 | import org.apache.ibatis.session.RowBounds; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.beans.factory.annotation.Qualifier; 21 | import org.springframework.stereotype.Component; 22 | 23 | import java.lang.reflect.Field; 24 | import java.util.List; 25 | 26 | 27 | @Intercepts({ 28 | @Signature( 29 | type = Executor.class, 30 | method = "query", 31 | args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} 32 | ), 33 | @Signature( 34 | type = Executor.class, 35 | method = "query", 36 | args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), 37 | }) 38 | @Component 39 | public class PaginatorExecuteHandler extends BaseInterceptor { 40 | 41 | @Autowired 42 | @Qualifier("mybatisMethodCacheImpl") 43 | MybatisMethodCache methodCache; 44 | 45 | 46 | @Override 47 | public Object intercept(Invocation invocation) throws Throwable { 48 | MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; 49 | removePagingInfoWrapper(); 50 | if (mappedStatement.getSqlCommandType() != SqlCommandType.SELECT) 51 | return invocation.proceed(); 52 | MybatisMethodCache.MethodInfo methodInfo = methodCache.getValidMethod(mappedStatement); 53 | if (methodInfo == null) { 54 | return invocation.proceed(); 55 | } 56 | 57 | RowBounds rowBounds = (RowBounds) invocation.getArgs()[2]; 58 | PageParamsWrapper pageParams = null; 59 | PagingRowBounds pagingRowBounds = null; 60 | // When no RowBounds parameter is passed in 61 | if (rowBounds.getOffset() == RowBounds.NO_ROW_OFFSET && rowBounds.getLimit() == RowBounds.NO_ROW_LIMIT) { 62 | if (methodInfo.getPaginatorType() == MybatisMethodCache.TYPE_NORMAL) { 63 | if (methodInfo.getNormalPaginator().auto()) { 64 | Paginator.autoInjectFromRequest(); 65 | } 66 | pageParams = Paginator.readPageParams(); 67 | if (pageParams == null) { 68 | throw new RuntimeException("Please actively set paging parameters!"); 69 | } 70 | pagingRowBounds = new PagingRowBounds( 71 | pageParams, 72 | methodInfo.getNormalPaginator().startOffset() 73 | ); 74 | } else if (methodInfo.getPaginatorType() == MybatisMethodCache.TYPE_N_PLUS_ONE) { 75 | if (methodInfo.getNPlusOnePaginator().auto()) { 76 | Paginator.autoInjectFromRequest(); 77 | } 78 | pageParams = Paginator.readPageParams(); 79 | if (pageParams == null) { 80 | throw new RuntimeException("Please actively set paging parameters!"); 81 | } 82 | pagingRowBounds = new PagingRowBounds( 83 | pageParams, 84 | methodInfo.getNPlusOnePaginator().startOffset() 85 | ); 86 | } 87 | } else { 88 | if (rowBounds instanceof PagingRowBounds) { 89 | pagingRowBounds = (PagingRowBounds) rowBounds; 90 | } else { 91 | pagingRowBounds = new PagingRowBounds(rowBounds); 92 | } 93 | } 94 | 95 | //Modify rowBounds, For compatibility with mybatis caching mechanism 96 | //Compatible with mybatis logical paging 97 | invocation.getArgs()[2] = createFixedLimitRowBounds(pagingRowBounds.getLimit()); 98 | PagingInfoWrapper pagingInfoWrapper = createPaging(); 99 | pagingInfoWrapper.methodInfo = methodInfo; 100 | pagingInfoWrapper.mappedStatement = (MappedStatement) invocation.getArgs()[0]; 101 | pagingInfoWrapper.executor = (Executor) invocation.getTarget(); 102 | pagingInfoWrapper.pagingRowBounds = pagingRowBounds; 103 | 104 | //Get database type 105 | DataBaseType dbType = getDataBaseType(mappedStatement); 106 | // DataBaseType dbType = DataBaseType.MYSQL; 107 | pagingInfoWrapper.dataBaseType = dbType; 108 | 109 | List resultMaps = mappedStatement.getResultMaps(); 110 | ResultMap resultMap = resultMaps.get(0); 111 | if( 112 | NormalPageWrapper.class.isAssignableFrom(resultMap.getType()) || 113 | NPlusOnePageWrapper.class.isAssignableFrom(resultMap.getType())){ 114 | Field field = ResultMap.class.getDeclaredField("type"); 115 | field.setAccessible(true); 116 | field.set(resultMap, methodInfo.getGenericReturnClass()); 117 | } 118 | 119 | 120 | Object proceed = invocation.proceed(); 121 | return proceed; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/cache/impl/CountResultCacheImpl.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.cache.impl; 2 | 3 | import com.xiwh.paginator.cache.CountResultCache; 4 | import org.apache.ibatis.cache.CacheKey; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | import java.util.*; 11 | import java.util.concurrent.Callable; 12 | import java.util.concurrent.locks.Lock; 13 | import java.util.concurrent.locks.ReentrantReadWriteLock; 14 | 15 | @Component("countResultCacheImpl") 16 | public class CountResultCacheImpl implements CountResultCache { 17 | 18 | private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 19 | private static Lock r = lock.readLock(); 20 | private static Lock w = lock.writeLock(); 21 | private Map cacheMap = new HashMap<>(); 22 | private PriorityQueue cacheQueue; 23 | 24 | @PostConstruct 25 | private void init(){ 26 | cacheQueue = new PriorityQueue<>(); 27 | } 28 | 29 | @Override 30 | public int size(){ 31 | return cacheMap.size(); 32 | } 33 | 34 | @Override 35 | public void startClear(){ 36 | w.lock(); 37 | long time = System.currentTimeMillis(); 38 | try { 39 | //Clear expired cache 40 | CacheInfo oldCacheInfo = cacheQueue.peek(); 41 | while (oldCacheInfo != null) { 42 | if (time >= oldCacheInfo.expireTime) { 43 | cacheMap.remove(oldCacheInfo.cacheKey); 44 | cacheQueue.poll(); 45 | oldCacheInfo = cacheQueue.peek(); 46 | } else { 47 | break; 48 | } 49 | } 50 | }finally { 51 | w.unlock(); 52 | } 53 | } 54 | 55 | @Override 56 | public int getCachedCount(CacheKey cacheKey){ 57 | r.lock(); 58 | long time = System.currentTimeMillis(); 59 | try { 60 | CacheInfo cacheInfo = cacheMap.get(cacheKey); 61 | //If there is a cache and within the valid time, return directly 62 | if(cacheInfo != null && time < cacheInfo.expireTime){ 63 | return cacheInfo.count; 64 | }else{ 65 | return -1; 66 | } 67 | }finally { 68 | r.unlock(); 69 | } 70 | } 71 | 72 | @Override 73 | public boolean removeCache(CacheKey cacheKey) { 74 | w.lock(); 75 | try{ 76 | cacheMap.remove(cacheKey); 77 | return cacheQueue.remove(cacheKey); 78 | }finally { 79 | w.unlock(); 80 | } 81 | } 82 | 83 | @Override 84 | public int getCacheCount(CacheKey key, Callable countCallable, int expireTime, boolean forceUpdate) throws Exception { 85 | if(forceUpdate){ 86 | w.lock(); 87 | try{ 88 | long time = System.currentTimeMillis(); 89 | int count = countCallable.call(); 90 | CacheInfo cacheInfo = new CacheInfo(count, key, time+expireTime); 91 | CacheInfo oldCache = cacheMap.remove(key); 92 | if(oldCache!=null){ 93 | cacheQueue.remove(oldCache); 94 | } 95 | cacheMap.put(key, cacheInfo); 96 | cacheQueue.offer(cacheInfo); 97 | return count; 98 | }finally { 99 | w.unlock(); 100 | } 101 | } 102 | r.lock(); 103 | long time = System.currentTimeMillis(); 104 | try { 105 | CacheInfo cacheInfo = cacheMap.get(key); 106 | //If there is a cache and within the valid time, return directly 107 | if(cacheInfo != null && time < cacheInfo.expireTime){ 108 | return cacheInfo.count; 109 | } 110 | }finally { 111 | r.unlock(); 112 | } 113 | w.lock(); 114 | time = System.currentTimeMillis(); 115 | try{ 116 | //Double check 117 | CacheInfo cacheInfo = cacheMap.get(key); 118 | if(cacheInfo != null && time >= cacheInfo.expireTime){ 119 | startClear(); 120 | cacheInfo = null; 121 | } 122 | if(cacheInfo != null){ 123 | return cacheInfo.count; 124 | } 125 | 126 | int count = countCallable.call(); 127 | cacheInfo = new CacheInfo(count, key, time+expireTime); 128 | cacheMap.put(key, cacheInfo); 129 | cacheQueue.offer(cacheInfo); 130 | 131 | return count; 132 | }finally { 133 | w.unlock(); 134 | } 135 | } 136 | 137 | public void printCaches(){ 138 | synchronized (this){ 139 | StringBuilder builder = new StringBuilder("{CountCache["+cacheQueue.size()+"]}["); 140 | PriorityQueue queue = new PriorityQueue(this.cacheQueue); 141 | for(int i=0;!queue.isEmpty();i++){ 142 | if(i!=0){ 143 | builder.append(","); 144 | } 145 | CacheInfo cacheInfo = queue.poll(); 146 | builder.append("\r\n\t"); 147 | builder.append(cacheInfo); 148 | } 149 | builder.append("\r\n]"); 150 | System.out.println(builder); 151 | } 152 | } 153 | 154 | private static class CacheInfo implements Comparable{ 155 | private int count; 156 | private CacheKey cacheKey; 157 | private long expireTime; 158 | 159 | public CacheInfo(int count, CacheKey cacheKey, long expireTime){ 160 | this.count = count; 161 | this.cacheKey = cacheKey; 162 | this.expireTime = expireTime; 163 | } 164 | 165 | @Override 166 | public String toString() { 167 | final StringBuffer sb = new StringBuffer("CacheInfo{"); 168 | sb.append("count=").append(count); 169 | sb.append(", cacheKey=").append(cacheKey); 170 | sb.append(", expireTime=").append(expireTime); 171 | sb.append('}'); 172 | return sb.toString(); 173 | } 174 | 175 | @Override 176 | public int compareTo(Object o) { 177 | CacheInfo cacheInfo = (CacheInfo)o; 178 | return (int) (this.expireTime - cacheInfo.expireTime); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/cache/impl/MybatisMethodCacheImpl.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.cache.impl; 2 | 3 | import com.xiwh.paginator.annotations.NPlusOnePaginator; 4 | import com.xiwh.paginator.annotations.NormalPaginator; 5 | import com.xiwh.paginator.cache.MybatisMethodCache; 6 | import com.xiwh.paginator.utils.StringUtils; 7 | import org.apache.ibatis.mapping.MappedStatement; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.lang.annotation.Annotation; 13 | import java.lang.reflect.Method; 14 | import java.lang.reflect.ParameterizedType; 15 | import java.util.Map; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | 18 | @Component("mybatisMethodCacheImpl") 19 | public class MybatisMethodCacheImpl implements MybatisMethodCache { 20 | 21 | private final static MethodInfoImpl INVALID_METHOD = new MethodInfoImpl(null); 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(MybatisMethodCacheImpl.class); 24 | 25 | private Map methodInfoCache = new ConcurrentHashMap<>(); 26 | 27 | public MybatisMethodCache.MethodInfo getValidMethod(MappedStatement mappedStatement) { 28 | String id = mappedStatement.getId(); 29 | MethodInfo methodInfo = methodInfoCache.get(id); 30 | if(methodInfo==null){ 31 | int temp = id.lastIndexOf("."); 32 | String className = id.substring(0, temp); 33 | String methodName = id.substring(temp+1); 34 | Method method = _getMethod(className, methodName); 35 | if(method==null){ 36 | methodInfoCache.put(id, INVALID_METHOD); 37 | }else{ 38 | //Double check 39 | synchronized (method) { 40 | methodInfo = methodInfoCache.get(id); 41 | if(methodInfo==null) { 42 | methodInfo = _collectInfo(className, method); 43 | methodInfoCache.put(id, methodInfo); 44 | } 45 | } 46 | } 47 | } 48 | 49 | if(methodInfo.getMethod()==null){ 50 | return null; 51 | } 52 | return methodInfo; 53 | } 54 | 55 | 56 | private MethodInfo _collectInfo(String className, Method method){ 57 | Annotation[] annotations = method.getAnnotations(); 58 | MethodInfoImpl methodInfo = INVALID_METHOD; 59 | for(int i=0;i 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.3.3.RELEASE 10 | 11 | 12 | 13 | 14 | com.github.xiwh 15 | mybatis-paginator 16 | 1.0.2 17 | springboot-mybatis-paginator 18 | jar 19 | A powerful and easy-to-use mybatis physical paginator 20 | https://github.com/xiwh/springboot-mybatis-paginator 21 | 22 | 23 | 24 | 1.8 25 | 26 | 27 | 28 | scm:git:git://github.com/xiwh/springboot-mybatis-paginator.git 29 | scm:git:ssh://github.com/xiwh/springboot-mybatis-paginator.git 30 | https://github.com/xiwh/springboot-mybatis-paginator 31 | 32 | 33 | 34 | ossrh 35 | 36 | true 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-source-plugin 43 | 2.2.1 44 | 45 | 46 | attach-sources 47 | 48 | jar-no-fork 49 | 50 | 51 | 52 | 53 | 54 | org.apache.maven.plugins 55 | maven-javadoc-plugin 56 | 2.9.1 57 | 58 | 59 | attach-javadocs 60 | 61 | jar 62 | 63 | 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-gpg-plugin 69 | 1.5 70 | 71 | 72 | sign-artifacts 73 | verify 74 | 75 | sign 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ossrh 85 | https://oss.sonatype.org/content/repositories/snapshots 86 | 87 | 88 | ossrh 89 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.mybatis.spring.boot 97 | mybatis-spring-boot-starter 98 | 2.1.3 99 | provided 100 | 101 | 102 | com.alibaba 103 | druid 104 | 1.1.23 105 | 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-starter-web 111 | 2.3.3.RELEASE 112 | true 113 | 114 | 115 | org.springframework.boot 116 | spring-boot-starter-tomcat 117 | 2.3.3.RELEASE 118 | true 119 | 120 | 121 | 122 | 123 | 124 | com.microsoft.sqlserver 125 | mssql-jdbc 126 | test 127 | 128 | 129 | mysql 130 | mysql-connector-java 131 | test 132 | 133 | 134 | org.postgresql 135 | postgresql 136 | test 137 | 138 | 139 | org.springframework.boot 140 | spring-boot-starter-test 141 | test 142 | 143 | 144 | org.junit.vintage 145 | junit-vintage-engine 146 | 147 | 148 | 149 | 150 | junit 151 | junit 152 | 4.12 153 | test 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | maven-compiler-plugin 165 | 2.3.2 166 | 167 | 1.8 168 | 1.8 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/interceptors/BaseInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.interceptors; 2 | 3 | import com.xiwh.paginator.cache.MybatisMethodCache; 4 | import com.xiwh.paginator.sqlGenerator.DataBaseType; 5 | import com.xiwh.paginator.sqlGenerator.PaginatorSqlGenerator; 6 | import com.xiwh.paginator.wrappers.PagingRowBounds; 7 | import org.apache.ibatis.cache.CacheKey; 8 | import org.apache.ibatis.executor.Executor; 9 | import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; 10 | import org.apache.ibatis.executor.statement.StatementHandler; 11 | import org.apache.ibatis.logging.Log; 12 | import org.apache.ibatis.logging.jdbc.ConnectionLogger; 13 | import org.apache.ibatis.mapping.*; 14 | import org.apache.ibatis.plugin.Interceptor; 15 | import org.apache.ibatis.plugin.Plugin; 16 | import org.apache.ibatis.reflection.MetaObject; 17 | import org.apache.ibatis.session.Configuration; 18 | import org.apache.ibatis.session.RowBounds; 19 | import org.apache.ibatis.type.TypeHandlerRegistry; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.sql.*; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Properties; 27 | import java.util.concurrent.ConcurrentHashMap; 28 | 29 | public abstract class BaseInterceptor implements Interceptor { 30 | 31 | private static final ThreadLocal mThreadLocal = new ThreadLocal(); 32 | private static final ThreadLocal mThreadLocal2 = new ThreadLocal(); 33 | private static final Map dbTypeMap = new ConcurrentHashMap(); 34 | 35 | private static final Logger logger = LoggerFactory.getLogger(BaseInterceptor.class); 36 | 37 | public PagingInfoWrapper createPaging(){ 38 | PagingInfoWrapper pagingInfoWrapper = mThreadLocal.get(); 39 | if(pagingInfoWrapper==null){ 40 | pagingInfoWrapper = new PagingInfoWrapper(); 41 | mThreadLocal.set(pagingInfoWrapper); 42 | }else{ 43 | pagingInfoWrapper.reset(); 44 | } 45 | return pagingInfoWrapper; 46 | } 47 | 48 | public PagingInfoWrapper getPagingInfoWrapper(){ 49 | PagingInfoWrapper pagingInfoWrapper = mThreadLocal.get(); 50 | return pagingInfoWrapper; 51 | } 52 | 53 | public void removePagingInfoWrapper(){ 54 | mThreadLocal.remove(); 55 | } 56 | 57 | public DataBaseType getDataBaseType(MappedStatement mappedStatement){ 58 | String id = mappedStatement.getDatabaseId(); 59 | id = id==null?"default":id; 60 | DataBaseType dataBaseType = dbTypeMap.get(id); 61 | if (dataBaseType == null) { 62 | synchronized (dbTypeMap) { 63 | dataBaseType = dbTypeMap.get(id); 64 | if (dataBaseType == null) { 65 | try { 66 | dataBaseType = DataBaseType.findByURL( 67 | getConnection().getMetaData().getURL() 68 | ); 69 | dbTypeMap.put(id, dataBaseType); 70 | } catch (Exception e) { 71 | throw new RuntimeException("Failed to get database type!", e); 72 | } 73 | } 74 | } 75 | } 76 | return dataBaseType; 77 | } 78 | 79 | /** 80 | * Compatible with mybatis logical paging 81 | */ 82 | public RowBounds createFixedLimitRowBounds(int limit){ 83 | FixedLimitRowBounds fixedLimitRowBounds = mThreadLocal2.get(); 84 | if(fixedLimitRowBounds ==null){ 85 | fixedLimitRowBounds = new FixedLimitRowBounds(limit+1); 86 | mThreadLocal2.set(fixedLimitRowBounds); 87 | }else{ 88 | fixedLimitRowBounds.setLimit(limit+1); 89 | } 90 | return fixedLimitRowBounds; 91 | } 92 | 93 | private static class FixedLimitRowBounds extends RowBounds{ 94 | private int limit; 95 | 96 | private FixedLimitRowBounds(int limit){ 97 | this.limit = limit; 98 | } 99 | 100 | public void setLimit(int limit) { 101 | this.limit = limit; 102 | } 103 | 104 | @Override 105 | public int getLimit() { 106 | return limit; 107 | } 108 | 109 | @Override 110 | public int getOffset() { 111 | return 0; 112 | } 113 | } 114 | 115 | public static class PagingInfoWrapper{ 116 | MybatisMethodCache.MethodInfo methodInfo; 117 | PagingRowBounds pagingRowBounds; 118 | PaginatorSqlGenerator sqlGenerator; 119 | int count; 120 | MappedStatement mappedStatement; 121 | Executor executor; 122 | DataBaseType dataBaseType; 123 | BoundSql boundSql; 124 | StatementHandler statementHandler; 125 | boolean counting = false; 126 | 127 | public void reset(){ 128 | methodInfo = null; 129 | pagingRowBounds = null; 130 | sqlGenerator = null; 131 | count = 0; 132 | mappedStatement = null; 133 | executor = null; 134 | dataBaseType = null; 135 | boundSql = null; 136 | statementHandler = null; 137 | counting = false; 138 | } 139 | } 140 | 141 | protected Connection getConnection() throws SQLException { 142 | PagingInfoWrapper pagingInfoWrapper = getPagingInfoWrapper(); 143 | Log statementLog = pagingInfoWrapper.mappedStatement.getStatementLog(); 144 | Connection connection = pagingInfoWrapper.executor.getTransaction().getConnection(); 145 | if (statementLog.isDebugEnabled()) { 146 | return ConnectionLogger.newInstance(connection, statementLog, 0); 147 | } else { 148 | return connection; 149 | } 150 | } 151 | 152 | public PreparedStatement createCountStatement(boolean optimization) throws SQLException 153 | { 154 | PagingInfoWrapper pagingInfoWrapper = getPagingInfoWrapper(); 155 | String sql = optimization ? 156 | pagingInfoWrapper.sqlGenerator.toOptimizedCountSql() 157 | : pagingInfoWrapper.sqlGenerator.toCountSql(); 158 | 159 | if(logger.isDebugEnabled()) { 160 | logger.debug(String.format("Custom count preparing: %s", sql)); 161 | } 162 | 163 | Connection connection = getConnection(); 164 | if (pagingInfoWrapper.mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { 165 | String[] keyColumnNames = pagingInfoWrapper.mappedStatement.getKeyColumns(); 166 | if (keyColumnNames == null) { 167 | return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); 168 | } else { 169 | return connection.prepareStatement(sql, keyColumnNames); 170 | } 171 | } else if (pagingInfoWrapper.mappedStatement.getResultSetType() == ResultSetType.DEFAULT) { 172 | return connection.prepareStatement(sql); 173 | } else { 174 | return connection.prepareStatement( 175 | sql, 176 | pagingInfoWrapper.mappedStatement.getResultSetType().getValue(), 177 | ResultSet.CONCUR_READ_ONLY 178 | ); 179 | } 180 | } 181 | 182 | public CacheKey createCountCacheKey(){ 183 | PagingInfoWrapper pagingInfoWrapper = getPagingInfoWrapper(); 184 | CacheKey cacheKey = new CacheKey(); 185 | cacheKey.update(pagingInfoWrapper.mappedStatement.getId()); 186 | cacheKey.update(pagingInfoWrapper.boundSql.getSql()); 187 | List parameterMappings = pagingInfoWrapper.boundSql.getParameterMappings(); 188 | TypeHandlerRegistry typeHandlerRegistry = pagingInfoWrapper.mappedStatement.getConfiguration().getTypeHandlerRegistry(); 189 | Object parameterObject = pagingInfoWrapper.boundSql.getParameterObject(); 190 | Configuration configuration = pagingInfoWrapper.mappedStatement.getConfiguration(); 191 | // mimic DefaultParameterHandler logic 192 | for (ParameterMapping parameterMapping : parameterMappings) { 193 | if (parameterMapping.getMode() != ParameterMode.OUT) { 194 | Object value; 195 | String propertyName = parameterMapping.getProperty(); 196 | if (pagingInfoWrapper.boundSql.hasAdditionalParameter(propertyName)) { 197 | value = pagingInfoWrapper.boundSql.getAdditionalParameter(propertyName); 198 | } else if (parameterObject == null) { 199 | value = null; 200 | } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { 201 | value = parameterObject; 202 | } else { 203 | MetaObject metaObject = configuration.newMetaObject(parameterObject); 204 | value = metaObject.getValue(propertyName); 205 | } 206 | cacheKey.update(value); 207 | } 208 | } 209 | return cacheKey; 210 | } 211 | 212 | @Override 213 | public Object plugin(Object target) { 214 | return Plugin.wrap(target, this); 215 | } 216 | 217 | @Override 218 | public void setProperties(Properties properties) { 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/interceptors/PaginatorResultHandler.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.interceptors; 2 | 3 | import com.xiwh.paginator.Paginator; 4 | import com.xiwh.paginator.annotations.NormalPaginator; 5 | import com.xiwh.paginator.cache.CountResultCache; 6 | import com.xiwh.paginator.cache.MybatisMethodCache; 7 | import com.xiwh.paginator.cache.impl.CountResultCacheImpl; 8 | import com.xiwh.paginator.cache.impl.MybatisMethodCacheImpl; 9 | import com.xiwh.paginator.wrappers.NPlusOnePage; 10 | import com.xiwh.paginator.wrappers.NPlusOnePageWrapper; 11 | import com.xiwh.paginator.wrappers.NormalPageWrapper; 12 | import com.xiwh.paginator.sqlGenerator.PaginatorSqlGenerator; 13 | import com.xiwh.paginator.utils.StringUtils; 14 | import com.xiwh.paginator.wrappers.PagingRowBounds; 15 | import org.apache.ibatis.cache.CacheKey; 16 | import org.apache.ibatis.executor.Executor; 17 | import org.apache.ibatis.executor.resultset.ResultSetHandler; 18 | import org.apache.ibatis.executor.statement.StatementHandler; 19 | import org.apache.ibatis.mapping.BoundSql; 20 | import org.apache.ibatis.mapping.MappedStatement; 21 | import org.apache.ibatis.plugin.*; 22 | import org.apache.ibatis.session.RowBounds; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.beans.factory.annotation.Qualifier; 27 | import org.springframework.stereotype.Component; 28 | 29 | import java.sql.*; 30 | import java.util.ArrayList; 31 | import java.util.Iterator; 32 | import java.util.List; 33 | import java.util.concurrent.Callable; 34 | 35 | 36 | @Intercepts({ 37 | @Signature( 38 | type = ResultSetHandler.class, 39 | method = "handleResultSets", 40 | args = {Statement.class} 41 | ) 42 | }) 43 | @Component 44 | public class PaginatorResultHandler extends BaseInterceptor { 45 | 46 | private static final Logger logger = LoggerFactory.getLogger(PaginatorLimitHandler.class); 47 | 48 | @Autowired 49 | @Qualifier("mybatisMethodCacheImpl") 50 | MybatisMethodCache methodCache; 51 | @Autowired 52 | @Qualifier("countResultCacheImpl") 53 | CountResultCache countResultCache; 54 | 55 | private ThreadLocal listThreadLocal = new ThreadLocal<>(); 56 | 57 | @Override 58 | public Object intercept(Invocation invocation) throws Throwable { 59 | PagingInfoWrapper pagingInfoWrapper = getPagingInfoWrapper(); 60 | if(pagingInfoWrapper==null){ 61 | return invocation.proceed(); 62 | }else if(pagingInfoWrapper.counting){ 63 | return invocation.proceed(); 64 | } 65 | 66 | BoundSql boundSql = pagingInfoWrapper.boundSql; 67 | StatementHandler statementHandler = pagingInfoWrapper.statementHandler; 68 | MybatisMethodCache.MethodInfo methodInfo = pagingInfoWrapper.methodInfo; 69 | List result = (List) invocation.proceed(); 70 | PagingRowBounds pagingRowBounds = pagingInfoWrapper.pagingRowBounds; 71 | if(methodInfo.getPaginatorType() == MybatisMethodCache.TYPE_NORMAL) { 72 | NormalPaginator normalPaginator = methodInfo.getNormalPaginator(); 73 | // The count result can be calculated on the last page 74 | // If the count result can be calculated directly, it is forced to update 75 | boolean canGetCountResult = result.size()!=0 && result.size() < pagingRowBounds.getLimit(); 76 | Callable countCall = () -> { 77 | // The count result can be calculated on the last page 78 | if(canGetCountResult){ 79 | int count = pagingRowBounds.getOffset() + result.size(); 80 | if(logger.isDebugEnabled()) { 81 | logger.debug(String.format("Enable last page optimization")); 82 | } 83 | return count; 84 | } 85 | String customCountMapperId = pagingInfoWrapper.methodInfo.getCustomCountMapperId(); 86 | pagingInfoWrapper.counting = true; 87 | try { 88 | //Auto count 89 | if (StringUtils.isEmpty(customCountMapperId)) { 90 | PreparedStatement preparedStatement = createCountStatement(methodInfo.getNormalPaginator().countOptimization()); 91 | statementHandler.parameterize(preparedStatement); 92 | ResultSet executeQuery = preparedStatement.executeQuery(); 93 | executeQuery.next(); 94 | int tempCount = executeQuery.getInt(PaginatorSqlGenerator.COUNT_ALIAS); 95 | executeQuery.close(); 96 | preparedStatement.close(); 97 | return tempCount; 98 | } 99 | //Custom Count statement 100 | else { 101 | Executor executor = pagingInfoWrapper.executor; 102 | MappedStatement customCountMappedStatement = pagingInfoWrapper.mappedStatement 103 | .getConfiguration().getMappedStatement( 104 | customCountMapperId, false 105 | ); 106 | 107 | if(logger.isDebugEnabled()) { 108 | logger.debug(String.format("Count preparing:: %s", boundSql)); 109 | } 110 | List results = executor.query( 111 | customCountMappedStatement, 112 | boundSql.getParameterObject(), 113 | RowBounds.DEFAULT, 114 | Executor.NO_RESULT_HANDLER 115 | ); 116 | return results.get(0); 117 | } 118 | }finally { 119 | pagingInfoWrapper.counting = false; 120 | } 121 | }; 122 | if(methodInfo.getNormalPaginator().cache()) { 123 | //Create count cache key 124 | CacheKey cacheKey = createCountCacheKey(); 125 | 126 | //Get count result 127 | int count = countResultCache.getCacheCount( 128 | cacheKey, 129 | countCall, 130 | pagingInfoWrapper.methodInfo.getNormalPaginator().cacheExpiryTime() * 1000, 131 | canGetCountResult || pagingRowBounds.isForceCounting() 132 | ); 133 | pagingInfoWrapper.count = count; 134 | }else{ 135 | pagingInfoWrapper.count = countCall.call(); 136 | } 137 | 138 | if(logger.isInfoEnabled()) { 139 | logger.info(String.format("Paging count: %s", pagingInfoWrapper.count)); 140 | } 141 | 142 | pagingRowBounds.setCount(pagingInfoWrapper.count); 143 | pagingRowBounds.setHasNext(pagingRowBounds.getCount()>pagingRowBounds.getEnd()); 144 | 145 | Class returnClass = methodInfo.getReturnClass(); 146 | if(NormalPageWrapper.class.isAssignableFrom(returnClass)) { 147 | NormalPageWrapper pageWrapper = (NormalPageWrapper) returnClass.newInstance(); 148 | pageWrapper.init( 149 | result, 150 | pagingInfoWrapper.count, 151 | methodInfo.getNormalPaginator().startOffset(), 152 | pagingRowBounds.getPage(), 153 | pagingRowBounds.getLimit() 154 | ); 155 | ArrayList arrayList = getArrayList(); 156 | arrayList.add(pageWrapper); 157 | return arrayList; 158 | }else{ 159 | return result; 160 | } 161 | }else if(methodInfo.getPaginatorType() == MybatisMethodCache.TYPE_N_PLUS_ONE){ 162 | boolean hasNext = result.size() > pagingRowBounds.getLimit(); 163 | if (hasNext) { 164 | result.remove(result.size() - 1); 165 | } 166 | pagingRowBounds.setHasNext(hasNext); 167 | Class returnClass = methodInfo.getReturnClass(); 168 | if(NPlusOnePageWrapper.class.isAssignableFrom(returnClass)) { 169 | NPlusOnePage pageWrapper = (NPlusOnePage) methodInfo.getReturnClass().newInstance(); 170 | pageWrapper.init( 171 | result, 172 | hasNext, 173 | pagingRowBounds.getPageOffset(), 174 | pagingRowBounds.getPage(), pagingRowBounds.getLimit() 175 | ); 176 | ArrayList arrayList = getArrayList(); 177 | arrayList.add(pageWrapper); 178 | return arrayList; 179 | }else{ 180 | return result; 181 | } 182 | }else{ 183 | return result; 184 | } 185 | } 186 | 187 | private ArrayList getArrayList(){ 188 | ArrayList arrayList = listThreadLocal.get(); 189 | if(arrayList==null){ 190 | arrayList = new ArrayList(1); 191 | listThreadLocal.set(arrayList); 192 | }else{ 193 | arrayList.clear(); 194 | } 195 | return arrayList; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/com/xiwh/paginator/sqlGenerator/PaginatorSqlGenerator.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.sqlGenerator; 2 | 3 | import com.alibaba.druid.sql.SQLUtils; 4 | import com.alibaba.druid.sql.ast.SQLExpr; 5 | import com.alibaba.druid.sql.ast.SQLObject; 6 | import com.alibaba.druid.sql.ast.SQLStatement; 7 | import com.alibaba.druid.sql.ast.expr.*; 8 | import com.alibaba.druid.sql.ast.statement.*; 9 | import com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser; 10 | import com.alibaba.druid.sql.dialect.oracle.parser.OracleStatementParser; 11 | import com.alibaba.druid.sql.dialect.postgresql.parser.PGSQLStatementParser; 12 | import com.alibaba.druid.sql.dialect.sqlserver.parser.SQLServerStatementParser; 13 | import com.alibaba.druid.sql.parser.SQLStatementParser; 14 | import com.alibaba.druid.sql.visitor.SQLASTOutputVisitor; 15 | import com.xiwh.paginator.utils.StringUtils; 16 | 17 | import java.util.Iterator; 18 | import java.util.List; 19 | 20 | public class PaginatorSqlGenerator { 21 | public static final String COUNT_ALIAS = "p_count"; 22 | public static final String TEMP_VAR_ALIAS = "p_temp_v"; 23 | public static final String TEMP_TABLE_ALIAS = "p_temp_t"; 24 | public static final String TEMP_TABLE2_ALIAS = "p_temp_t2"; 25 | public static final String TEMP_ROWNUM_ALIAS = "p_temp_rn"; 26 | 27 | 28 | SQLSelect rawSelect; 29 | DataBaseType dataBaseType; 30 | 31 | /** 32 | * @param sql query SQL 33 | * @param dataBaseType 34 | */ 35 | public PaginatorSqlGenerator(String sql, DataBaseType dataBaseType){ 36 | rawSelect = parserSelectSql(sql, dataBaseType); 37 | this.dataBaseType = dataBaseType; 38 | } 39 | 40 | /** 41 | * Analysis of SQL structure 42 | * @param sql 43 | * @param dataBaseType 44 | * @return 45 | */ 46 | private static SQLSelect parserSelectSql(String sql, DataBaseType dataBaseType){ 47 | SQLStatementParser sqlStatementParser = null; 48 | switch (dataBaseType){ 49 | case MYSQL: 50 | sqlStatementParser = new MySqlStatementParser(sql); 51 | break; 52 | case ORACLE: 53 | sqlStatementParser = new OracleStatementParser(sql); 54 | break; 55 | case POSGRESQL: 56 | sqlStatementParser = new PGSQLStatementParser(sql); 57 | break; 58 | case SQLSERVER: 59 | sqlStatementParser = new SQLServerStatementParser(sql); 60 | break; 61 | } 62 | List statementList = sqlStatementParser.parseStatementList(); 63 | SQLStatement sqlStatement = statementList.get(0); 64 | List sqls = sqlStatement.getChildren(); 65 | SQLSelect sqlSelect = (SQLSelect) sqls.get(0); 66 | return sqlSelect; 67 | } 68 | 69 | public String toLimitSql(int offset, int limit){ 70 | SQLSelect sqlSelect = rawSelect.clone(); 71 | SQLSelectQuery selectQuery = sqlSelect.getQuery(); 72 | SQLSelectQueryBlock selectQueryBlock = (SQLSelectQueryBlock) selectQuery; 73 | // Fuckkkkkkkkk oracle 74 | if(dataBaseType == DataBaseType.ORACLE){ 75 | return String.format( 76 | "select * from ( select rownum %s,%s.* from ( %s ) %s where rownum <= %s ) %s where %s.%s > %s", 77 | TEMP_ROWNUM_ALIAS, 78 | TEMP_TABLE_ALIAS, 79 | toSQLString(sqlSelect, dataBaseType), 80 | TEMP_TABLE_ALIAS, 81 | offset+limit, 82 | TEMP_TABLE2_ALIAS, 83 | TEMP_TABLE2_ALIAS, 84 | TEMP_ROWNUM_ALIAS, 85 | offset 86 | ); 87 | }else if(dataBaseType == DataBaseType.SQLSERVER){ 88 | String sql = toSQLString(sqlSelect, dataBaseType); 89 | return String.format("%s OFFSET %s ROWS FETCH NEXT %s ROWS ONLY", sql, offset, limit); 90 | }else { 91 | selectQueryBlock.limit(limit, offset); 92 | return toSQLString(sqlSelect, dataBaseType); 93 | } 94 | } 95 | 96 | 97 | /** 98 | * Convert query SQL to count SQL 99 | * @return counted SQL 100 | */ 101 | public String toCountSql(){ 102 | SQLSelect sqlSelect = rawSelect.clone(); 103 | SQLSelectQuery selectQuery = sqlSelect.getQuery(); 104 | SQLSelectQueryBlock selectQueryBlock = (SQLSelectQueryBlock) selectQuery; 105 | //Remove order by 106 | selectQueryBlock.setOrderBy(null); 107 | SQLSelectGroupByClause groupByClause = selectQueryBlock.getGroupBy(); 108 | //Group by needs to be wrapped 109 | if(groupByClause!=null&&!groupByClause.getItems().isEmpty()){ 110 | //Replace all select with count 111 | List selectItems = selectQueryBlock.getSelectList(); 112 | selectItems.clear(); 113 | selectItems.add(new SQLSelectItem(new SQLIntegerExpr(1),TEMP_VAR_ALIAS)); 114 | //Wrapping group SQL 115 | return String.format( 116 | "SELECT count(%s.%s) as %s FROM (%s) as %s", 117 | TEMP_TABLE_ALIAS, 118 | TEMP_VAR_ALIAS, 119 | COUNT_ALIAS, 120 | toSQLString(sqlSelect, dataBaseType), 121 | TEMP_TABLE_ALIAS 122 | ); 123 | } 124 | else{ 125 | //Replace all select with count 126 | List selectItems = selectQueryBlock.getSelectList(); 127 | selectItems.clear(); 128 | selectItems.add(new SQLSelectItem(new SQLAggregateExpr("count", null, new SQLIntegerExpr(1)),COUNT_ALIAS)); 129 | return toSQLString(sqlSelect, dataBaseType); 130 | } 131 | } 132 | 133 | /** 134 | * Convert query SQL to optimized count SQL 135 | * What are the optimization cases: 136 | * 1.Clean invalid order by 137 | * 2.Clean invalid select 138 | * @return Optimized count SQL 139 | */ 140 | public String toOptimizedCountSql(){ 141 | SQLSelect sqlSelect = rawSelect.clone(); 142 | SQLSelectQuery selectQuery = sqlSelect.getQuery(); 143 | SQLSelectQueryBlock selectQueryBlock = (SQLSelectQueryBlock) selectQuery; 144 | String havingStr = null; 145 | //Remove order by 146 | selectQueryBlock.setOrderBy(null); 147 | List selectItems = selectQueryBlock.getSelectList(); 148 | SQLSelectGroupByClause groupByClause = selectQueryBlock.getGroupBy(); 149 | boolean needWrapUp = false; 150 | //Has groupBy or having 151 | if(groupByClause!=null){ 152 | SQLExpr having = groupByClause.getHaving(); 153 | if(!groupByClause.getItems().isEmpty()) { 154 | needWrapUp = true; 155 | } 156 | // If there is "having", "select" will be cleaned up based on the "having" reference 157 | if(having!=null){ 158 | havingStr = having.toString(); 159 | Iterator iterator = selectItems.iterator(); 160 | while (iterator.hasNext()) { 161 | SQLSelectItem item = iterator.next(); 162 | // Get field name 163 | String fieldName = item.getAlias(); 164 | if(StringUtils.isEmpty(fieldName)){ 165 | String fieldBody = item.toString(); 166 | int dotIndex = fieldBody.lastIndexOf('.'); 167 | if(dotIndex==-1){ 168 | fieldName = fieldBody; 169 | }else{ 170 | fieldName = fieldBody.substring(dotIndex+1); 171 | } 172 | } 173 | if (havingStr.contains(fieldName)) { 174 | continue; 175 | } 176 | iterator.remove(); 177 | } 178 | selectItems.add(new SQLSelectItem(new SQLIntegerExpr(1),TEMP_VAR_ALIAS)); 179 | } 180 | //Replace the group without having with count(distinct xxx) 181 | else{ 182 | selectItems.clear(); 183 | selectItems.add( 184 | new SQLSelectItem( 185 | new SQLIntegerExpr(1),TEMP_VAR_ALIAS 186 | ) 187 | ); 188 | selectQueryBlock.setGroupBy(null); 189 | } 190 | }else{ 191 | // When there is no "having" or "group", directly replace "select" 192 | selectItems.clear(); 193 | selectItems.add(new SQLSelectItem(new SQLAggregateExpr("count", null, new SQLIntegerExpr(1)),COUNT_ALIAS)); 194 | } 195 | String newSql = toSQLString(sqlSelect, dataBaseType); 196 | //Wrapping group SQL 197 | if(needWrapUp){ 198 | return String.format( 199 | "SELECT count(%s.%s) as %s FROM (%s) as %s", 200 | TEMP_TABLE_ALIAS, 201 | TEMP_VAR_ALIAS, 202 | COUNT_ALIAS, 203 | newSql, 204 | TEMP_TABLE_ALIAS 205 | ); 206 | }else{ 207 | return newSql; 208 | } 209 | } 210 | 211 | public static String toSQLString(SQLObject sqlObject, DataBaseType dataBaseType) { 212 | String sql = sqlObject.toString(); 213 | return sql.replaceAll("[\\t\\n\\r]"," "); 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/test/java/com/xiwh/paginator/demo/cache/CountCacheTests.java: -------------------------------------------------------------------------------- 1 | package com.xiwh.paginator.demo.cache; 2 | 3 | import com.xiwh.paginator.TestApplication; 4 | import com.xiwh.paginator.cache.CountResultCache; 5 | import com.xiwh.paginator.cache.impl.CountResultCacheImpl; 6 | import org.apache.ibatis.cache.CacheKey; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.test.context.ContextConfiguration; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | import java.util.concurrent.Callable; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | @SpringBootTest 18 | @RunWith(SpringRunner.class) 19 | @ContextConfiguration(classes = TestApplication.class) 20 | class CountCacheTests { 21 | 22 | @Autowired 23 | CountResultCacheImpl cache; 24 | 25 | @Test 26 | void cacheTest() throws Exception { 27 | int count = cache.getCacheCount(new CacheKey(new Object[]{"1"}), new Callable() { 28 | 29 | @Override 30 | public Integer call() throws Exception { 31 | return 1; 32 | } 33 | },1000, false); 34 | System.out.println("count1:"+count); 35 | 36 | count = cache.getCacheCount(new CacheKey(new Object[]{"1"}), new Callable() { 37 | 38 | @Override 39 | public Integer call() throws Exception { 40 | return 11; 41 | } 42 | },1000, false); 43 | System.out.println("count2:"+count); 44 | Thread.sleep(2000); 45 | count = cache.getCacheCount(new CacheKey(new Object[]{"1"}), new Callable() { 46 | 47 | @Override 48 | public Integer call() throws Exception { 49 | return 111; 50 | } 51 | },2000, false); 52 | System.out.println("count3:"+count); 53 | Thread.sleep(2100); 54 | count = cache.getCacheCount(new CacheKey(new Object[]{"1"}), new Callable() { 55 | 56 | @Override 57 | public Integer call() throws Exception { 58 | return 1111; 59 | } 60 | },2000, false); 61 | System.out.println("count4:"+count); 62 | } 63 | 64 | @Test 65 | void cacheTest2() throws Exception { 66 | int countA = cache.getCacheCount(new CacheKey(new Object[]{"a"}), new Callable() { 67 | 68 | @Override 69 | public Integer call() throws Exception { 70 | return 1; 71 | } 72 | },1000, false); 73 | 74 | int countB = cache.getCacheCount(new CacheKey(new Object[]{"b"}), new Callable() { 75 | 76 | @Override 77 | public Integer call() throws Exception { 78 | return 2; 79 | } 80 | },5000, false); 81 | 82 | int countC = cache.getCacheCount(new CacheKey(new Object[]{"c"}), new Callable() { 83 | 84 | @Override 85 | public Integer call() throws Exception { 86 | return 3; 87 | } 88 | },2999,false); 89 | 90 | int countD = cache.getCacheCount(new CacheKey(new Object[]{"d"}), new Callable() { 91 | 92 | @Override 93 | public Integer call() throws Exception { 94 | return 4; 95 | } 96 | },9999,false); 97 | 98 | cache.printCaches(); 99 | } 100 | 101 | private Integer finishedThreads = 0; 102 | 103 | @Test 104 | void cacheTest3() throws Exception { 105 | 106 | for(int i=0;i<100;i++){ 107 | Thread thread = new Thread(new Runnable() { 108 | @Override 109 | public void run() { 110 | try { 111 | for(int i = 0;i<10;i++) { 112 | final int temp= i; 113 | int countA = cache.getCacheCount(new CacheKey(new Object[]{"i-"+i}), new Callable() { 114 | 115 | @Override 116 | public Integer call() throws Exception { 117 | System.out.println("i-"+temp+" Executed,thread:"+Thread.currentThread().getName()); 118 | return temp; 119 | } 120 | }, (temp+1)*1000, false); 121 | } 122 | synchronized (finishedThreads){ 123 | finishedThreads++; 124 | if(finishedThreads==10){ 125 | System.out.println("Finished:"); 126 | cache.printCaches(); 127 | } 128 | } 129 | } catch (Exception e) { 130 | e.printStackTrace(); 131 | } 132 | } 133 | }); 134 | thread.setName("thread-"+i); 135 | thread.start(); 136 | } 137 | 138 | try{ 139 | Thread.sleep(100); 140 | for(int j=0;j<10;j++){ 141 | Thread.sleep(1000); 142 | cache.getCacheCount(new CacheKey(new Object[]{"i-"+j}), new Callable() { 143 | 144 | @Override 145 | public Integer call() throws Exception { 146 | return -1; 147 | } 148 | }, 1000, false); 149 | System.out.println((j+1)+" second later:"); 150 | cache.printCaches(); 151 | 152 | } 153 | }catch (Exception e){ 154 | e.printStackTrace(); 155 | } 156 | 157 | } 158 | 159 | @Test 160 | void cacheTest4() throws Exception { 161 | 162 | for(int i=0;i<10;i++){ 163 | new Thread(new Runnable() { 164 | @Override 165 | public void run() { 166 | try { 167 | for(int i = 0;i<10;i++) { 168 | final int temp= i*1000; 169 | int countA = cache.getCacheCount(new CacheKey(new Object[]{"i-"+i}), new Callable() { 170 | 171 | @Override 172 | public Integer call() throws Exception { 173 | return temp; 174 | } 175 | }, temp+1000, false); 176 | } 177 | synchronized (finishedThreads){ 178 | finishedThreads++; 179 | if(finishedThreads==10){ 180 | System.out.println("Finished:"); 181 | cache.printCaches(); 182 | } 183 | } 184 | } catch (Exception e) { 185 | e.printStackTrace(); 186 | } 187 | } 188 | }).start(); 189 | } 190 | 191 | try{ 192 | Thread.sleep(100); 193 | for(int j=0;j<10;j++){ 194 | Thread.sleep(1000); 195 | cache.startClear(); 196 | System.out.println((j+1)+" second later:"); 197 | cache.printCaches(); 198 | 199 | } 200 | }catch (Exception e){ 201 | e.printStackTrace(); 202 | } 203 | 204 | } 205 | 206 | AtomicInteger finishedNum = new AtomicInteger(); 207 | 208 | @Test 209 | void cacheTest5() throws Exception { 210 | Object lock = new Object(); 211 | System.out.println("Begin"); 212 | long startTime = System.currentTimeMillis(); 213 | for(int i=0;i<8;i++){ 214 | long finalStartTime = startTime; 215 | Thread thread = new Thread(new Runnable() { 216 | @Override 217 | public void run() { 218 | for(int i=0;i<1000000;i++){ 219 | final int num = finishedNum.addAndGet(1); 220 | try { 221 | cache.getCacheCount(new CacheKey(new Object[]{"i-" + num}), new Callable() { 222 | 223 | @Override 224 | public Integer call() throws Exception { 225 | // System.out.println("i-"+num+" Executed,thread:"+Thread.currentThread().getName()); 226 | return num; 227 | } 228 | }, (int) (finalStartTime -System.currentTimeMillis()), false); 229 | }catch (Exception e){ 230 | e.printStackTrace(); 231 | } 232 | if(num == 8000000){ 233 | System.out.println(String.format("Finished %s:", num)); 234 | synchronized (lock){ 235 | lock.notify(); 236 | } 237 | } 238 | } 239 | } 240 | }); 241 | thread.setName("thread-"+i); 242 | thread.start(); 243 | } 244 | synchronized (lock) { 245 | lock.wait(); 246 | System.out.println("Insert "+((System.currentTimeMillis()-startTime)/1000)+"s"); 247 | startTime = System.currentTimeMillis(); 248 | System.out.println("Size " + cache.size()); 249 | cache.startClear(); 250 | System.out.println("Clear "+((System.currentTimeMillis()-startTime)/1000)+"s"); 251 | cache.printCaches(); 252 | 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Springboot-Mybatis-Paginator 2 | ============ 3 | ![Maven Central](https://img.shields.io/maven-central/v/com.github.xiwh/mybatis-paginator) 4 | ![GitHub](https://img.shields.io/github/license/xiwh/springboot-mybatis-paginator) 5 | 6 | 它是一款基于Springboot的高效易用易扩展的Mybatis全自动物理分页插件,提供基础分页、N+1分页、无侵入式物理分页、微侵入式易用分页等方式 7 | 8 | Features 9 | -------- 10 | * 即插即用,您甚至不需要添加任何配置,只需引入jar包即可直接使用 11 | * 普通分页(基于Count聚合统计的传统管理后台分页方式) 12 | * N+1分页(适用于移动端上拉加载的高效分页方式) 13 | * 全自动化生成Count和Limit语句,支持主流数据库方言,并提供自动优化Count的可选项(默认开启),支持包括GroupBy在内的Count语句自动生成,实现简单场景下的高效开发 14 | * 提供完全自定义Limit和Count语句的方案,覆盖一些复杂SQL需要手动写的场景 15 | * 提供Count结果缓存的可选项,适用对Count结果无强一致要求并且有一定性能要求的场景 16 | * 提供微侵入和无侵入两种使用方式 17 | * 提供完全自定义分页结构方案,可以做到快速无缝衔接到前端或已有项目中 18 | 19 | Supported databases 20 | -------- 21 | * `mysql` 22 | * `mariadb` 23 | * `oracle` 24 | * `sqlserver>=2012` 25 | * `posgresql` 26 | * `sqlite` 27 | 28 | Setup 29 | -------- 30 | 31 | Gradle: 32 | ```groovy 33 | compile 'com.github.xiwh:mybatis-paginator:1.0.2' 34 | ``` 35 | Maven: 36 | ```xml 37 | 38 | com.github.xiwh 39 | mybatis-paginator 40 | 1.0.2 41 | 42 | ``` 43 | 44 | 快速使用 45 | -------- 46 | ***(必须)将"com.xiwh.paginator"加入到自动扫描路径下,如下*** 47 | ```java 48 | @SpringBootApplication 49 | @ComponentScan({"com.xiwh.paginator","com.example.demo"}) 50 | public class DemoApplication { 51 | public static void main(String[] args) { 52 | SpringApplication.run(DemoApplication.class, args); 53 | } 54 | } 55 | ``` 56 | ***普通分页*** 57 | ```java 58 | public interface SimplePagingMapper { 59 | @Select("SELECT * FROM a") 60 | @NormalPaginator() 61 | SimplePage select(); 62 | 63 | public static void main(String[] args){ 64 | SimplePagingMapper mapper = xxx; 65 | int page = 0; 66 | int size = 10; 67 | Paginator.paginate(page,size); 68 | SimplePage page = mapper.select(); 69 | System.out.println(page); 70 | System.out.println(page.hasLast()); 71 | System.out.println(page.hasNext()); 72 | System.out.println(page.totalPage()); 73 | System.out.println(page.total()); 74 | System.out.println(page.size()); 75 | System.out.println(page.list()); 76 | System.out.println(page.toMap()); 77 | for (Bean item:page) { 78 | item.setName(item.getId()+":"+item.getName()); 79 | } 80 | } 81 | } 82 | 83 | ``` 84 | 85 | ***N+1分页*** 86 | ```java 87 | public interface NPlusOnePagingMapper { 88 | @Select("SELECT * FROM a") 89 | @NPlusOnePaginator 90 | NPlusOnePage select(); 91 | 92 | public static void main(String[] args){ 93 | NPlusOnePagingMapper mapper = xxx; 94 | Paginator.paginate(0,10); 95 | NPlusOnePage page = mapper.select(); 96 | System.out.println(page); 97 | System.out.println(page.hasNext()); 98 | System.out.println(page.toMap()); 99 | for (Bean item:page) { 100 | item.setName(item.getId()+":"+item.getName()); 101 | } 102 | } 103 | } 104 | 105 | ``` 106 | 107 | 进阶 108 | -------- 109 | ***无侵入物理分页*** 110 | ```java 111 | public interface SimplePagingMapper { 112 | @Select("SELECT * FROM a") 113 | @NormalPaginator(cache=true,cacheExpiryTime=3600) 114 | List select(RowBounds rowBounds); 115 | } 116 | ``` 117 | ***无侵入物理分页进阶(取分页结构)*** 118 | ```java 119 | public interface SimplePagingMapper { 120 | @Select("SELECT * FROM a") 121 | @NormalPaginator(cache=true,cacheExpiryTime=3600) 122 | List select(PagingRowBounds rowBounds); 123 | 124 | public static void main(String[] args){ 125 | NPlusOnePagingMapper mapper = xxx; 126 | List list = mapper.select(); 127 | int page = 0; 128 | int size = 10; 129 | PagingRowBounds rowBounds = new PagingRowBounds(page,size); 130 | NormalPaginator page = rowBounds.toNormalPage(NormalPaginator.class, list); 131 | } 132 | } 133 | ``` 134 | ***设置起始页偏移量(用以兼容前端分页参数,通常前端分页从1开始)*** 135 | ```java 136 | public interface SimplePagingMapper { 137 | //第一种方式 138 | @Select("SELECT * FROM a") 139 | @NormalPaginator(pageOffset=1) 140 | List select(); 141 | 142 | //第二种方式(通过RowBounds 的传参方式,注解内pageOffset将不会生效) 143 | @Select("SELECT * FROM a") 144 | @NormalPaginator() 145 | List selectRowBounds(PagingRowBounds rowBounds); 146 | public static void main(String[] args){ 147 | NPlusOnePagingMapper mapper = xxx; 148 | List list = mapper.select(); 149 | int page = 0; 150 | int size = 10; 151 | int pageOffset = 1; 152 | boolean forceCounting = true; 153 | PagingRowBounds rowBounds = new PagingRowBounds(page,size,pageOffset, forceCounting); 154 | NormalPaginator page = rowBounds.toNormalPage(NormalPaginator.class, list); 155 | } 156 | } 157 | ``` 158 | ***通过当前GET请求自动注入分页参数*** 159 | ```java 160 | @Mapper 161 | public interface SimplePagingMapper { 162 | @Select("SELECT * FROM a") 163 | @NormalPaginator(auto = true,pageOffset = 1) 164 | SimplePage select(); 165 | } 166 | @RestController 167 | public class PagingController{ 168 | @Autowired 169 | SimplePagingMapper simplePagingMapper; 170 | /** 171 | * http://127.0.0.1:8080/test?page=1&size=20 172 | * { 173 | * "total": 16, 174 | * "size": 10, 175 | * "total_page": 2, 176 | * "has_last": false, 177 | * "has_next": true, 178 | * "page": 1, 179 | * "list": [{ 180 | * "id": 1, 181 | * "name": "a", 182 | * "value": "v1" 183 | * }, { 184 | * "id": 2, 185 | * "name": "a", 186 | * "value": "v1" 187 | * }, { 188 | * "id": 3, 189 | * "name": "a", 190 | * "value": "v1" 191 | * }, { 192 | * "id": 4, 193 | * "name": "d", 194 | * "value": "v4" 195 | * }, { 196 | * "id": 5, 197 | * "name": "e", 198 | * "value": "v5" 199 | * }, { 200 | * "id": 6, 201 | * "name": "f", 202 | * "value": "v6" 203 | * }, { 204 | * "id": 7, 205 | * "name": "g", 206 | * "value": "v7" 207 | * }, { 208 | * "id": 8, 209 | * "name": "h", 210 | * "value": "v8" 211 | * }, { 212 | * "id": 9, 213 | * "name": "i", 214 | * "value": "v9" 215 | * }, { 216 | * "id": 10, 217 | * "name": "j", 218 | * "value": "v10" 219 | * }] 220 | * } 221 | */ 222 | @RequestMapping("/test") 223 | public Map test(HttpServletRequest request){ 224 | //默认自动读取page 和 size参数,如不存在则取默认值, 参数key和默认值可在配置修改 225 | Map map = simplePagingMapper.select().toMap(); 226 | return map; 227 | } 228 | } 229 | ``` 230 | ***启用Count结果缓存*** 231 | ```java 232 | public interface SimplePagingMapper { 233 | @Select("SELECT * FROM a") 234 | //缓存有效时间3600秒 235 | @NormalPaginator(cache=true,cacheExpiryTime=3600) 236 | SimplePage select(); 237 | } 238 | ``` 239 | 240 | ***自定义Count方法*** 241 | ```java 242 | public interface SimplePagingMapper { 243 | @Select("SELECT count(1) FROM a where id != ${bb} or id != #{aa}") 244 | Integer customCount(String aa, Integer bb); 245 | 246 | //与Count方法参数必须要完全一致! 247 | @Select("SELECT * FROM a where id != ${bb} or id != #{aa}") 248 | @NormalPaginator(countMethod = "customCount") 249 | SimplePage customCountSelect(String aa, Integer bb); 250 | } 251 | ``` 252 | 253 | ***自定义Limit语句(兼容XML Mapper)*** 254 | ```java 255 | public interface SimplePagingMapper { 256 | @Select("SELECT count(1) FROM a where id != ${bb} or id != #{aa}") 257 | Integer customCount(String aa, Integer bb); 258 | 259 | //开启自定义limit后,必须同时自定义count方法 260 | //提供:offset :limit :end(与oracle等数据库配套使用)三个内置变量 261 | @Select("SELECT * FROM a WHERE id != ${bb} OR id != #{aa} LIMIT :offset,:limit") 262 | @NormalPaginator(customLimit = true, countMethod = "customCount") 263 | SimplePage customLimitSelect(String aa, Integer bb); 264 | } 265 | ``` 266 | 267 | ***自定义分页返回数据结构*** 268 | ```java 269 | public class MyPagingResult extends NormalPageWrapperBase { 270 | 271 | private int total; 272 | private int startOffset; 273 | private int physicalPage; 274 | private int page; 275 | private int size; 276 | private int totalPage; 277 | private List list; 278 | 279 | public boolean hasNext(){ 280 | return list.size()>=size; 281 | } 282 | 283 | public boolean hasLast(){ 284 | return physicalPage>0; 285 | } 286 | 287 | public int size(){ 288 | return size; 289 | } 290 | 291 | public int page(){ 292 | return page; 293 | } 294 | 295 | public int total(){ 296 | return total; 297 | } 298 | 299 | public int totalPage(){ 300 | return totalPage; 301 | } 302 | 303 | public List list() { 304 | return list; 305 | } 306 | 307 | /** 308 | * Paged callback 309 | * @param list 310 | * @param count 311 | * @param startOffset 312 | * @param physicalPage 313 | * @param size 314 | */ 315 | @Override 316 | public void onInit(List list, int count, int startOffset, int physicalPage, int size) { 317 | this.list = list; 318 | this.total = count; 319 | this.physicalPage = physicalPage; 320 | this.startOffset = startOffset; 321 | this.page = physicalPage + startOffset; 322 | this.size = size; 323 | this.totalPage = total/size+(total%size==0?0:1); 324 | } 325 | } 326 | public interface SimplePagingMapper { 327 | @Select("SELECT * FROM a") 328 | @NormalPaginator() 329 | MyPagingResult select(); 330 | } 331 | ``` 332 | 配置 333 | -------- 334 | ```yaml 335 | #显示分页SQL 336 | logging.level.com.xiwh.paginator: debug 337 | paginator: 338 | # 根据Get请求自动完成参数注入Key 339 | size-key: size 340 | page-key: page 341 | # 根据Get请求自动注入默认页大小 342 | default-size: 10 343 | ``` 344 | License 345 | -------- 346 | MIT License 347 | 348 | Copyright (c) 2020 xiwh 349 | 350 | Permission is hereby granted, free of charge, to any person obtaining a copy 351 | of this software and associated documentation files (the "Software"), to deal 352 | in the Software without restriction, including without limitation the rights 353 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 354 | copies of the Software, and to permit persons to whom the Software is 355 | furnished to do so, subject to the following conditions: 356 | 357 | The above copyright notice and this permission notice shall be included in all 358 | copies or substantial portions of the Software. 359 | 360 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 361 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 362 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 363 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 364 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 365 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 366 | SOFTWARE. --------------------------------------------------------------------------------