SpringBoot项目通用配置及工具类封装

Java系列文章


文章目录

  • Java系列文章
  • 前言
  • 一、通用配置
    • 1.1 自定义AOP注解日志打印
      • 1.1.1 控制台日志信息
      • 1.1.2 日志打印封装类
    • 1.2 枚举类封装
    • 1.3 自定义捕获异常及处理
      • 1.3.1 异常类封装
      • 1.3.2 全局异常捕获
      • 1.3.3 异常信息返回
    • 1.4 设置过滤器防御XSS脚本攻击
      • 1.4.1 设置过滤器
      • 1.4.2 XSS脚本攻击
    • 1.5 允许CORS跨域请求
    • 1.6 配置线程池
    • 1.7 自定义RESTful结果集封装类
  • 二、工具类
    • 2.1 复制对象
    • 2.2 分页
    • 2.3 密码生成
    • 2.4 雪花ID
    • 2.5 树形结构转换


前言

本文将介绍SpringBoot项目常用的配置及工具类封装。


一、通用配置

1.1 自定义AOP注解日志打印

1.1.1 控制台日志信息

SpringBoot项目通用配置及工具类封装_第1张图片

1.1.2 日志打印封装类

public class LogAspect {

    private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    /** 定义一个切点 */
    @Pointcut("execution(public * com.example.*.controller..*Controller.*(..))")
    public void controllerPointcut() {}

    @Resource
    private SnowFlake snowFlake;

    @Before("controllerPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {

        // 增加日志流水号
        MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));

        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();

        // 打印请求信息
        LOG.info("------------- 开始 -------------");
        LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
        LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
        LOG.info("远程地址: {}", request.getRemoteAddr());

        RequestContext.setRemoteAddr(getRemoteIp(request));

        // 打印请求参数
        Object[] args = joinPoint.getArgs();
        // LOG.info("请求参数: {}", JSONObject.toJSONString(args));

        Object[] arguments  = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof ServletRequest
                    || args[i] instanceof ServletResponse
                    || args[i] instanceof MultipartFile) {
                continue;
            }
            arguments[i] = args[i];
        }
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
    }

    @Around("controllerPointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
        LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
        return result;
    }

    /**
     * 使用nginx做反向代理,需要用该方法才能取到真实的远程IP
     * @param request
     * @return
     */
    public String getRemoteIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

1.2 枚举类封装

public enum ResultCodeEnum {
    SUCCESS("200","成功"),
    SUCCESS_VALIDATE("201","成功"),
    SYSTEM_ERROR("500","系统异常");

    public String code;
    public String msg;

    ResultCodeEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

1.3 自定义捕获异常及处理

1.3.1 异常类封装

@Data
public class MisException extends RuntimeException {
    private String msg;
    private int code = 500;

    public MisException(Exception e) {
        super(e);
        this.msg = "执行异常";
    }

    public MisException(String msg) {
        super(msg);
        this.msg = msg;
    }

    public MisException(String msg, Throwable e) {
        super(msg, e);
        this.msg = msg;
    }

    public MisException(String msg, int code) {
        super(msg);
        this.msg = msg;
        this.code = code;
    }

    public MisException(String msg, int code, Throwable e) {
        super(msg, e);
        this.msg = msg;
        this.code = code;
    }
}

1.3.2 全局异常捕获

@ControllerAdvice(basePackages={"com.example.mis.controller"})
public class ExceptionAdvice {

    private static final Logger LOG = LoggerFactory.getLogger(ExceptionAdvice.class);

