springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables

在上一篇博文中我们简单介绍了下mybatis-plus(简称mp)的使用,并测试了下CURD。这篇文章我们重点书写如何将数据渲染到datatables。有一定编程基础的同学重点看下第3节编写DatatablesUtil类。

目录

1、mybatis-plus条件、分页查询

1.1、条件查询

1.2、分页查询

2、datatables的使用

2.1、controller层的设计

2.2、视图代码的修改

2.3、datatables配置与参数说明

2.4、处理datatables的ajax请求

3、编写DatatablesUtil类(重点)


1、mybatis-plus条件、分页查询

1.1、条件查询

mp沿用了mybatis一贯的灵活,对于条件查询的方式也有多种,我们这里就使用以QueryWrapper为查询条件封装的方式。如以下测试代码:

/**
 * 条件查询
 * */
@Test
public void testQueryCondition() {		
	QueryWrapper query = new QueryWrapper<>();
	//query.eq("title", "hello");		
	query.like("title", "h");
	noticeServiceImpl.list(query).forEach(System.out::println);			
}

最后一句用了lambda表达式,也就是将查询出来的结果列表打印出来。值得注意的是,在使用QueryWrapper时候,条件参数是数据库表的列名,而不是javabean对应的属性名。这里就不放自己的查询结果了,自己设计数据测试就可以。

1.2、分页查询

在使用分页查询的时候,需要先引入分页插件PaginationInterceptor,具体的使用方法是
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第1张图片
写到这里,大家就可以直接使用了,在使用之前,我们先将代码重构一下,因为我们不能把所有的配置都放置在启动类当中来。当后续的配置越来越多的时候,不利于我们的开发与维护。那么我们将mp的配置放置到单独的类当中去。如图:
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第2张图片
这里我就直接沿用官网的代码,具体的设置,按里面的注释去测试就可以了:

package com.ruiyi.common.mp;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;

