多级菜单DTO出参封装的递归、非递归实现

多级菜单DTO出参封装的递归、非递归实现

  • 1. 多级菜单封装实现方式介绍
    • 1.1. 递归方式使用递归实现多级菜单DTO的封装
    • 1.2.非递归方式
  • 2. xxx系统场景
    • 2.1. 输入参数定义
    • 2.2. 出参DTO定义
    • 2.3. 原始递归实现
    • 2.4. 非递归实现

最近在公司做需求时,发现自己所负责的另一个系统中的某个方法报TP50频率较高:
TP50=1037ms[偏差245.66%],超过10次TP50>=300ms

这里的TP50其实是个方法性能监控指标:在一个时间段内(如5分钟),统计该方法每次调用所消耗的时间,并将这些时间按从小到大的顺序进行排序,取第50%的那个值作为TP50值;配置此监控指标对应的报警阀值后,需要保证在这个时间段内该方法所有调用的消耗时间至少有50%的值要小于此阀值,否则系统将会报警。

趁着最近也有技改的需求,顺带优化下该方法,避免一直出现报警。于是,开始进行排查:

  1. 方法中对于数据库查询只涉及到4个表,并且是单表查询,另外分析了sql的执行计划,也确实走到了索引,因此查询时间并没有影响到该方法的执行效率。
  2. 然后,在出参封装的时候,之前是使用递归的方式进行封装的。如果,结果集数量很多,而且多个实体存在子属性,那么递归封装就会浪费太多时间。

因此,采用非递归的方式进行优化,即:空间换时间
其实这种问题在java出参封装中很常见,包括:多级菜单、多级题目等等。所以,我在这里进行一个简单的总结,具体解决方法可见2.4

1. 多级菜单封装实现方式介绍

在Java中实现多级菜单DTO的出参封装,一般使用递归或非递归两种方式

1.1. 递归方式使用递归实现多级菜单DTO的封装

  • 创建一个DTO类,用于表示菜单项,包含菜单项的名称、ID、type、title、子菜单列表等属性。
  • 在DTO类中定义一个递归方法recursion,该方法接收一个菜单项的ID作为参数,并返回一个完整的菜单项DTO对象。
  • 在递归方法中,首先根据传入的菜单项ID从数据源中获取对应的菜单项信息。
  • 如果该菜单项存在子菜单,那么递归调用该方法recursion,传入子菜单项的ID(或实体),并将返回的子菜单项DTO对象添加到当前菜单项的子菜单列表中。
  • 最后返回当前菜单项DTO对象。

1.2.非递归方式

  • 创建一个DTO类,于表示菜单项,包含菜单项的名称、ID、type、title、子菜单列表等属性。

  • 定义一个栈stack,用于保存待处理的菜单实体。
    首先将一级菜单实体入栈(栈中保存的内容视场景决定,有可能是id、有可能是某个实例)。

  • 进入循环,直到栈为空为止:

    • 弹出栈顶的菜单项实体
    • 根据该菜单项ID(或实体)从数据源中获取对应的菜单项信息。
    • 创建一个需要返回的菜单项DTO对象,并设置相应的属性。
    • 如果该菜单项存在子菜单,将子菜单项的ID(或实体)依次入栈。
    • 将当前菜单项DTO对象添加到其父菜单项的子菜单列表中。
  • 返回结果集

2. xxx系统场景

将数据库查到的问题集合Question映射成问题QuestionDTO集合。这里的Question有一个属性为type字段,用于表示当前题目是否是父题目

2.1. 输入参数定义

public class Question extends AbstractEntity {

    /**
     * 对应题库编号
     */
    private Long paperId;

    /**
     * 题目序号
     */
    private Long questionIndex;

    /**
     * 段落编号
     */
    private Long sectionId;

    private Long knowledgeId = 0l;

    private String knowledgeName;

