類以及屬性集按照規則過濾——一個簡單的構建思路來解決

近期在工作中遇到一個功能需求,基於類以及屬性集的數據過濾,功能比較簡單,但是的確花費了我一天的工作量來完成該功能,在開發這個功能過程中,我覺的有些問題的思考和處理方式覺得很有幫助,所以整理如下的博文,以便自己以後遇到類似的需求能快速的解決

需求如下:

需要開發一個功能,能維護系統每個業務單據的數據項,每個業務單據的數據項是可以選擇是否展示數據值,然後再系統展示的時候,根據已經維護好的數據項的單據,過濾其中不需要顯示數據值的

需求很簡單,現在說明下代碼結構:

  1. 每個業務單據有DTO對象和Entity對象,其中DTO對象是顯示給前端界面顯示用的,Entity對象是單據對應的數據庫模型
  2. 一個單據DTO對象可以有屬性集List,表示父子級屬性,Entity對象中不存在屬性集
  3. DTO對象和Entity對象屬性名稱相同(DTO比Entity多)

需求擴展說明:

  1. 根據需求,參數傳遞過來的是DTO對象,然後規則裏面保存的Entity對象的屬性全路徑(包括屬性集中的屬性全路徑)
  2. 需要根據Entity屬性全路徑的過濾規則來過濾DTO中的數據項的數據值
  3. 一個DTO對象可能含有多個屬性集對象,屬性集對象中的DTO也可能含有多個屬性集對象

問題思考點:

  1. 因爲傳遞的參數的DTO對象,而規則裏面對應的都是Entity中的屬性全路徑,我們怎麼去匹配他們之間的對應關係?

    我們需要建立一個映射關係,來獲取DTO屬性與Entity屬性之間對應關係,這裏需要注意的是DTO中有屬性集List,映射關係也需要維護屬性集的DTO和Entity對應的關係,

    我們在根據Entity過濾規則來過濾時,其實也就是根據映射關係中的DTO的屬性全路徑來過濾DTO中的對象(DTO的屬性全路徑和Entity屬性全路徑是不一致的,也有人會說直接根據屬性字段來匹配,這裏需要說明下,由於存在相同的基類,所以不同類的屬性名稱存在相同的,所以不能單據的根據屬性名稱來匹配,應該根據屬性全路徑來匹配)


    這裏其實還有個問題,就是怎麼知道DTO對應的Entity呢?方法很簡單,我們可以通過自定義註解來解決,稍後在代碼中我們會細細說明

  2. 需要考慮到過濾的規則裏面含有多個屬性集的數據項過濾,如何才能過濾屬性集中的數據項呢?

    這是個難點,我們來分析下,最頂級的是一個DTO對象,然後DTO對象中存在屬性集,每個屬性集對象中也可能含有屬性集,這裏我們需要建議一個模型結構:

  • 該模型結構能知道當前的屬性集對應的對象class信息,該class是爲了能夠知道該層級所有的屬性字段,
  • 模型結構需要存儲當前節點的所有的屬性集,爲了迭代遍歷
  • 這個數據模型必須要符合樹形節點結構,並且支持很好的遍歷
  • 再添加我們的業務邏輯,每個節點的層級需要知道該層級中哪些字段屬性是需要過濾的

開發代碼邏輯:

分析了上述的模型結構,可以整理出一下的開發代碼邏輯:

  1. 我們需要獲取DTO對象和Entity對象之間的映射關係
  2. 根據初始對象和過濾的規則字段,我們需要構建出模型結構
  3. 採用迭代的方式,循環遍歷模型結構,依據當前節點的過濾屬性來將該節點的數據項賦值爲空
  4. 返回DTO對象

代碼實現

  1. 首先我們先定義好自定義註解,爲了獲取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();
    }
  1. 定義好過濾的規則實體和基類
    /**
     * 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 {
    }
  1. 數據過濾處理器,代碼中已經添加說明
    /**
     * 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\": \"[email protected]\",\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屬性,在過濾後的數據中已經沒有了,目前來說功能還是正常的。

總結

上述代碼的中心思想是構建一個類似樹形結構的節點數據,然後解析類的結構,按照樹形結構的特點來初始化該類的樹形結構,同時整合業務需要過濾的字段,這樣做的好處是爲接下來的類的層級遍歷提供的方便,並且過濾數據時,只需要考慮本節點的屬性過濾即可

將原來複雜的多層級的結構數據過濾換元成單層級的數據過濾,這樣處理起來了就方便多了,唯一要注意的地方是過濾完成之後的數據需要重新複製到當前層級的對象中

接入方法如下:

  1. 需要過濾的DTO對象添加自定義註解 @AmMapping, 同時對象中的屬性集也需要添加該自定義註解,註解的值是對應的Entity類
  2. 定義好數據過濾的規則,然後調用處理器來處理即可

總的來說,接入還是非常簡單的,只需要在需要過濾的DTO對象中添加自定義註解,維護好DTO與Entity之間的屬性對應關係即可

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章