    // 校验异常统一处理
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public R validExceptionHandler(BindException e) {
        R result = new R();
        LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        result.setCode(ResultCodeEnum.SUCCESS_VALIDATE.code);
        result.setData(false);
        result.setMsg(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return result;
    }

    //统一异常处理@ExceptionHandler,主要用于Exception
    @ExceptionHandler(Exception.class)
    @ResponseBody//返回json串
    public R error(HttpServletRequest request, Exception e) throws MinioException {
        long activityTimeout = StpUtil.getTokenActivityTimeout();
        R result = new R();
        if(activityTimeout < 0) {
            LOG.error("异常错误:",e);
            result.setCode(ResultCodeEnum.TOKEN_TIMEOUT.code);
            result.setData(e.getMessage());
            result.setMsg(e.getMessage());
            return result;
        }else {
            LOG.error("异常错误:",e);
            result.setCode(ResultCodeEnum.SYSTEM_ERROR.code);
            result.setData(String.valueOf(e));
            result.setMsg(ResultCodeEnum.SYSTEM_ERROR.msg);
            return result;
        }
    }
}

1.3.3 异常信息返回

SpringBoot项目通用配置及工具类封装_第2张图片
SpringBoot项目通用配置及工具类封装_第3张图片

1.4 设置过滤器防御XSS脚本攻击

1.4.1 设置过滤器

@WebFilter(urlPatterns = "/*")
public class XssFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper(request);
        filterChain.doFilter(wrapper, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

1.4.2 XSS脚本攻击

public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        String value= super.getParameter(name);
        if(!StrUtil.hasEmpty(value)){
            value=HtmlUtil.cleanHtmlTag(value);
        }
        return value;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values= super.getParameterValues(name);
        if(values!=null){
            for (int i=0;i<values.length;i++){
                String value=values[i];
                if(!StrUtil.hasEmpty(value)){
                    value=HtmlUtil.cleanHtmlTag(value);
                }
                values[i]=value;
            }
        }
        return values;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> parameters = super.getParameterMap();
        LinkedHashMap<String, String[]> map=new LinkedHashMap();
        if(parameters!=null){
            for (String key:parameters.keySet()){
                String[] values=parameters.get(key);
                for (int i = 0; i < values.length; i++) {
                    String value = values[i];
                    if (!StrUtil.hasEmpty(value)) {
                        value = HtmlUtil.cleanHtmlTag(value);
                    }
                    values[i] = value;
                }
                map.put(key,values);
            }
        }
        return map;
    }

    @Override
    public String getHeader(String name) {
        String value= super.getHeader(name);
        if (!StrUtil.hasEmpty(value)) {
            value = HtmlUtil.cleanHtmlTag(value);
        }
        return value;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        InputStream in= super.getInputStream();
        InputStreamReader reader=new InputStreamReader(in, Charset.forName("UTF-8"));
        BufferedReader buffer=new BufferedReader(reader);
        StringBuffer body=new StringBuffer();
        String line=buffer.readLine();
        while(line!=null){
            body.append(line);
            line=buffer.readLine();
        }
        buffer.close();
        reader.close();
        in.close();
        Map<String,Object> map=JSONUtil.parseObj(body.toString());
        Map<String,Object> result=new LinkedHashMap<>();
        for(String key:map.keySet()){
            Object val=map.get(key);
            if(val instanceof String){
                if(!StrUtil.hasEmpty(val.toString())){
                    result.put(key,HtmlUtil.cleanHtmlTag(val.toString()));
                }
            }
            else {
                result.put(key,val);
            }
        }
        String json=JSONUtil.toJsonStr(result);
        ByteArrayInputStream bain=new ByteArrayInputStream(json.getBytes());
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bain.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }
}

1.5 允许CORS跨域请求

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH")
                .allowedHeaders("*")
                .maxAge(3600);
    }
}

1.6 配置线程池

@Configuration
public class ThreadPoolConfig {

    @Bean("AsyncTaskExecutor")
    public AsyncTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(8);
        // 设置最大线程数
        executor.setMaxPoolSize(16);
        // 设置队列容量
        executor.setQueueCapacity(1000);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 线程名称的前缀
        executor.setThreadNamePrefix("task-");
        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //初始化线程池
        executor.initialize();
        return executor;
    }

}

1.7 自定义RESTful结果集封装类

public class R {
    private String code;
    private String msg;
    private Object data;


    public R() {
    }

    private R(Object data) {
        this.data = data;
    }

    public static R success() {
        R r = new R();
        r.setCode(ResultCodeEnum.SUCCESS.code);
        r.setMsg(ResultCodeEnum.SUCCESS.msg);
        return r;
    }

    public static R success(Object data) {
        R r = new R(data);
        r.setCode(ResultCodeEnum.SUCCESS.code);
        r.setMsg(ResultCodeEnum.SUCCESS.msg);
        return r;
    }

    public static R success(String code, String msg) {
        R r = new R();
        r.setCode(code);
        r.setMsg(msg);
        return r;
    }

    public static R error() {
        R r = new R();
        r.setCode(ResultCodeEnum.SYSTEM_ERROR.code);
        r.setMsg(ResultCodeEnum.SYSTEM_ERROR.msg);
        return r;
    }

