在上一篇博文中我们简单介绍了下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类(重点)
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对应的属性名。这里就不放自己的查询结果了,自己设计数据测试就可以。
在使用分页查询的时候,需要先引入分页插件PaginationInterceptor,具体的使用方法是
写到这里,大家就可以直接使用了,在使用之前,我们先将代码重构一下,因为我们不能把所有的配置都放置在启动类当中来。当后续的配置越来越多的时候,不利于我们的开发与维护。那么我们将mp的配置放置到单独的类当中去。如图:
这里我就直接沿用官网的代码,具体的设置,按里面的注释去测试就可以了:
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;
}
}
至于翻页当中的参数意义,我们可以来看看源码,这里就复制一段属性与构造函数的代码说明
/**
* 查询数据列表
*/
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;
}
我们已经能够拿到条件查询的分页数据了,那么剩下的事情就是将得到的结果渲染出来,因为我们一直在使用H-ui作为我们的后台模板,所以数据的渲染就不像是直接拿数据在页面中用thymeleaf的语法了。而是将所得到的数据转换成json,并交给datatables渲染。这里我画两张图来说明下。
这样的话,我们可以很清淅的来编写controller层了。在看代码之前,我们先对我们的项目结构做一些调整。这样的修改其实是为了方便配合后来mp的代码生成器。对于项目结构,按自己习惯了的命名做就行,触类旁通,可以少走一些的弯路。
上面的项目结构,是根据”模块——>功能“来划分的。这里在resources中加了mapper目录,里面存放sql的映射文件(这类文件一般是在mp中遇上复杂查询,自定义sql的时候才用)。当我们修改了项目结构后,mp对应的扫描配置也要做相关的修改,如图:
而在配置文件中,目前我们是用开发版的yml
修改完成后,不妨先测试一下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";
}
}
我们先写跳转页面。在H-ui中选一个含有datatables的简单页面,这里我套用会员列表页面member-list.html。我们先看下结构。
再将member-list.html中的代码copy到noticeList.html中,并引入之前封装好的公共静态资源代替写死的资源,如下图:
然后在index.html页面加入当前页面的controller链接
我们启动服务器,并链接查看,我们会发现后台报了一个错误:
仔细阅读后,会发现,是thymeleaf的解析错误,为什么呢?原来[[......]]之间的内容会被thymeleaf认为是内联表达式,所以它会按thymeleaf的语法去解析。显然,datatables中的这段代码——"aaSorting": [[ 1, "desc" ]],,thymeleaf解析不了。我们只要在两个中括号之间加上空格就可以了。如:"aaSorting": [ [ 1, "desc" ] ],修改后我们再访问一下,这个静态页面就成功导入了。如图:
再修改页面上的信息为公告相关的内容就可以。这里放一下页面代码:
我们看到的页面就是如下图片:
H-ui里面用到的时间插件是WdatePicker,只要按所在的目录引入就可以。它的表达式,也只要查看官方的api即可。我们在接下来的博文会来说明。
接下来的逻辑到了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的配置已经成功了。
我们在上一小节对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
上面的代码有些多,这里梳理下思路:
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时所对应的值,如图中椭圆所标识出来的。
我们这里是按属性来的,所以写了个简单的方法toSeparatorLower,将类的属性转成表的列名
bSortable_x:x下标对应的列是否允许点击排序,如上图中方框所标识出来的。在展示的地方也就是:
4、封装返回的数据结果,我们同样只要了解data所需要的参数的意义就可以了
{
"sEcho":1,//每次请求都不同,用来区分操作的
"iTotalRecords":100,//实际的总数量
"iTotalDisplayRecords":100,//过滤之后的数量,我们不用datatables的假翻页的话,它和iTotalRecords等价
"aaData":'.....',//当前页的所有结果集
...
}
如果不出意外的话,就可以出结果了:
大家可以调小下每页显示数量的值,测试下分页;并测试下条件与排序。
所有的代码,都存在重构的空间,尤其是java。大家在完成了Notice的渲染datatables功能后,建议再多做几张表的datatables功能,并找找它们的一些共同点,尤其是做下多表关联查询。我再来看这节内容。
当我们多写了几遍类似的代码后,大家会提出一个疑问,这些重复的工作量,怎么把它们优雅地抽取出来?我们先来看看我们用到的mybatis-plus的类结构:
看到这里,大家会也许会想到, 我们写一个公共的类,假设取名叫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
里面又抽取出了反射工具类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,我们下篇见。