最近碰到个需求,要做个透明的mybatis分页功能,描述如下:
目标:搜索列表的Controller action要和原先保持一样,并且返回的json需要有分页信息,如:@ResponseBody@RequestMapping(value="/search", method={RequestMethod.POST})public Listsearch(@RequestBody SearchProjectCommand command){ List projects=projectFetcher.search(command.getKey(), command.getFrom(), command.getTo()); return projects;}
返回信息:
{ "successful": true, "message": null, "messages": null, "dateTime": 1505651777350, "body": { "totalCount": 2, "totalPage": 1, "records": [ { "projectId": "1111", "projectName": "11111111111111", "title": "11111111111111" }, { "projectId": "22222", "projectName": "222222", "title": "222222" } ] }}
关键点:
- 针对Controller方法的aop
- Mybatis interceptor && PagingContext保存分页信息
- ResponseBodyAdvice(用于在输出json之前加入通用格式)
开始之前,先来看看消息格式,以及某些限制,主要是针对分页pageIndex这种参数的传递:
public abstract class PagingCommand { private int pageSize; private int pageIndex; public PagingCommand getPagingInfo() { return this; } public int getPageSize() { if(pageSize<=0) return Integer.MAX_VALUE; return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getPageIndex() { if(pageIndex<0) return 0; return pageIndex; } public void setPageIndex(int pageIndex) { this.pageIndex = pageIndex; }}public class PagingResponse { private int totalCount; private int totalPage; private List
PagingCommand是抽象类,所有的具体Command必须继承这个Command
PagingResponse是分页结果
先来看看横切入口AOP类:
1 @Aspect 2 @Component 3 public class PagingAop { 4 private static final Logger logger = LoggerFactory.getLogger(PagingAop.class); 5 6 @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)") 7 public void controllerMethodPointcut() { 8 } 9 10 @Around("controllerMethodPointcut()")11 public Object Interceptor(ProceedingJoinPoint pjp) throws Throwable {12 13 logger.info("Paging...");14 15 //找到是否具有PagingCommand的class作为输入参数16 //有,则放入PagingContext中17 for(Object arg:pjp.getArgs())18 {19 if(arg==null)20 continue;21 22 logger.info(arg.getClass().toString());23 if(PagingCommand.class.isAssignableFrom(arg.getClass()))24 {25 logger.info("需要分页行为");26 PagingContext.setPagingCommand((PagingCommand)arg);27 }28 else29 {30 logger.info("不需要分页行为");31 }32 }33 34 return pjp.proceed();35 }36 }
代码很容易识别,判断参数是否是继承自PagingCommand,只要有1个继承自PagingCommand就会设置相应参数到PagingContext来标识需要分页处理,下面看看这个Context类:
1 public final class PagingContext { 2 private static ThreadLocalpagingCommand=new ThreadLocal (); 3 private static ThreadLocal totalCount=new ThreadLocal (); 4 private static ThreadLocal totalPage=new ThreadLocal (); 5 6 public static void setPagingCommand(PagingCommand cmd) 7 { 8 pagingCommand.set(cmd); 9 }10 11 public static PagingCommand getPagingCommand()12 {13 return pagingCommand.get();14 }15 16 public static boolean isPagingCommandEmpty()17 {18 if(pagingCommand.get()==null)19 return true;20 21 return false;22 }23 24 25 public static int getTotalCount() {26 return totalCount.get();27 }28 29 public static void setTotalCount(int count) {30 totalCount.set(count);31 }32 33 public static boolean isTotalCountEmpty()34 {35 if(totalCount.get()==null)36 return true;37 38 return false;39 }40 41 42 public static int getTotalPage() {43 return totalPage.get();44 }45 46 public static void setTotalPage(int pages) {47 totalPage.set(pages);48 }49 }
针对各个线程的ThreadLocal变量,但是目前只支持普通的httprequest线程才能正常工作,ThreadPool的有问题,等以后再解决。
下面是核心的mybatis分页插件了:1 @Intercepts({@Signature(type=Executor.class,method="query",args={ MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class/*, CacheKey.class, BoundSql.class*/})}) 2 public class PagingInterceptor implements Interceptor { 3 private static final Logger logger = LoggerFactory.getLogger(PagingInterceptor.class); 4 5 @Override 6 public Object intercept(Invocation invocation) throws Throwable { 7 8 9 logger.info("intercept.............");10 11 //判断是否需要分页行为, from PagingContext中12 if(PagingContext.isPagingCommandEmpty())13 return invocation.proceed();14 15 MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0];16 Object parameter = invocation.getArgs()[1];17 BoundSql boundSql = mappedStatement.getBoundSql(parameter);18 String originalSql = boundSql.getSql().trim();19 20 //生成count sql,然后执行21 int totalCount = getTotalCount(mappedStatement, boundSql, originalSql);22 //set totalCount value to context23 PagingContext.setTotalCount(totalCount);24 25 int totalPages=calculateTotalPagesCount(totalCount, PagingContext.getPagingCommand().getPageSize());26 PagingContext.setTotalPage(totalPages);27 28 //生成分页limit sql,然后执行29 MappedStatement newMs = wrapPagedMappedStatement(mappedStatement, boundSql, originalSql);30 invocation.getArgs()[0]= newMs;31 32 return invocation.proceed();33 }34 35 private int calculateTotalPagesCount(int totalCount, int pageSize) {36 int pageCount=totalCount/pageSize;37 38 if(pageCount==0)39 return 1;40 41 if(pageCount*pageSize<=totalCount)42 return pageCount;43 44 return pageCount+1;45 }46 47 private MappedStatement wrapPagedMappedStatement(MappedStatement mappedStatement, BoundSql boundSql, String originalSql) {48 PagingCommand page= PagingContext.getPagingCommand();49 int offset = (page.getPageIndex()) * page.getPageSize();50 StringBuffer sb = new StringBuffer();51 sb.append(originalSql).append(" limit ").append(offset).append(",").append(page.getPageSize());52 BoundSql newBoundSql = MyBatisUtils.copyFromBoundSql(mappedStatement, boundSql, sb.toString());53 return MyBatisUtils.copyFromMappedStatement(mappedStatement,new BoundSqlSqlSource(newBoundSql));54 }55 56 private int getTotalCount(MappedStatement mappedStatement, BoundSql boundSql, String originalSql) throws SQLException {57 Object parameterObject = boundSql.getParameterObject();58 String countSql = getCountSql(originalSql);59 Connection connection=mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection() ;60 PreparedStatement countStmt = connection.prepareStatement(countSql);61 BoundSql countBS = MyBatisUtils.copyFromBoundSql(mappedStatement, boundSql, countSql);62 DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBS);63 parameterHandler.setParameters(countStmt);64 ResultSet rs = countStmt.executeQuery();65 int totalCount=0;66 if (rs.next()) {67 totalCount = rs.getInt(1);68 }69 rs.close();70 countStmt.close();71 connection.close();72 return totalCount;73 }74 75 private String getCountSql(String sql) {76 return "SELECT COUNT(1) FROM (" + sql + ") Mybatis_Pager_TBL_ALIAS";77 }78 79 @Override80 public Object plugin(Object o) {81 return Plugin.wrap(o, this);82 }83 84 @Override85 public void setProperties(Properties properties) {86 87 }88 }
最后就一步了,就是写一个ResponseBodyAdvice来根据判断是否分页输出,来返回json:
1 @ControllerAdvice 2 public class GlobalMessageResponseBodyAdvice implements ResponseBodyAdvice { 3 4 @Override 5 public boolean supports(MethodParameter methodParameter, Class aClass) { 6 return true; 7 } 8 9 @Override10 public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {11 12 Object payload = o;13 14 //判断是否需要分页15 if (isNeedPagingResponse()) {16 PagingResponse response = new PagingResponse();17 18 response.setTotalCount(PagingContext.getTotalCount());19 response.setTotalPage(PagingContext.getTotalPage());20 response.setRecords((List) payload);21 22 payload = response;23 }24 25 NormalMessage msg = new NormalMessage();26 msg.setSuccessful(true);27 msg.setMessage(null);28 msg.setBody(payload);29 return msg;30 31 }32 33 public boolean isNeedPagingResponse() {34 if(PagingContext.isPagingCommandEmpty())35 return false;36 37 return true;38 }39 }
完成。