    /**
     * 知识库链接[旧],用于关联知识点后,编辑试题时展示
     */
    private String knowledgeLink;

    /**
     *  问卷相关内容
     */
    private Long dimensionId;

    private String dimensionName;

    private Long subDimensionId;

    private String subDimensionName;

    private String hasElse;

    /**
     * 试题类型
     * 单选,多选,情景
     */
    private Integer type;

    /**
     * 分数
     */
    private Integer score;

    /**
     * 题目
     */
    private String title;

    /**
     * 解析
     */
    private String solution;

    /**
     * 知识库链接[新],用于考试结束展示作答记录以及练习模式显示
     */
    private String knowLink;


    /**
     * 情景试题 ---> 子试题
     */
    private Long parentId = 0L;

    private transient Long tempId;


    private List<Answer> answers;

    private List<Question> children = new ArrayList<Question>();

    private List<QuestionAttachment> questionAttachments = new ArrayList<QuestionAttachment>();

    private Boolean skip;

    private Long targetQuestionId;

    private Long targetQuestionIndex;

    private Long usedPaper=0L;
}


public class AbstractEntity implements Serializable {
    private Long id;
    @JsonSerialize(
        using = Json.DateTimeMinuteSerializer.class
    )
    private Date createdAt;
    private String createdPin;
    @JsonSerialize(
        using = Json.DateTimeMinuteSerializer.class
    )
    private Date updatedAt;
    private String updatedPin;
    private Integer sysVersion;
    private Boolean yn;
}

2.2. 出参DTO定义

public class QuestionDTO implements Serializable {
    private Long id;

    private List<QuestionDTO> children;

    private List<AnswerDTO> answerDTOs;
    private Long questionIndex;
    /**
     * 题目
     */
    private String title;
    /**
     * 试题类型
     * 单选,多选,简答
     */
    private Integer type;

    private Boolean skip;

    private Long targetQuestionId;
}

2.3. 原始递归实现

/**
     * List  转换question 成 questionDTO
     * @param questions
     * @return
     */
    public List<QuestionDTO> convertQuestions2DTO(List<Question> questions){
        List<QuestionDTO> questionDTOs = new ArrayList<QuestionDTO>();
        for(Question question : questions){
            QuestionDTO questionDTO = convert2QuestionDTO(question);
            questionDTOs.add(questionDTO);
        }
        return questionDTOs;
    }

    /**
     * 转换 question 2 questionDTO
     * @param question
     * @return
     */
    private QuestionDTO convert2QuestionDTO(Question question){
        QuestionDTO questionDTO = new QuestionDTO();
        BeanUtils.copyProperties(question,questionDTO);
        // 当前问题是父问题,递归寻找该问题的所有子问题,封装为questionDTOs
        if(SurveyTypeEnum.PARENT.getCode().equals(question.getType())){
            List<QuestionDTO> questionDTOs = convertQuestions2DTO(question.getChildren());
            questionDTO.setChildren(questionDTOs);
        }else{
            // 当前问题不是父问题,直接设置当前问题的答案属性
            List<AnswerDTO> answerDTOs = convertAnswer2DTO(question);
            questionDTO.setAnswerDTOs(answerDTOs);
        }
        return questionDTO;
    }

    /**
     * 转换 list answer 2 answerDto
     * @param question
     * @return
     */
    private  List<AnswerDTO> convertAnswer2DTO(Question question){
        List<AnswerDTO> answerDTOs = new ArrayList<AnswerDTO>();
        for(Answer answer : question.getAnswers()){
            AnswerDTO answerDTO = new AnswerDTO();
            BeanUtils.copyProperties(answer,answerDTO);
            answerDTOs.add(answerDTO);
        }
        return answerDTOs;
    }

2.4. 非递归实现

