基础工具集
这里介绍一下一些基础工具集的使用。
主要包含在 wmeimob-fastboot-utils
jar 中。
请求日志记录
通过启用注解 @EnableRestfulApiLog
,可以实现对 controller 下所有 action 请求进行记录,如果想忽略某个 action,则可以使用 @IgnoreRestfulApiLog
注解。
xss 防御方案
框架支持两种模式:
- 对非法脚本进行编码
例如 < 符号将被编码为 <
- 剔除非法脚本(基于 jsoup 实现),默认实现。
支持的完整配置如下:
wmeimob:
xss:
exclude-path-patterns:
- /logout #放行 url
mode: jsoup #模式
tags: #自定义放行标签
- hr
- div:data-w-e-type
- div:data-w-e-is-void
bean 属性复制
推荐使用BeanCopierUtil
类,后续部署需要手动增加启动参数-noverify
。
其中 list copy 推荐使用 copyLists 方法,可保证集合不会出现 null 问题。更好的方式推荐使用 MapStruct。
枚举扩展
对于枚举需要扩展 message,code 的需求,可以实现EnumValue
接口。这方便在swagger中集成显示枚举注释。
为了方便前端对接后端定义的枚举内容,对实现了EnumValue
接口的枚举,后端通过 @EnableEnumRegister
注解来启用枚举扫描注册,通过 controller 暴露对应的枚举接口供前端使用。
/**
* 通用枚举控制器
*
* @author mjyang
* @date 2022/3/30 23:06
*/
@Api(tags = "枚举")
@RestController
@RequestMapping("/api/enum")
public class EnumController {
@Resource
private CommonEnumRegistry commonEnumRegistry;
@GetMapping
@ApiOperation(value = "获取所有枚举配置")
public JsonResult<Map<String, List<PublicKeyValueDto>>> list() {
Map<String, List<EnumValue>> dict = this.commonEnumRegistry.getNameDict();
Map<String, List<PublicKeyValueDto>> dictVo = MapUtil.newHashMap(dict.size());
for (Map.Entry<String, List<EnumValue>> entry : dict.entrySet()) {
List<EnumValue> value = entry.getValue();
dictVo.put(entry.getKey(), value
.stream()
.map(x -> {
return new PublicKeyValueDto(x.getKey(), x.getValue().toString());
}).collect(Collectors.toList()));
}
return JsonResult.ok(dictVo);
}
@GetMapping("types")
@ApiOperation(value = "获取所有枚举配置类型名称")
public JsonResult<List<String>> types() {
Map<String, List<EnumValue>> dict = this.commonEnumRegistry.getNameDict();
return JsonResult.ok(new ArrayList<>(dict.keySet()));
}
@GetMapping("{type}")
@ApiOperation(value = "根据枚举配置类型名称获取枚举信息")
public JsonResult<List<PublicKeyValueDto>> getByType(@PathVariable("type") String type) {
Map<String, List<EnumValue>> dict = this.commonEnumRegistry.getNameDict();
List<EnumValue> commonEnums = Optional.ofNullable(dict.get(type)).orElseGet(ListUtil::empty);
return JsonResult.ok(
commonEnums
.stream()
.map(x -> {
return new PublicKeyValueDto(x.getKey(), x.getValue().toString());
}).collect(Collectors.toList())
);
}
spring validate 验证注解
NotEmptyLongListValidator list 非默认值(某个给定值)验证
RangeExtend 枚举扩展验证
Mobile 手机号验证
IdCard 身份证验证
StringSpiltValidator 字符分隔符验证
验证分组
- insert
- update
查看所有支持的内置注解
分页基础模型
有分页需求的可以统一继承BaseDTO
类。里面包含一些常用的分页检索参数。标品里面分页分为两种:
/**
* 普通查询输出参数
*
* @author mjyang
* @date 2021/7/6
*/
@Getter
@RequiredArgsConstructor
public class PagedResult<T> extends BasePageResult{
private Long total;
private List<T> list;
public static <T> PagedResult<T> of(Long total, List<T> list) {
return new PagedResult<>(total, list);
}
public static <T> PagedResult<T> empty() {
return of(null, ListUtil.empty());
}
public static <T> Page<T> startPage(int pageNum, int pageSize) {
return PageHelper.startPage(pageNum, pageSize);
}
public static <T> Page<T> startPage(Object obj) {
if (obj instanceof BaseDTO) {
BaseDTO pageDto = (BaseDTO) obj;
return startPage(pageDto.getPageNum(), pageDto.getPageSize());
}
throw SimpleException.getInstance("模型是否未继承自 BaseDTO ?");
}
}
/**
* 普通查询输出参数
*
* @author mjyang
* @date 2021/7/6
*/
@Getter
@Setter
public class PagedResult<T> implements Serializable {
private static final long serialVersionUID = 568969944565726191L;
private Long total;
private List<T> list;
}
/**
* 滚动查询输出参数
*
* @author mjyang
* @date 2021/7/6
*/
public class PagedScrollResult<T> extends BasePageResult{
@Getter
private final List<T> list;
@Getter
private Long nextToken;
private Boolean isLastPage;
private PagedScrollResult(List<T> list) {
if (CollectionUtil.isNotEmpty(list)) {
int size = list.size();
T obj = list.get(size - 1);
this.nextToken = obj.getId();
this.isLastPage = ObjUtil.isNull(nextToken);
}
this.list = list;
}
public static <T extends NextToken> PagedScrollResult<T> of(List<T> list) {
return new PagedScrollResult<>(list);
}
public static <T extends NextToken> PagedScrollResult<T> empty() {
return new PagedScrollResult<>(ListUtil.empty());
}
public static <T> Page<T> scrollPage(int pageNum, int pageSize) {
return PageHelper.startPage(pageNum, pageSize, false);
}
public static <T> Page<T> scrollPage(Object obj) {
if (obj instanceof BaseDTO) {
BaseDTO pageDto = (BaseDTO) obj;
return scrollPage(pageDto.getPageNum(), pageDto.getPageSize());
}
throw SimpleException.getInstance("模型是否未继承自 BaseDTO ?");
}
public Boolean getIsLastPage() {
return this.isLastPage;
}
}
/**
* 滚动查询输出参数
*
* @author mjyang
* @date 2021/7/6
*/
@Getter
@Setter
public class PagedScrollResult<T extends NextToken> implements Serializable {
private static final long serialVersionUID = 568969944565726191L;
private List<T> list;
private Long nextToken;
private Boolean isLastPage;
public Boolean getIsLastPage() {
return this.isLastPage;
}
}
/**
* 基础分页
*
* @author mjyang
* @date 2021/7/6
*/
@RequiredArgsConstructor
public abstract class BasePageResult {
/**
* 追加分页 limit 1 1,不统计 count
*/
public static void startPageByFirstOnly() {
startPageByFirstOnly(1);
}
/**
* 追加分页 limit 1 pageSize,不统计 count
*/
public static void startPageByFirstOnly(int pageSize) {
PageHelper.startPage(1, pageSize, false);
}
}
/**
* page 工具类
*
* @author mjyang
* @date 2024/6/3 21:30
*/
@UtilityClass
public class PageUtil {
public static <T> PagedResult<T> of(Long total, List<T> list) {
PagedResult<T> pagedResult = new PagedResult<>();
pagedResult.setTotal(total);
pagedResult.setList(list);
return pagedResult;
}
public static <T> PagedResult<T> empty() {
return of(null, ListUtil.empty());
}
public static <T extends NextToken> PagedScrollResult<T> ofScroll(Integer pageSize, List<T> list) {
PagedScrollResult<T> pagedScrollResult = new PagedScrollResult<>();
if (CollectionUtil.isNotEmpty(list)) {
int size = list.size();
Boolean isLastPage;
if (size < pageSize) {
isLastPage = Boolean.TRUE;
} else {
isLastPage = Boolean.FALSE;
}
T obj = list.get(size - 1);
pagedScrollResult.setNextToken(obj.getId());
pagedScrollResult.setIsLastPage(isLastPage);
} else {
pagedScrollResult.setIsLastPage(Boolean.TRUE);
}
pagedScrollResult.setList(list);
return pagedScrollResult;
}
public static <T extends NextToken> PagedScrollResult<T> emptyScroll() {
return new PagedScrollResult<>();
}
public static <T> Page<T> startPage(int pageNum, int pageSize) {
return PageHelper.startPage(pageNum, pageSize);
}
public static <T> Page<T> startPage(Object obj) {
if (obj instanceof BaseDTO) {
BaseDTO pageDto = (BaseDTO) obj;
return startPage(pageDto.getPageNum(), pageDto.getPageSize());
}
throw SimpleException.getInstance("模型是否未继承自 BaseDTO ?");
}
public static <T> Page<T> scrollPage(int pageNum, int pageSize) {
return PageHelper.startPage(pageNum, pageSize, false);
}
public static <T> Page<T> scrollPage(Object obj) {
if (obj instanceof BaseDTO) {
BaseDTO pageDto = (BaseDTO) obj;
return scrollPage(pageDto.getPageNum(), pageDto.getPageSize());
}
throw SimpleException.getInstance("模型是否未继承自 BaseDTO ?");
}
/**
* 追加分页 limit 1 1,不统计 count
*/
public static void startPageByFirstOnly() {
startPageByFirstOnly(1);
}
/**
* 追加分页 limit 1 pageSize,不统计 count
*/
public static void startPageByFirstOnly(int pageSize) {
PageHelper.startPage(1, pageSize, false);
}
}
滚动分页的实现上是不计算 count,且再实现上做了取巧的实现,大数据量场景下会有深度分页问题,因此在模型里面有一个 nextToken 参数,联同分页结果一起给到前端,但同时需要前端在分页时带上该参数作为请求参数,后端查询时使用此参数做为查询条件 where id > nextToken。这样可以解决深度分页问题。默认情况下,nextToken 为主键 id 值,默认有序。BaseDTO 默认接收 nextToken 参数。
为什么 2.5.9.2 会出现两种不同的 pageResult 与 pageScrollResult 呢?这是因为当使用 dubbo 时,模型需要序列化,而 2.5.9.1 版本代码难以达到这个目的,因此再 2.5.9.2 里重新重构出 pageUtil 工具类来做分页需求。
数据脱敏
基于 jackson 序列化实现。目前支持的脱敏规则有:
- 手机号
- 姓名
- 详细地址
为了简化使用提供 @DataMasking 注解,使用如下:
@ApiModelProperty("用户手机号")
@DataMasking(rule = DataMaskingRule.MOBILE)
private String mobile;
excel 工具类
基于 easypoi 实现。提供 EasyPoiUtil 工具类。
ip 工具类
IpUtils
web request 工具类
WebServletUtil
- 常用正则工具类
- cn.hutool.core.lang.Validator
- cn.hutool.core.lang.PatternPool
其他工具类需求
原则上一律使用 hutool 类库。当前框架版本 5.8.20。
常见问题
BeanCopierUtil
与accessor
注解问题
当类上使用accessor
注解时,它会帮助你实现链式调用。但当你使用BeanCopierUtil
进行属性复制时,会出现属性一直为 null 的问题。这是因为它的底层进行属性 set 时,判断 set 方法必须为 void,而当使用accessor
注解时,set 方法返回类型不是 void。如果要规避这个问题,只能是去掉accessor
注解。