    public static R error(String code, String msg) {
        R r = new R();
        r.setCode(code);
        r.setMsg(msg);
        return r;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

二、工具类

2.1 复制对象

public class CopyUtil {
    /**
     * 单体复制
     */
    public static <T> T copy(Object source, Class<T> clazz) {
        if (source == null) {
            return null;
        }
        T obj = null;
        try {
            obj = clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        BeanUtils.copyProperties(source, obj);
        return obj;
    }

    /**
     * 列表复制
     */
    public static <T> List<T> copyList(List source, Class<T> clazz) {
        List<T> target = new ArrayList<>();
        if (!CollectionUtils.isEmpty(source)){
            for (Object c: source) {
                T obj = copy(c, clazz);
                target.add(obj);
            }
        }
        return target;
    }
}

2.2 分页

public class PageUtils {
    @NotNull(message = "页码不能为空")
    @Min(value = 1,message = "页码不能小于1")
    private int pageNum = 1;

    @NotNull(message = "每页条数不能为空")
    @Max(value = 100,message = "每页条数不能超过100条")
    private int pageSize = 10;

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
}

2.3 密码生成

/**
 * 密码生成工具类
 * 功能:生成包含大小写字母、数字、特殊符号的安全密码
 */
public class PasswordUtils {

    // 默认字符池定义
    private static final String LOWER_CASE = "abcdefghijklmnopqrstuvwxyz";
    private static final String UPPER_CASE = LOWER_CASE.toUpperCase();
    private static final String NUMBERS = "0123456789";
    private static final String SYMBOLS = "!@#$%^&*_";

    /**
     * 生成默认8位密码(包含大小写、数字、符号)
     */
    public static String generate() {
        return generate(8, true, true, true, true);
    }

    /**
     * 自定义密码生成
     * @param length     密码长度
     * @param useLower   是否包含小写字母
     * @param useUpper   是否包含大写字母
     * @param useNumber  是否包含数字
     * @param useSymbol  是否包含符号
     */
    public static String generate(int length, boolean useLower, boolean useUpper,
                                  boolean useNumber, boolean useSymbol) {
        if (length < 4) {
            throw new IllegalArgumentException("密码长度至少4位");
        }

        // 1. 构建字符池
        StringBuilder charPool = new StringBuilder();
        List<Character> mandatoryChars = new ArrayList<>();

        if (useLower) {
            charPool.append(LOWER_CASE);
            mandatoryChars.add(RandomUtil.randomChar(LOWER_CASE));
        }
        if (useUpper) {
            charPool.append(UPPER_CASE);
            mandatoryChars.add(RandomUtil.randomChar(UPPER_CASE));
        }
        if (useNumber) {
            charPool.append(NUMBERS);
            mandatoryChars.add(RandomUtil.randomChar(NUMBERS));
        }
        if (useSymbol) {
            charPool.append(SYMBOLS);
            mandatoryChars.add(RandomUtil.randomChar(SYMBOLS));
        }

        if (charPool.length() == 0) {
            throw new IllegalArgumentException("至少启用一种字符类型");
        }

        // 2. 确保每种类型至少一个字符
        StringBuilder password = new StringBuilder();
        for (Character ch : mandatoryChars) {
            password.append(ch);
        }

        // 3. 填充剩余随机字符
        int remaining = length - mandatoryChars.size();
        for (int i = 0; i < remaining; i++) {
            password.append(RandomUtil.randomChar(charPool.toString()));
        }

        // 4. 打乱顺序避免固定模式
        List<Character> chars = new ArrayList<>();
        for (char c : password.toString().toCharArray()) {
            chars.add(c);
        }
        Collections.shuffle(chars);

        StringBuilder finalPassword = new StringBuilder();
        for (char c : chars) {
            finalPassword.append(c);
        }

        return finalPassword.toString();
    }
}

2.4 雪花ID

/**
 * Twitter的分布式自增ID雪花算法
 **/
@Component
public class SnowFlake {

    /**
     * 起始的时间戳
     */
    private final static long START_STMP = 1609459200000L; // 2021-01-01 00:00:00

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId = 1;  //数据中心
    private long machineId = 1;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳

    public SnowFlake() {
    }

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) throws ParseException {
        // 时间戳
        // System.out.println(System.currentTimeMillis());
        // System.out.println(new Date().getTime());
        //
        // String dateTime = "2021-01-01 08:00:00";
        // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        // System.out.println(sdf.parse(dateTime).getTime());

        SnowFlake snowFlake = new SnowFlake(1, 1);

        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            System.out.println(snowFlake.nextId());
            System.out.println(System.currentTimeMillis() - start);
        }
    }
}

2.5 树形结构转换

/**
 * 树形结构转换工具类(支持泛型、循环依赖检测、排序等)
 */
public class TreeUtils {