在非递归实现中,我使用了一个栈来保存待处理的问题对象question。

  • 首先,将初始的问题对象列表入栈。
  • 在循环中,从栈中弹出一个问题对象,进行转换为问题DTO对象,并根据问题类型进行不同的处理。
  • 如果问题类型是父问题(对于2.3方法中进行递归逻辑),则将其子问题对象逐个入栈,并创建相应的问题DTO对象,并将其添加父问题DTO对象的子问题列表中
  • 如果问题类型是其他类型(不进行递归),则直接进行答案实体的封装。
  • 最后,将转换后的问题DTO对象添加到结果列表中
/**
     * 使用非递归方式进行问题集合DTO的封装
     * @param questions 问题集合
     * @return 问题实例进行一一映射
     */
    private List<QuestionDTO> convertQuestions2DTOByNonRecursion(List<Question> questions) {
        List<QuestionDTO> questionDTOs = new ArrayList<>();
        Stack<Question> stack = new Stack<>();
        // 自定义线程安全的栈
        ConcurrentStack<QuerySurveyDTO> concurrentStack = new ConcurrentStack<>();
        // 问题实体依次入栈
        for (Question question : questions) {
            stack.push(question);
        }
        // 栈为空,表示封装结束
        while (!stack.isEmpty()) {
            Question question = stack.pop();
            QuestionDTO questionDTO = new QuestionDTO();
            BeanUtils.copyProperties(question, questionDTO);
         // 如果当前问题是父问题,创建子问题集合childQuestionDTOs。
        // 遍历当前问题实体的所有子问题,将每个子问题依次入栈
        // 并且映射成一个dto, 加入到子问题集合中childQuestionDTOs,全部遍历结束设置父问题属性children为childQuestionDTOs
            if (SurveyTypeEnum.PARENT.getCode().equals(question.getType())) {
                List<QuestionDTO> childQuestionDTOs = new ArrayList<>();
                for (Question childQuestion : question.getChildren()) {
                    stack.push(childQuestion);
                    QuestionDTO childQuestionDTO = new QuestionDTO();
                    BeanUtils.copyProperties(childQuestion, childQuestionDTO);
                    childQuestionDTOs.add(childQuestionDTO);
                }
                questionDTO.setChildren(childQuestionDTOs);
            } else {
          // 如果当前问题不是父问题,则遍历当前问题的答案集合,逐一封装成answerDTO,加入到answerDTOs集合。注入到问题DTO的answerDTOs属性中
                List<AnswerDTO> answerDTOs = new ArrayList<>();
                for (Answer answer : question.getAnswers()) {
                    AnswerDTO answerDTO = new AnswerDTO();
                    BeanUtils.copyProperties(answer, answerDTO);
                    answerDTOs.add(answerDTO);
                }
                questionDTO.setAnswerDTOs(answerDTOs);
            }
            questionDTOs.add(questionDTO);
        }
        return questionDTOs;
    }

虽然Stack继承了Vector ,其poppeek方法加了synchronized关键字,是线程安全的,但官方并不建议继续使用用Stack。

因此,在并发场景下,我使用了线程安全队列ConcurrentLinkedQueue来实现栈的功能:使用一个队列实现栈,需要注意的是,在元素element每次入队时,需要将除当前元素element之外的所有元素,都依次出队,再入队。那么,最新加入的元素element又跑到队头去了,保证栈的先进后出特性。

/**
     * 使用一个线程安全的队列ConcurrentLinkedQueue实现栈
     * @param  数据类型
     */
    static class ConcurrentStack<T>{
        Queue<T> queue;

        public ConcurrentStack() {
            this.queue = new ConcurrentLinkedQueue<>();
        }
        
        public void push(T element){
            queue.offer(element);
            int size = queue.size();
            while(size-- > 1){
                queue.offer(queue.poll());
            }
        }

        public T pop(){
        return queue.poll();
        }

        public boolean isEmpty(){
            return queue.isEmpty();
        }
    }

你可能感兴趣的:(java&框架,mybatis,链表,后端,java)