近期在工作中遇到一个功能需求,基于类以及属性集的数据过滤,功能比较简单,但是的确花费了我一天的工作量来完成该功能,在开发这个功能过程中,我觉的有些问题的思考和处理方式觉得很有帮助,所以整理如下的博文,以便自己以后遇到类似的需求能快速的解决
需求如下:
需要开发一个功能,能维护系统每个业务单据的数据项,每个业务单据的数据项是可以选择是否展示数据值,然后再系统展示的时候,根据已经维护好的数据项的单据,过滤其中不需要显示数据值的
需求很简单,现在说明下代码结构:
- 每个业务单据有DTO对象和Entity对象,其中DTO对象是显示给前端界面显示用的,Entity对象是单据对应的数据库模型
- 一个单据DTO对象可以有属性集List,表示父子级属性,Entity对象中不存在属性集
- DTO对象和Entity对象属性名称相同(DTO比Entity多)
需求扩展说明:
- 根据需求,参数传递过来的是DTO对象,然后规则里面保存的Entity对象的属性全路径(包括属性集中的属性全路径)
- 需要根据Entity属性全路径的过滤规则来过滤DTO中的数据项的数据值
- 一个DTO对象可能含有多个属性集对象,属性集对象中的DTO也可能含有多个属性集对象
问题思考点:
-
因为传递的参数的DTO对象,而规则里面对应的都是Entity中的属性全路径,我们怎么去匹配他们之间的对应关系?
我们需要建立一个映射关系,来获取DTO属性与Entity属性之间对应关系,这里需要注意的是DTO中有属性集List,映射关系也需要维护属性集的DTO和Entity对应的关系,
我们在根据Entity过滤规则来过滤时,其实也就是根据映射关系中的DTO的属性全路径来过滤DTO中的对象(DTO的属性全路径和Entity属性全路径是不一致的,也有人会说直接根据属性字段来匹配,这里需要说明下,由于存在相同的基类,所以不同类的属性名称存在相同的,所以不能单据的根据属性名称来匹配,应该根据属性全路径来匹配)
这里其实还有个问题,就是怎么知道DTO对应的Entity呢?方法很简单,我们可以通过自定义注解来解决,稍后在代码中我们会细细说明
-
需要考虑到过滤的规则里面含有多个属性集的数据项过滤,如何才能过滤属性集中的数据项呢?
这是个难点,我们来分析下,最顶级的是一个DTO对象,然后DTO对象中存在属性集,每个属性集对象中也可能含有属性集,这里我们需要建议一个模型结构:
- 该模型结构能知道当前的属性集对应的对象class信息,该class是为了能够知道该层级所有的属性字段,
- 模型结构需要存储当前节点的所有的属性集,为了迭代遍历
- 这个数据模型必须要符合树形节点结构,并且支持很好的遍历
- 再添加我们的业务逻辑,每个节点的层级需要知道该层级中哪些字段属性是需要过滤的
开发代码逻辑:
分析了上述的模型结构,可以整理出一下的开发代码逻辑:
- 我们需要获取DTO对象和Entity对象之间的映射关系
- 根据初始对象和过滤的规则字段,我们需要构建出模型结构
- 采用迭代的方式,循环遍历模型结构,依据当前节点的过滤属性来将该节点的数据项赋值为空
- 返回DTO对象
代码实现
- 首先我们先定义好自定义注解,为了获取DTO对象的Entity对象Class信息
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: AmMapping
* @Package: com.amos.stuff.filter.anno
* @author: zhuqb
* @Description: DTO映射Entity对象
* <p>
* 该属性添加在类或者属性上,通过该注解可以知道DTO对应的Entity对象
* @date: 2019/10/9 0009 下午 17:18
* @Version: V1.0
*/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AmMapping {
/**
* 当前DTO对象对应的Entity对象Class信息
*
* @return
*/
Class<?> entity();
}
- 定义好过滤的规则实体和基类
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: FilterRule
* @Package: com.amos.stuff.filter.bean
* @author: zhuqb
* @Description: 过滤规则的对象
* @date: 2019/10/9 0009 下午 17:23
* @Version: V1.0
*/
@Data
public class FilterRule {
/**
* Entity对象属性全路径
*/
private String entityFieldFullName;
/**
* 是否显示 true 是 false 否
*/
private Boolean show;
}
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: BaseDTO
* @Package: com.amos.stuff.filter.bean
* @author: zhuqb
* @Description: 数据模型的基类 这里作为泛型约束
* @date: 2019/10/9 0009 下午 17:27
* @Version: V1.0
*/
public class BaseDTO {
}
- 数据过滤处理器,代码中已经添加说明
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: DataFilterHandler
* @Package: com.amos.stuff.filter.handler
* @author: zhuqb
* @Description: 数据过滤处理器
* @date: 2019/10/9 0009 下午 17:26
* @Version: V1.0
*/
public class DataFilterHandler<T extends BaseDTO> {
public final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 属性全路径默认分隔符
*/
public static final String DEFAULT_SPLIT_STR = ".";
public static final int DEFAULT_MAP_LENGTH = 100;
/**
* 泛型为 BaseDTO 的实体类对象
*/
private T dto;
/**
* 实体类对象的 Class
*/
private Class clazz;
/**
* DTO中与Entity中字段的映射关系
* Key 是DTO中的属性全称
* Value 是Entity中与DTO匹配上的属性的全称
* <p>
* 该映射包括子集的属性列表
*/
private Map<String, String> mapping = new ConcurrentHashMap<>(DEFAULT_MAP_LENGTH);
/**
* 需要过滤的字段列表
*/
private List<FilterRule> filterList;
/**
* 模型结构
*/
private MockNode node = null;
/**
* 构造器初始化
*
* @param dto
* @param filterList
*/
public DataFilterHandler(T dto, List<FilterRule> filterList) {
this.dto = dto;
this.clazz = dto.getClass();
this.filterList = filterList;
// 获取DTO与Entity之间的映射关系
this.mapping();
this.node = this.initMockNode(null, this.clazz);
this.logger.info("组装之后的Node节点数据:{}", JSONObject.toJSONString(this.node));
}
/**
* 初始化节点数据
* <p>
* 该方法主要是构建实体对象中的Node节点数据
* 将实体类中含有需要过滤的属性集数据解析组装成固有数据结构的Node节点
*
* @param pre 上一个节点的对象
* @param cls 当前节点实体Class
* @return
*/
private MockNode initMockNode(MockNode pre, Class<?> cls) {
MockNode node = new MockNode();
// 前一个节点 对于root节点来说,前一个节点为空
node.setPre(pre);
// 本节点的class类型 对于属性集来说,class类型是属性集的泛型
// 由于这里通过 注解 @RxPrintBean 来判断是否需要过滤的,需要属性集中的泛型对应的DTO对象也需要添加相应的注解
node.setClazz(cls);
// 获取本节点对象的所有的Field
List<Field> fields = FieldUtils.getAllFieldsList(cls);
List<MockNode> nexts = new ArrayList<>();
List<String> noShowFields = new ArrayList<>();
for (Field field : fields) {
field.setAccessible(Boolean.TRUE);
if (field.getType().isAssignableFrom(List.class) && field.isAnnotationPresent(AmMapping.class)) {
this.logger.info("属性集Node:{}", field.getName());
try {
Class<?> lClazz = ClassUtils.getFieldType(field);
nexts.add(this.initMockNode(node, lClazz));
} catch (Exception e) {
e.printStackTrace();
}
} else {
// 属性全称
String fieldName = cls.getName() + DEFAULT_SPLIT_STR + field.getName();
// 如果 过滤列表中属性全称和 映射关系中的 Entity 属性全称相同
// 并且 od是否发送为不发送
for (FilterRule od : this.filterList) {
for (Map.Entry<String, String> entry : this.mapping.entrySet()) {
if (entry.getValue().equals(od.getEntityFieldFullName())
&& !od.getShow()
&& fieldName.equals(entry.getKey())) {
noShowFields.add(fieldName);
}
}
}
}
}
node.setNexts(nexts);
node.setNoShowFields(noShowFields);
return node;
}
/**
* 获取映射关系
*/
private void mapping() {
try {
this.logger.info("开始组装DTO与Entity之间的映射关系");
this.iteratorClass(this.clazz);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
/**
* 循环迭代生成DTO对象与Entity对象属性对应的映射关系
*
* @param dtoClazz DTO 对象class
*/
public void iteratorClass(Class dtoClazz) {
/**
* 如果实体类中没有 RxPrintBean的注解 则直接返回
*/
if (!this.clazz.isAnnotationPresent(AmMapping.class)) {
return;
}
// 获取类上的 RxPrintBean 的注解
AmMapping rpb = AnnotationUtils.findAnnotation(dtoClazz, AmMapping.class);
if (StringUtils.isEmpty(rpb)) {
throw new RuntimeException(dtoClazz.getName() + "需要添加注解:@AmMapping");
}
Class cls = rpb.entity();
// 获取注解值 指定类的 Field 属性列表
List<Field> srcList = FieldUtils.getAllFieldsList(cls);
// 获取目标的属性列表
List<Field> destList = FieldUtils.getAllFieldsList(dtoClazz);
for (Field field : destList) {
Boolean accessible = field.isAccessible();
field.setAccessible(Boolean.TRUE);
// 循环生成属性列表
if (field.getType().isAssignableFrom(List.class)) {
if (field.isAnnotationPresent(AmMapping.class)) {
// 属性集的泛型
Class<?> lClazz = ClassUtils.getFieldType(field);
if (!StringUtils.isEmpty(lClazz)) {
this.logger.info("实体中的集合:{}", lClazz);
this.iteratorClass(lClazz);
}
}
} else {
// 生成DTO与Entity之间的映射关系
for (Field srcField : srcList) {
if (srcField.getName().equals(field.getName())) {
this.mapping.put(dtoClazz.getName() + DEFAULT_SPLIT_STR + field.getName(), cls.getName() + DEFAULT_SPLIT_STR + srcField.getName());
continue;
}
}
}
field.setAccessible(accessible);
}
}
/**
* 过滤数据项的方法
* 1. 先过滤父类的数据,然后迭代过滤属性集中的数据
*
* @return
*/
public T filter() {
// 如果没有过滤的字段 ,则全部返回
if (CollectionUtils.isEmpty(this.filterList)) {
return this.dto;
}
this.logger.info("过滤之前的数据:{}", JSONObject.toJSONString(this.dto));
try {
this.filterData(this.dto, this.node.getNoShowFields());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
} finally {
this.mapping.clear();
}
this.logger.info("过滤之后的数据:{}", JSONObject.toJSONString(this.dto));
return this.dto;
}
/**
* 过滤数据,主要用来过滤父级的数据,在父级属性过滤的过程中,过滤属性集中的数据
*
* @param dto DTO 对象
* @param nowShowFieldList 不需要展示的DTO属性列表
* @throws Exception
*/
private void filterData(T dto, List<String> nowShowFieldList) throws Exception {
try {
Class<?> dtoClass = dto.getClass();
List<Field> fields = FieldUtils.getAllFieldsList(dtoClass);
String fieldName = null;
for (Field field : fields) {
field.setAccessible(Boolean.TRUE);
fieldName = dtoClass.getName() + DEFAULT_SPLIT_STR + field.getName();
if (nowShowFieldList.contains(fieldName)) {
field.set(dto, null);
}
if (field.getType().isAssignableFrom(List.class) && field.isAnnotationPresent(AmMapping.class)) {
List<T> list = (List<T>) field.get(dto);
this.filterData(this.node, list);
field.set(dto, list);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 迭代循环过滤数据,主要是用来过滤属性集中的数据
*
* @param umn Node节点数据
* @param list 属性集数据
* @throws Exception
*/
private void filterData(MockNode umn, List<T> list) throws Exception {
// 获取下级节点,如果没有下级节点就返回
List<MockNode> nexts = umn.getNexts();
if (CollectionUtils.isEmpty(nexts)) {
return;
}
Class<?> cls = null;
// 这里的处理步骤,循环每个属性节点
// 将每条记录信息里面的所有的属性字段遍历
// 如果该属性字段是不需要显示值的,则将该属性的值置为空
// 查看本级属性有没有是属性集的,如果有的话,则循环迭代继续重复上述的步骤来过滤数据
for (MockNode mockNode : nexts) {
cls = mockNode.getClazz();
this.logger.info("当前节点的Class信息:{}", cls);
// 获取本级不需要显示的字段信息
List<String> noShowFields = mockNode.getNoShowFields();
// 获取本级所有的属性字段信息
List<Field> fields = FieldUtils.getAllFieldsList(cls);
String fieldName = null;
for (T t : list) {
for (Field field : fields) {
fieldName = cls.getName() + DEFAULT_SPLIT_STR + field.getName();
if (noShowFields.contains(fieldName)) {
field.setAccessible(Boolean.TRUE);
field.set(t, null);
}
if (field.getType().isAssignableFrom(List.class) && field.isAnnotationPresent(AmMapping.class)) {
List<T> data = (List<T>) field.get(t);
this.filterData(mockNode, data);
// 过滤后的数据需要重新赋值到原来的属性中
field.set(t, data);
}
}
}
}
}
}
核心的代码我一次性贴了出来,代码不是很难,主要是根据上面的分析的思路来编写代码的,一些比较晦涩的地方,我也都添加详细的注释了,具体的代码可以查看我的Gitee项目:stuff
测试用例
上述核心的代码已经实现了,接下来我们来编写测试用例来看看我们的代码功能是否正常:
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: BizDetailEntity
* @Package: com.amos.stuff.filter.bean.entity
* @author: zhuqb
* @Description:
* @date: 2019/10/10 0010 上午 8:13
* @Version: V1.0
*/
@Data
public class BizDetailEntity {
private String company;
private Date startTime;
private Integer quantum;
private String experience;
}
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: BizEntity
* @Package: com.amos.stuff.filter.bean.entity
* @author: zhuqb
* @Description:
* @date: 2019/10/10 0010 上午 8:09
* @Version: V1.0
*/
@Data
public class BizEntity {
private String name;
private String email;
private Integer age;
private String grade;
}
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: BizDetailDTO
* @Package: com.amos.stuff.filter.bean
* @author: zhuqb
* @Description:
* @date: 2019/10/9 0009 下午 18:00
* @Version: V1.0
*/
@Data
@AmMapping(entity = BizDetailEntity.class)
public class BizDetailDTO extends BaseDTO {
private String company;
private Date startTime;
private Integer quantum;
private String experience;
}
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: BizDTO
* @Package: com.amos.stuff.filter.bean
* @author: zhuqb
* @Description:
* @date: 2019/10/9 0009 下午 17:58
* @Version: V1.0
*/
@Data
@AmMapping(entity = BizEntity.class)
public class BizDTO extends BaseDTO {
private String name;
private String email;
private Integer age;
private String grade;
@AmMapping(entity = BizDetailEntity.class)
private List<BizDetailDTO> bizDetails;
}
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: MockData
* @Package: com.amos.stuff.filter.mock
* @author: zhuqb
* @Description: 组装模拟数据
* @date: 2019/10/10 0010 上午 8:16
* @Version: V1.0
*/
public class MockData {
/**
* 模拟DTO数据对象
*
* @return
*/
public BizDTO mockBiz() {
String mockStr = "{\n" +
" \"name\": \"amos\",\n" +
" \"email\": \"amoszhu@aliyun.com\",\n" +
" \"age\": 30,\n" +
" \"grade\": \"6\",\n" +
" \"bizDetails\": [\n" +
" {\n" +
" \"company\": \"xxx公司1\",\n" +
" \"startTime\": 1570666889755,\n" +
" \"quantum\": 2,\n" +
" \"experience\": \"能吃苦耐劳\"\n" +
" },\n" +
" {\n" +
" \"company\": \"xxx公司2\",\n" +
" \"startTime\": 1570666899755,\n" +
" \"quantum\": 2,\n" +
" \"experience\": \"能吃苦耐劳\"\n" +
" }\n" +
" ]\n" +
"}";
BizDTO bizDTO = JSONObject.parseObject(mockStr, BizDTO.class);
return bizDTO;
}
/**
* 模拟数据过滤规则
*
* @return
*/
public List<FilterRule> mockRule() {
String mockStr = "[\n" +
" {\n" +
" \"entityFieldFullName\":\"com.amos.stuff.filter.bean.entity.BizEntity.name\",\n" +
" \"show\":false\n" +
" },\n" +
" {\n" +
" \"entityFieldFullName\":\"com.amos.stuff.filter.bean.entity.BizDetailEntity.experience\",\n" +
" \"show\":false\n" +
" }\n" +
"]";
List<FilterRule> list = JSONArray.parseArray(mockStr, FilterRule.class);
return list;
}
}
模拟测试数据
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: stuff
* @ClassName: DataFilterTest
* @Package: com.amos.stuff.filter
* @author: zhuqb
* @Description:
* @date: 2019/10/9 0009 下午 17:53
* @Version: V1.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {StuffApplication.class})// 指定启动类
@Slf4j
public class DataFilterTest {
@Test
public void testFilter() {
MockData data = new MockData();
BizDTO dto = data.mockBiz();
List<FilterRule> filterList = data.mockRule();
DataFilterHandler handler = new DataFilterHandler(dto, filterList);
handler.filter();
}
}
执行该段代码,查看运行的日志,看看是否数据是否已经过滤
从上面的运行日志我们可以看出,我们之前模拟的过滤父类属性全路径中的name属性和子类属性全路径中的experience属性,在过滤后的数据中已经没有了,目前来说功能还是正常的。
总结
上述代码的中心思想是构建一个类似树形结构的节点数据,然后解析类的结构,按照树形结构的特点来初始化该类的树形结构,同时整合业务需要过滤的字段,这样做的好处是为接下来的类的层级遍历提供的方便,并且过滤数据时,只需要考虑本节点的属性过滤即可
将原来复杂的多层级的结构数据过滤换元成单层级的数据过滤,这样处理起来了就方便多了,唯一要注意的地方是过滤完成之后的数据需要重新复制到当前层级的对象中
接入方法如下:
- 需要过滤的DTO对象添加自定义注解
@AmMapping
, 同时对象中的属性集也需要添加该自定义注解,注解的值是对应的Entity类 - 定义好数据过滤的规则,然后调用处理器来处理即可
总的来说,接入还是非常简单的,只需要在需要过滤的DTO对象中添加自定义注解,维护好DTO与Entity之间的属性对应关系即可