    /**
     * 创建树构建器
     * @param list 原始数据列表
     * @param   节点类型
     * @param   ID类型
     * @return 树构建器
     */
    public static <T, K> TreeBuilder<T, K> build(List<T> list) {
        return new TreeBuilder<>(list);
    }

    /**
     * 树构建器(链式配置)
     */
    public static class TreeBuilder<T, K> {
        private final List<T> list;
        private Function<T, K> idGetter;
        private Function<T, K> parentIdGetter;
        private Function<T, List<T>> childrenGetter;
        private Comparator<T> comparator;
        private K rootParentId;
        private boolean checkCyclic = true;

        public TreeBuilder(List<T> list) {
            Objects.requireNonNull(list, "List cannot be null");
            this.list = new ArrayList<>(list); // 避免原始列表被修改
        }

        /**
         * 设置ID获取方法
         */
        public TreeBuilder<T, K> id(Function<T, K> idGetter) {
            this.idGetter = Objects.requireNonNull(idGetter, "idGetter cannot be null");
            return this;
        }

        /**
         * 设置父ID获取方法
         */
        public TreeBuilder<T, K> parentId(Function<T, K> parentIdGetter) {
            this.parentIdGetter = Objects.requireNonNull(parentIdGetter, "parentIdGetter cannot be null");
            return this;
        }

        /**
         * 设置子节点属性(通过Getter/Setter)
         */
        public TreeBuilder<T, K> children(Function<T, List<T>> childrenGetter) {
            this.childrenGetter = Objects.requireNonNull(childrenGetter, "childrenGetter cannot be null");
            return this;
        }

        /**
         * 设置排序规则
         */
        public TreeBuilder<T, K> sorted(Comparator<T> comparator) {
            this.comparator = comparator;
            return this;
        }

        /**
         * 设置根节点的父ID值(默认为null)
         */
        public TreeBuilder<T, K> rootParentId(K rootParentId) {
            this.rootParentId = rootParentId;
            return this;
        }

        /**
         * 是否检测循环依赖(默认开启)
         */
        public TreeBuilder<T, K> checkCyclic(boolean checkCyclic) {
            this.checkCyclic = checkCyclic;
            return this;
        }

        /**
         * 构建树形结构
         */
        public List<T> build() {
            validate();
            Map<K, List<T>> parentIdMap = list.stream()
                    .collect(Collectors.groupingBy(parentIdGetter, Collectors.toList()));

            Set<K> visitedIds = new HashSet<>(); // 用于循环依赖检测

            return list.stream()
                    .filter(node -> {
                        K parentId = parentIdGetter.apply(node);
                        return Objects.equals(parentId, rootParentId) || 
                              (parentId == null && rootParentId == null);
                    })
                    .peek(root -> buildChildren(root, parentIdMap, visitedIds))
                    .sorted(comparator == null ? (a, b) -> 0 : comparator)
                    .collect(Collectors.toList());
        }

        private void validate() {
            Objects.requireNonNull(idGetter, "idGetter must be set");
            Objects.requireNonNull(parentIdGetter, "parentIdGetter must be set");
            Objects.requireNonNull(childrenGetter, "childrenGetter must be set");
        }

        private void buildChildren(T node, Map<K, List<T>> parentIdMap, Set<K> visitedIds) {
            K nodeId = idGetter.apply(node);
            if (checkCyclic && !visitedIds.add(nodeId)) {
                throw new IllegalStateException("检测到循环依赖,节点ID: " + nodeId);
            }

            List<T> children = parentIdMap.getOrDefault(nodeId, new ArrayList<>())
                    .stream()
                    .peek(child -> buildChildren(child, parentIdMap, visitedIds))
                    .sorted(comparator == null ? (a, b) -> 0 : comparator)
                    .collect(Collectors.toList());

            if (!children.isEmpty()) {
                childrenGetter.apply(node).clear();
                childrenGetter.apply(node).addAll(children);
            }

            if (checkCyclic) {
                visitedIds.remove(nodeId);
            }
        }
    }
}

你可能感兴趣的:(Java,spring,boot,java,后端,spring)