/**
* 

Title:

*

Description:

* @user tim * @date 2020年3月18日 * @版本 1.00 * @修改记录 *
* 版本           修改人          修改时间                    修改内容描述
* ----------------------------------------
* 1.00 yangtao  2020年3月18日     初始化版本
* ----------------------------------------
* 
*/ @Configuration @EnableTransactionManagement @MapperScan("com.ruiyi.business.dao") public class MpConfiguration { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false // paginationInterceptor.setOverflow(false); // 设置最大单页限制数量,默认 500 条,-1 不受限制 // paginationInterceptor.setLimit(500); // 开启 count 的 join 优化,只针对部分 left join paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true)); return paginationInterceptor; } }

那么我们的启动类又重新干净了。
 springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第3张图片

然后我们在测试类中测试使用一下:
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第4张图片

至于翻页当中的参数意义,我们可以来看看源码,这里就复制一段属性与构造函数的代码说明

/**
     * 查询数据列表
     */
    private List records = Collections.emptyList();

    /**
     * 总数
     */
    private long total = 0;
    /**
     * 每页显示条数,默认 10
     */
    private long size = 10;

    /**
     * 当前页
     */
    private long current = 1;

    /**
     * 排序字段信息
     */
    private List orders = new ArrayList<>();

    /**
     * 自动优化 COUNT SQL
     */
    private boolean optimizeCountSql = true;
    /**
     * 是否进行 count 查询
     */
    private boolean isSearchCount = true;

    public Page() {
    }

    /**
     * 分页构造函数
     *
     * @param current 当前页
     * @param size    每页显示条数
     */
    public Page(long current, long size) {
        this(current, size, 0);
    }

    public Page(long current, long size, long total) {
        this(current, size, total, true);
    }

    public Page(long current, long size, boolean isSearchCount) {
        this(current, size, 0, isSearchCount);
    }

    public Page(long current, long size, long total, boolean isSearchCount) {
        if (current > 1) {
            this.current = current;
        }
        this.size = size;
        this.total = total;
        this.isSearchCount = isSearchCount;
    }

 那么翻页条件查询就很简单了,直接使用对应参数的方法就可以了
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第5张图片

2、datatables的使用

2.1、controller层的设计

我们已经能够拿到条件查询的分页数据了,那么剩下的事情就是将得到的结果渲染出来,因为我们一直在使用H-ui作为我们的后台模板,所以数据的渲染就不像是直接拿数据在页面中用thymeleaf的语法了。而是将所得到的数据转换成json,并交给datatables渲染。这里我画两张图来说明下。
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第6张图片
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第7张图片
这样的话,我们可以很清淅的来编写controller层了。在看代码之前,我们先对我们的项目结构做一些调整。这样的修改其实是为了方便配合后来mp的代码生成器。对于项目结构,按自己习惯了的命名做就行,触类旁通,可以少走一些的弯路。
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第8张图片
上面的项目结构,是根据”模块——>功能“来划分的。这里在resources中加了mapper目录,里面存放sql的映射文件(这类文件一般是在mp中遇上复杂查询,自定义sql的时候才用)。当我们修改了项目结构后,mp对应的扫描配置也要做相关的修改,如图:
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第9张图片
而在配置文件中,目前我们是用开发版的yml
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第10张图片
修改完成后,不妨先测试一下mp的相关配置是否已经成功。

接下来我们只要书写相关的控制层代码了。

package com.ruiyi.business.sys.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.ruiyi.business.sys.service.INoticeService;

/**
 * 
* @author tim* 
*/
@Controller
@RequestMapping("/admin/notice")
public class NoticeController {
	
	@Autowired
    private INoticeService noticeServiceImpl;

	/**
	 * 公告列表页面
	 */
	@RequestMapping("/noticeListPage")
	public String noticeListPage() {
		return "/admin/sys/notice/noticeList";
	}
	
}

2.2、视图代码的修改

我们先写跳转页面。在H-ui中选一个含有datatables的简单页面,这里我套用会员列表页面member-list.html。我们先看下结构。
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第11张图片
再将member-list.html中的代码copy到noticeList.html中,并引入之前封装好的公共静态资源代替写死的资源,如下图:
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第12张图片
然后在index.html页面加入当前页面的controller链接
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第13张图片
我们启动服务器,并链接查看,我们会发现后台报了一个错误
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第14张图片
仔细阅读后,会发现,是thymeleaf的解析错误,为什么呢?原来[[......]]之间的内容会被thymeleaf认为是内联表达式,所以它会按thymeleaf的语法去解析。显然,datatables中的这段代码——"aaSorting": [[ 1, "desc" ]],,thymeleaf解析不了。我们只要在两个中括号之间加上空格就可以了。如:"aaSorting": [ [ 1, "desc" ] ],修改后我们再访问一下,这个静态页面就成功导入了。如图:
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第15张图片
再修改页面上的信息为公告相关的内容就可以。这里放一下页面代码:




公告标题 创建时间 操作

我们看到的页面就是如下图片:
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第16张图片
H-ui里面用到的时间插件是WdatePicker,只要按所在的目录引入就可以。它的表达式,也只要查看官方的api即可。我们在接下来的博文会来说明

2.3、datatables配置与参数说明

接下来的逻辑到了datatables异步获取数据与渲染了。datatables的知识点,实在太多,这里不可能一一列举,这里就直接上代码,并在代码中注释下。使用的时候注意下当前的版本与对应的JQuery版本。其实知识点再多,无外乎就是ajax传参到后台获取数据,再将获取的数据渲染到具体的行与列,并对行与列作相关的修饰,datatables官网提供了非常清淅的api,直接查看就可以:https://datatables.net/。

 

到了这一步,我们解释下页面查询条件传入的格式,也就是下面的这段代码:

//查询条件数据封装
aoData.push({
	"name":"condition",
	"value":{
		"title": $("#title").val(),
		"titleAccurate": $("#titleAccurate").prop("checked") ? "1" : "",
		"createTimeBegin": $("#createTimeBegin").val(),
		"createTimeEnd": $("#createTimeEnd").val()
	}
});

为什么是这种格式呢?大家可以在后台打印下aoData是怎么封装的就很清楚,它是以name和value为键,对应的值为值的json格式。我们看下我调试时的具体值。

[
{"name":"sEcho","value":1},
{"name":"iColumns","value":4},
{"name":"sColumns","value":",,,"},
{"name":"iDisplayStart","value":0},
{"name":"iDisplayLength","value":20},
{"name":"mDataProp_0","value":"id"},
{"name":"bSortable_0","value":false},
{"name":"mDataProp_1","value":"title"},
{"name":"bSortable_1","value":true},
{"name":"mDataProp_2","value":"createTime"},
{"name":"bSortable_2","value":true},
{"name":"mDataProp_3","value":""},
{"name":"bSortable_3","value":false},
{"name":"iSortCol_0","value":0},
{"name":"sSortDir_0","value":"desc"},
{"name":"iSortingCols","value":1},
{"name":"condition","value":{"title":"","titleAccurate":"","createTimeBegin":"","createTimeEnd":""}}
]

这也是我们ajax传给后台的参数。现在再刷新我们的页面,大家可以看到404的提醒,因为我们还没有写ajax所请求的URL,但说明我们datatables的配置已经成功了。

2.4、处理datatables的ajax请求

我们在上一小节对ajax的请求地址写的是:"sAjaxSource" : "/admin/notice/noticeListData",我们现在去后台添加。我们通过noticeListData获取datatables的传入参数,也就是上面所复制的代码。然后我们在service层加入处理逻辑:
1、解析参数信息(json格式)
2、封装查询条件
3、封装datatables分页、排序
4、返回渲染datatables的数据

那么我们这里就来上代码:
controller层:

/**
 * 公告列表数据
 */
@PostMapping("/noticeListData")
@ResponseBody
public String noticeListData(String aoData,Notice vo) {
    return noticeServiceImpl.dataTablesInfo(aoData,vo);
}

service层,这里只上实现类的代码,父接口增加对应的方法就可以:

package com.ruiyi.business.sys.service.impl;

import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruiyi.business.sys.entity.Notice;
import com.ruiyi.business.sys.mapper.NoticeMapper;
import com.ruiyi.business.sys.service.INoticeService;

/**
 * 

* 公告 服务实现类 *

* * @author tim */ @Service public class NoticeServiceImpl extends ServiceImpl implements INoticeService { @Override public String dataTablesInfo(String aoData,Notice vo) { /* * 1、获取DataTables所有参数信息,并转换为Map * */ //jackson json解析对象 ObjectMapper om = new ObjectMapper(); //封装DataTables参数信息的Map容器 Map tableDataMap = new HashMap(); try { @SuppressWarnings("unchecked") List> objLst = om.readValue(aoData, List.class); for (Map obj : objLst) { tableDataMap.put(obj.get("name").toString(), obj.get("value")); } }catch(IOException e) { throw new RuntimeException("解析json数据错误:" + e.getMessage()); } /* * 2、设置查询条件值 * */ //获取查询条件信息 Object qw = tableDataMap.get("condition"); QueryWrapper queryWrapper = null; if(null != qw) { queryWrapper = new QueryWrapper(); @SuppressWarnings("unchecked") Map conditionMap = (Map)qw; //查询条件键值的Set容器 Set propSet = conditionMap.keySet(); //标题 if(conditionMap.get("title") != null && StringUtils.isNoneBlank(conditionMap.get("title").toString())) { //是否模糊查询 if(conditionMap.get("titleAccurate") != null && StringUtils.equals(conditionMap.get("titleAccurate").toString(), "1")) { queryWrapper.like("title", conditionMap.get("title")); }else { queryWrapper.eq("title", conditionMap.get("title")); } } //开始与结束时间 boolean beginTimeFill = conditionMap.get("createTimeBegin") != null && StringUtils.isNotBlank(conditionMap.get("createTimeBegin").toString()); boolean endTimeFill = conditionMap.get("createTimeEnd") != null && StringUtils.isNotBlank(conditionMap.get("createTimeEnd").toString()); //开始时间条件封装 if(beginTimeFill) { //当前字符串日期最小时间 queryWrapper.ge("create_time", LocalDateTime.of(LocalDate.parse(conditionMap.get("createTimeBegin").toString()), LocalTime.MIN)); } //结束时间条件封装 if(endTimeFill) { //当前字符串日期最小时间 queryWrapper.le("create_time", LocalDateTime.of(LocalDate.parse(conditionMap.get("createTimeEnd").toString()), LocalTime.MAX)); } } //3、设置分页、排序信息 Page page = new Page<>(); //计算每页显示行数,当前页 int iDisplayStart = Integer.parseInt(tableDataMap.get("iDisplayStart").toString()); int iDisplayLength = Integer.parseInt(tableDataMap.get("iDisplayLength").toString()); int size = iDisplayLength; int current = iDisplayStart/size + 1; page.setSize(size); page.setCurrent(current); //处理排序 String sSortDir_0 = null; //升序、降序值 String iSortCol_0 = null; //排序列下标 String orderCol = null; //客户点击的排序列 if(tableDataMap.get("sSortDir_0") != null && tableDataMap.get("iSortCol_0") != null) { sSortDir_0 = tableDataMap.get("sSortDir_0").toString(); iSortCol_0 = tableDataMap.get("iSortCol_0").toString(); } if(StringUtils.isNotEmpty(sSortDir_0) && StringUtils.isNotEmpty(iSortCol_0)) { orderCol = tableDataMap.get("mDataProp_" + iSortCol_0).toString(); //将属性转成表列,并装入排序信息 orderCol = toSeparatorLower(orderCol, "_"); if(StringUtils.equalsIgnoreCase("asc", sSortDir_0)) { page.addOrder(OrderItem.asc(orderCol)); } if(StringUtils.equalsIgnoreCase("desc", sSortDir_0)) { page.addOrder(OrderItem.desc(orderCol)); } } //4、组织查询结果 IPage pageT = this.page(page, queryWrapper); List records = pageT.getRecords(); String recordsJson = null; try { recordsJson = om.writeValueAsString(records); } catch (JsonProcessingException e) { throw new RuntimeException("json处理异常:" + e.getMessage()); } //封装成datatables所需要的数据 StringBuilder sb = new StringBuilder(); sb.append("{\"sEcho\":"); sb.append(tableDataMap.get("sEcho").toString()); sb.append(",\"iTotalRecords\":"); sb.append(pageT.getTotal()); sb.append(",\"iTotalDisplayRecords\":"); sb.append(pageT.getTotal()); sb.append(",\"aaData\":"); sb.append(recordsJson); sb.append("}"); return sb.toString(); } /** * 根据驼峰字符串转换成界定的小写字符串 * 例如: * helloWorld => hello_world * HelloWorld => hello_world * @param String 转换的字符串 * @param separatorChar 界定符 * */ private String toSeparatorLower(String source,String separatorChar) { if(StringUtils.isEmpty(source)) { return ""; } int len = source.length(); StringBuilder sb = new StringBuilder(len); char c; for (int i = 0; i < len; i++) { c = source.charAt(i); if (Character.isUpperCase(c)) { if(i > 0) { sb.append(separatorChar); } sb.append(Character.toLowerCase(c)); } else { sb.append(c); } } return sb.toString(); } }

上面的代码有些多,这里梳理下思路:
1、解析参数:java解析json的第三方插件很多,像gson、fastjson等,这里我们用的jackson,因为springboot默认就是用的它;
2、根据我们传入的"condition"值,封装对应的查询条件到QueryWrapper;
3、设置分页排序信息,这里要求我们对datatables的参数有所了解,这里再简单说明下:
      iDisplayStart:开始行
      iDisplayLength:每页显示行数
      sSortDir_0:升序与降序?也就是asc与desc
      iSortCol_0:排序列下标,这个参数是与mDataProp_X参数配合使用的,我们看下具体参数

{"name":"mDataProp_0","value":"id"},
{"name":"bSortable_0","value":false},
{"name":"mDataProp_1","value":"title"},
{"name":"bSortable_1","value":true},
{"name":"mDataProp_2","value":"createTime"},
{"name":"bSortable_2","value":true},
{"name":"mDataProp_3","value":""},
{"name":"bSortable_3","value":false},
{"name":"iSortCol_0","value":0},

      我们可以发现, mDataProp_x,x代表的是每列的下标,当iSortCol_0的值取0,就是mDataProp_0的值“id”进行排序。排序的内容取sSortDir_0中的值。
      mDataProp_x:mDataProp_x所取的值是我们配置datatables时所对应的值,如图中椭圆所标识出来的。
      springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第17张图片
      我们这里是按属性来的,所以写了个简单的方法toSeparatorLower,将类的属性转成表的列名
      bSortable_x:x下标对应的列是否允许点击排序,如上图中方框所标识出来的。在展示的地方也就是:
      springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第18张图片

4、封装返回的数据结果,我们同样只要了解data所需要的参数的意义就可以了

{
"sEcho":1,//每次请求都不同,用来区分操作的
"iTotalRecords":100,//实际的总数量
"iTotalDisplayRecords":100,//过滤之后的数量,我们不用datatables的假翻页的话,它和iTotalRecords等价
"aaData":'.....',//当前页的所有结果集
...
}

如果不出意外的话,就可以出结果了:
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第19张图片
大家可以调小下每页显示数量的值,测试下分页;并测试下条件与排序。

3、编写DatatablesUtil类(重点)

所有的代码,都存在重构的空间,尤其是java。大家在完成了Notice的渲染datatables功能后,建议再多做几张表的datatables功能,并找找它们的一些共同点,尤其是做下多表关联查询。我再来看这节内容

当我们多写了几遍类似的代码后,大家会提出一个疑问,这些重复的工作量,怎么把它们优雅地抽取出来?我们先来看看我们用到的mybatis-plus的类结构:
springBoot、springSecurity、H-ui通用mvc后台(六)mybatis-plus分页并渲染到datatables_第20张图片
看到这里,大家会也许会想到, 我们写一个公共的类,假设取名叫BaseServiceImpl,实现关于datatables与具体实体(参数传入)的转换,然后自己再写XxxServiceImpl类去继承BaseServiceImpl。这样当然可以实现功能,但继承所带来的结果就是耦合粒度太大,并且打破了封装,而且并不是所有的表都要通过datatables来渲染。我们程序设计上有一个原则:复合优先于继承

这时候,我们应该想到,设计一个这样的工具类:将datatables传入的参数、映射的实体、该实体的处理对象传入到一个静态方法。当有需要的时候,直接调用这个方法就行,这样实现了功能,又大大的解耦了。我们的类名就取DatatablesUtil,这个静态方法设计成这样:

/**
 * @param String datatables的参数
 * @param T 实体对象泛型
 * @param IService 实参就是具体的xxxServiceImpl
 * @param Map propertyMap 多表关联的时候用,用来装映射列名与属性
 * */
public static  String dataTableInfo(String aoData,T vo, IService serviceImpl,Map propertyMap) {
	/*1、解析参数信息(json格式)
	2、封装查询条件
	3、封装datatables分页、排序
	4、返回渲染datatables的数据*/
	return xxx;
}

上面的代码设计用到了静态方法与泛型的知识点。那么接下来先上这个类的全部代码:

package com.ruiyi.common.util;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;


/**
* 

Title:

*

Description:

* @user yangtao * @date 2019年10月23日 * @版本 1.00 * @修改记录 *
* 版本           修改人          修改时间                    修改内容描述
* ----------------------------------------
* 1.00 yangtao  2019年10月23日     初始化版本
* ----------------------------------------
* 
*/ public class DataTablesUtil { //禁止实例化 private DataTablesUtil() {} //条件查询自定义常量 private static final String ACCURATE_SUFFIX = "Accurate"; private static final String TIME_BEGIN_SUFFIX = "Begin"; private static final String TIME_END_SUFFIX = "End"; private static final String QUERY_CONDITION = "condition"; //datatables常量 private static final String I_DISPLAY_START = "iDisplayStart"; private static final String I_DISPLAY_LENGTH = "iDisplayLength"; private static final String S_SORT_DIR_0 = "sSortDir_0"; private static final String I_SORT_COL_0 = "iSortCol_0"; private static final String M_DATA_PROP_ = "mDataProp_"; private static final String ASC = "asc"; private static final String DESC = "desc"; /** * @param String datatables的参数 * @param T 实体对象泛型 * @param IService 实参就是具体的xxxServiceImpl * * 单表重载 * */ public static String dataTableInfo(String aoData,T vo, IService serviceImpl) { return dataTableInfo(aoData,vo,serviceImpl,null); } /** * @param String datatables的参数 * @param T 实体对象泛型 * @param IService 实参就是具体的xxxServiceImpl * @param Map propertyMap 多表关联的时候用,用来装映射列名与性 * @return String datatables渲染结果集,JSON格式的字符串 * * 思路: * 1、解析参数信息(json格式) * 2、封装查询条件 * 3、封装datatables分页、排序 * 4、返回渲染datatables的数据 * */ public static String dataTableInfo(String aoData,T vo, IService serviceImpl,Map propertyMap) { /* * 1、获取DataTables所有参数信息,并转换为Map * */ //jackson json解析对象 ObjectMapper om = new ObjectMapper(); //封装DataTables参数信息的Map容器 Map tableDataMap = new HashMap(); try { @SuppressWarnings("unchecked") List> objLst = om.readValue(aoData, List.class); for (Map obj : objLst) { tableDataMap.put(obj.get("name").toString(), obj.get("value")); } }catch(IOException e) { throw new RuntimeException("解析json数据错误:" + e.getMessage()); } /* * 2、设置查询条件值 * */ //获取查询条件信息 Object qw = tableDataMap.get(QUERY_CONDITION); QueryWrapper queryWrapper = null; if(null != qw) { queryWrapper = new QueryWrapper(); @SuppressWarnings("unchecked") Map conditionMap = (Map)qw; //查询条件键值的Set容器 Set propSet = conditionMap.keySet(); for (String prop : propSet) { //字段查找与模糊查找,(默认所有字段都是精确查询,勾选后为模糊查询) if(Reflections.getAccessibleField(vo,prop) != null ) { if(conditionMap.get(prop) != null && StringUtils.isNotBlank(conditionMap.get(prop).toString()) ) { String c = prop; if(isTableFiled(vo,prop)) { c = BeanTableStringUtil.toSeparatorLower(prop); }else { if(null != propertyMap) { c = propertyMap.get(c); if(c == null) { throw new IllegalArgumentException("请配置属性 "+ prop +" 所对应的列名"); } } } //该字段是否为模糊查找? if(conditionMap.get(prop + ACCURATE_SUFFIX) != null && StringUtils.equals("1", conditionMap.get(prop + ACCURATE_SUFFIX).toString())) { queryWrapper.like(c, conditionMap.get(prop)); }else { queryWrapper.eq(c, conditionMap.get(prop)); } } } //时间段判断 //大于等于开始时间 boolean beginTimeFill = StringUtils.endsWith(prop, TIME_BEGIN_SUFFIX) && conditionMap.get(prop) != null && StringUtils.isNotBlank(conditionMap.get(prop).toString()); if(beginTimeFill) { queryWrapper.ge(BeanTableStringUtil.toSeparatorLower(StringUtils.substringBefore(prop, TIME_BEGIN_SUFFIX)), dateMinTime(conditionMap.get(prop).toString())); } //小于结束时间 boolean endTimeFill = StringUtils.endsWith(prop, TIME_END_SUFFIX) && conditionMap.get(prop) != null && StringUtils.isNotBlank(conditionMap.get(prop).toString()); if(endTimeFill) { queryWrapper.lt(BeanTableStringUtil.toSeparatorLower(StringUtils.substringBefore(prop, TIME_END_SUFFIX)), dateMaxTime(conditionMap.get(prop).toString())); } } } //3、设置分页、排序信息 Page page = new Page(); //计算每页显示行数,当前页 int iDisplayStart = Integer.parseInt(tableDataMap.get(I_DISPLAY_START).toString()); int iDisplayLength = Integer.parseInt(tableDataMap.get(I_DISPLAY_LENGTH).toString()); int size = iDisplayLength; int current = iDisplayStart/size + 1; page.setSize(size); page.setCurrent(current); //处理排序 String sSortDir_0 = null; //升序、降序值 String iSortCol_0 = null; //排序列下标 String orderCol = null; //客户点击的排序列 if(tableDataMap.get(S_SORT_DIR_0) != null && tableDataMap.get(I_SORT_COL_0) != null) { sSortDir_0 = tableDataMap.get(S_SORT_DIR_0).toString(); iSortCol_0 = tableDataMap.get(I_SORT_COL_0).toString(); } if(StringUtils.isNotEmpty(sSortDir_0) && StringUtils.isNotEmpty(iSortCol_0)) { orderCol = tableDataMap.get(M_DATA_PROP_ + iSortCol_0).toString(); if(isTableFiled(vo,orderCol)) { //将属性转成表列,并装入排序信息 orderCol = BeanTableStringUtil.toSeparatorLower(orderCol, "_"); } if(StringUtils.equalsIgnoreCase(ASC, sSortDir_0)) { page.addOrder(OrderItem.asc(orderCol)); } if(StringUtils.equalsIgnoreCase(DESC, sSortDir_0)) { page.addOrder(OrderItem.desc(orderCol)); } } //4、组织查询结果 IPage pageT = serviceImpl.page(page, queryWrapper); List records = pageT.getRecords(); String recordsJson = null; try { recordsJson = om.writeValueAsString(records); } catch (JsonProcessingException e) { throw new RuntimeException("json处理异常:" + e.getMessage()); } return tojson(recordsJson, tableDataMap.get("sEcho").toString(), pageT.getTotal()); } /* * 判断当前排序字段是否为是否为表中存在的字段 * 思路,通过字段中的@TableField是否设置exist=false判断 * */ private static boolean isTableFiled(Object vo,String orderCol) { Field f = Reflections.getAccessibleField(vo,orderCol); if(null != f) { Annotation[] annoArr = f.getAnnotations(); for (Annotation annotation : annoArr) { if(StringUtils.contains(annotation.toString(), "TableField")) { if(StringUtils.contains(annotation.toString(), "exist=false")) { return false; } } } } return true; } private static String tojson(String json, String sEcho, long count) { /*json = "{\"sEcho\":" + sEcho + ",\"iTotalRecords\":" + count + ",\"iTotalDisplayRecords\":" + count + ",\"aaData\":" + json + "}"; return json;*/ StringBuilder sb = new StringBuilder(); sb.append("{\"sEcho\":"); sb.append(sEcho); sb.append(",\"iTotalRecords\":"); sb.append(count); sb.append(",\"iTotalDisplayRecords\":"); sb.append(count); sb.append(",\"aaData\":"); sb.append(json); sb.append("}"); return sb.toString(); } private static LocalDateTime dateMinTime(String date_str) { return LocalDateTime.of(LocalDate.parse(date_str), LocalTime.MIN); } private static LocalDateTime dateMaxTime(String date_str) { return LocalDateTime.of(LocalDate.parse(date_str), LocalTime.MAX); } }

里面又抽取出了反射工具类Reflections与表列名与java属性互转类BeanTableStringUtil,代码如下:

package com.ruiyi.common.util;

import org.apache.commons.lang3.StringUtils;

/**
* 

Title:javaBean属性与表列之间字符串转换处理

*

Description: * 注意事项: * 使用org.apache.commons.lang3.StringUtils中的split方法来分割 * 可以避免界定符是特殊字符时不能达到分割效果的问题 *

* @user yangtao * @date 2019年1月5日 * @版本 1.00 * @修改记录 *
* 版本           修改人          修改时间                    修改内容描述
* ----------------------------------------
* 1.00 yangtao  2019年1月5日     初始化版本
* ----------------------------------------
* 
*/ public class BeanTableStringUtil{ //不实例化,只提供静态方法 private BeanTableStringUtil() {} /** * 根据有界定符的字符串转换成小驼峰 * 例如: * MEAL_TYPE => mealType * meal_type => mealType * @param String 转换的字符串 * @param separatorChar 界定符 * */ public static String toSmallHump(String source,String separatorChar) { String bigHump = toBigbHump(source,separatorChar); return String.valueOf(bigHump.charAt(0)).toLowerCase() + StringUtils.substring(bigHump, 1); } /** * 根据有界定符的字符串转换成大驼峰 * 例如: * MEAL_TYPE => MealType * meal_type => MealType * @param String 转换的字符串 * @param separatorChar 界定符 * */ public static String toBigbHump(String source,String separatorChar) { //如果没有包含指定的界定符,就返回默认的大驼峰 if(StringUtils.isEmpty(separatorChar) || !StringUtils.contains(source, separatorChar)) { return String.valueOf(source.charAt(0)).toUpperCase() + StringUtils.substring(source, 1).toLowerCase(); } String[] sourceArr = StringUtils.split(source, separatorChar); StringBuilder sb = new StringBuilder(); String temp = null; for (String str : sourceArr) { temp = String.valueOf(str.charAt(0)).toUpperCase() + StringUtils.substring(str, 1).toLowerCase(); sb.append(temp); } return sb.toString(); } /** * 根据驼峰字符串转换成界定的小写字符串 * 例如: * helloWorld => hello_world * HelloWorld => hello_world * @param String 转换的字符串 * @param separatorChar 界定符 * */ public static String toSeparatorLower(String source,String separatorChar) { if(StringUtils.isEmpty(source)) { return ""; } int len = source.length(); StringBuilder sb = new StringBuilder(len); char c; for (int i = 0; i < len; i++) { c = source.charAt(i); if (Character.isUpperCase(c)) { if(i > 0) { sb.append(separatorChar); } sb.append(Character.toLowerCase(c)); } else { sb.append(c); } } return sb.toString(); } public static String toSeparatorLower(String source) { if(StringUtils.isEmpty(source)) { throw new NullPointerException("被转换字符为null或者为空串"); } source = source.trim(); if(StringUtils.isEmpty(source)) { throw new IllegalArgumentException("参数不能为空格"); } int len = source.length(); StringBuilder sb = new StringBuilder(len); char c; for (int i = 0; i < len; i++) { c = source.charAt(i); if (Character.isUpperCase(c)) { if(i > 0) { sb.append("_"); } sb.append(Character.toLowerCase(c)); } else { sb.append(c); } } return sb.toString(); } /** * 根据驼峰字符串转换成界定的大写字符串 * 例如: * helloWorld => HELLO_WORLD * HelloWorld => HELLO_WORLD * @param String 转换的字符串 * @param separatorChar 界定符 * */ public static String toSeparatorUpper(String source,String separatorChar) { return toSeparatorLower(source,separatorChar).toUpperCase(); } }
package com.ruiyi.common.util;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

/**
 * 
* 

反射工具类.

*

提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数

* @作者 yangtao * @创建时间 2014年11月1日 * @版本 1.00 * @修改记录 *
* 版本   修改人    修改时间        修改内容描述
* ----------------------------------------
* 1.00   yangtao  2014年11月1日   初始化版本
* ----------------------------------------
* 
* */ @SuppressWarnings("rawtypes") public class Reflections { private static final String SETTER_PREFIX = "set"; private static final String GETTER_PREFIX = "get"; private static final String CGLIB_CLASS_SEPARATOR = "$$"; private static Logger logger = LoggerFactory.getLogger(Reflections.class); /** * 调用Getter方法. * 支持多级,如:对象名.对象名.方法 */ public static Object invokeGetter(Object obj, String propertyName) { Object object = obj; for (String name : StringUtils.split(propertyName, ".")){ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); } return object; } /** * 调用Setter方法, 仅匹配方法名。 * 支持多级,如:对象名.对象名.方法 */ public static void invokeSetter(Object obj, String propertyName, Object value) { Object object = obj; String[] names = StringUtils.split(propertyName, "."); for (int i=0; i[] parameterTypes, final Object[] args) { Method method = getAccessibleMethod(obj, methodName, parameterTypes); if (method == null) { throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + obj + "]"); } try { return method.invoke(obj, args); } catch (Exception e) { throw convertReflectionExceptionToUnchecked(e); } } /** * 直接调用对象方法, 无视private/protected修饰符, * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. * 只匹配函数名,如果有多个同名函数调用第一个。 */ public static Object invokeMethodByName(final Object obj, final String methodName, final Object[] args) { Method method = getAccessibleMethodByName(obj, methodName); if (method == null) { throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + obj + "]"); } try { return method.invoke(obj, args); } catch (Exception e) { throw convertReflectionExceptionToUnchecked(e); } } /** * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. * * 如向上转型到Object仍无法找到, 返回null. */ public static Field getAccessibleField(final Object obj, final String fieldName) { Validate.notNull(obj, "object can't be null"); Validate.notBlank(fieldName, "fieldName can't be blank"); for (Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) { try { Field field = superClass.getDeclaredField(fieldName); makeAccessible(field); return field; } catch (NoSuchFieldException e) {//NOSONAR // Field不在当前类定义,继续向上转型 continue;// new add } } return null; } /** * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. * 如向上转型到Object仍无法找到, 返回null. * 匹配函数名+参数类型。 * * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) */ public static Method getAccessibleMethod(final Object obj, final String methodName, final Class... parameterTypes) { Validate.notNull(obj, "object can't be null"); Validate.notBlank(methodName, "methodName can't be blank"); for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) { try { Method method = searchType.getDeclaredMethod(methodName, parameterTypes); makeAccessible(method); return method; } catch (NoSuchMethodException e) { // Method不在当前类定义,继续向上转型 continue;// new add } } return null; } /** * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. * 如向上转型到Object仍无法找到, 返回null. * 只匹配函数名。 * * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) */ public static Method getAccessibleMethodByName(final Object obj, final String methodName) { Validate.notNull(obj, "object can't be null"); Validate.notBlank(methodName, "methodName can't be blank"); for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) { Method[] methods = searchType.getDeclaredMethods(); for (Method method : methods) { if (method.getName().equals(methodName)) { makeAccessible(method); return method; } } } return null; } /** * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 */ public static void makeAccessible(Method method) { if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !method.isAccessible()) { method.setAccessible(true); } } /** * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 */ public static void makeAccessible(Field field) { if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || Modifier .isFinal(field.getModifiers())) && !field.isAccessible()) { field.setAccessible(true); } } /** * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 * 如无法找到, 返回Object.class. * eg. * public UserDao extends HibernateDao * * @param clazz The class to introspect * @return the first generic declaration, or Object.class if cannot be determined */ @SuppressWarnings("unchecked") public static Class getClassGenricType(final Class clazz) { return getClassGenricType(clazz, 0); } /** * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. * 如无法找到, 返回Object.class. * * 如public UserDao extends HibernateDao * * @param clazz clazz The class to introspect * @param index the Index of the generic ddeclaration,start from 0. * @return the index generic declaration, or Object.class if cannot be determined */ public static Class getClassGenricType(final Class clazz, final int index) { Type genType = clazz.getGenericSuperclass(); if (!(genType instanceof ParameterizedType)) { logger.warn(clazz.getSimpleName() + "'s superclass not ParameterizedType"); return Object.class; } Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); if (index >= params.length || index < 0) { logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length); return Object.class; } if (!(params[index] instanceof Class)) { logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); return Object.class; } return (Class) params[index]; } public static Class getUserClass(Object instance) { Assert.notNull(instance, "Instance must not be null"); Class clazz = instance.getClass(); if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { Class superClass = clazz.getSuperclass(); if (superClass != null && !Object.class.equals(superClass)) { return superClass; } } return clazz; } /** * 将反射时的checked exception转换为unchecked exception. */ public static RuntimeException convertReflectionExceptionToUnchecked(Exception e) { if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException || e instanceof NoSuchMethodException) { return new IllegalArgumentException(e); } else if (e instanceof InvocationTargetException) { return new RuntimeException(((InvocationTargetException) e).getTargetException()); } else if (e instanceof RuntimeException) { return (RuntimeException) e; } return new RuntimeException("Unexpected Checked Exception.", e); } }

接下来,我们删除service层中对datatables的具体实现,在controller层调用dataTableInfo并测试下就可以了。如:

/**
 * 公告列表数据
 */
@PostMapping("/noticeListData")
@ResponseBody
public String noticeListData(String aoData,Notice vo) {
    return DataTablesUtil.dataTableInfo(aoData, vo, noticeServiceImpl);
}

当然,代码的重构是无止境的,这里只是重构到我们目前适用的地步。对于DatatablesUtil中的不清楚地方,或者有更好的建议的优化,欢迎留言,大家共同进步。下一篇我们来集成下Ueditor,我们下篇见。

你可能感兴趣的:(springboot,datatables,H-ui,java,mysql,spring)