Filter实现请求日志记录

将锁有得外部访问都记录在日志文件里面,设计这个功能是为了(为什么):

1. 在不引入Promentheus进行接口监控时,基于日志文件就可以实现整个项目得监控。

2. 当出现问题时,可以基于此进行流量重放。

效果如下(预期):

来看一下请求日志记录得实现。

技术方案(选型)

如果单纯的将这个记录接口的请求信息,当作一个普通的需求来设计,我们可以怎么来实现呢?

        基于过滤器Filter,来拦截web请求,记录请求相关信息。

        基于AOP来实现方法拦截,借助@Around来实现请求方法执行前后增强,记录请求相关的信息

方案的选择?

Filter过滤器方案

关于过滤器的知识点,可以参考之前文章。若使用过滤器,则主要就是拦截web请求,具体的实现流程如下:

在过滤器的doFilter方法中,划分为三块:

  1.         doBefore:表示将请求转发到Controller执行之前
  •                 记录开始执行时间
  •                 记录请求相关信息
  •         doFilter: 即将请求转发到Controller去执行
  •         doAfter: Controller方法执行完
  •                 记录结束时间,计算执行耗时
  •                 日志输出

使用这种方式的优缺点比较突出,优点是适用性强,实现简单,缺点是只能记录Controller的请求相关信息,如果我们想统计某个Service方法、Mapper方方法,那么这种方法不太合适

AOP切面方案

若使用AOP来实现,则关键点在于我需要拦截那些方法,即定义切点

基本策略与前面差不多,不过有几个关键点

        定义切点:可以是直接拦截包路径方式,也可以是配合自定义注解,拦截某些特定注解的方式

  •         使用Around环绕方式
  •         使用AOP来实现的优缺点也比较明显
  • 优点:
  • 灵活性高,可以拦截任何共有方法
  • 缺点:
  • 需要自定义切点,通常不太容易一次编写,所有项目适用。

实现实例(Filter方案)

实现类

包路径:com/github/paicoding/forum/web/hook/filter/ReqRecordFilter.java

package com.github.paicoding.forum.web.hook.filter;

import com.github.paicoding.forum.api.model.context.ReqInfoContext;
import com.github.paicoding.forum.core.util.CrossUtil;
import com.github.paicoding.forum.core.util.IpUtil;
import com.github.paicoding.forum.service.statistics.service.StatisticsSettingService;
import com.github.paicoding.forum.web.global.GlobalInitService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;

/**
 * 1. 请求参数日志输出过滤器
 * 2. 判断用户是否登录
 *
 * @date 2022/7/6
 */
@Slf4j
@WebFilter(urlPatterns = "/*", filterName = "reqRecordFilter", asyncSupported = true)
public class ReqRecordFilter implements Filter {
    private static Logger REQ_LOG = LoggerFactory.getLogger("req");


    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        long start = System.currentTimeMillis();
        HttpServletRequest request = null;
        try {
            //构建请求上下文
            request = this.initReqInfo((HttpServletRequest) servletRequest);
            CrossUtil.buildCors(request, (HttpServletResponse) servletResponse);
            filterChain.doFilter(request, servletResponse);
        } finally {
            //根据请求上下文,输出请求日志
            buildRequestLog(ReqInfoContext.getReqInfo(), request, System.currentTimeMillis() - start);
            ReqInfoContext.clear();
        }
    }

    private HttpServletRequest initReqInfo(HttpServletRequest request) {
        String uri = request.getRequestURI();
        if (uri.startsWith("/js/") || uri.startsWith("/css/") || uri.endsWith(".js") || uri.endsWith(".css")) {
            // 静态资源直接放行
            return request;
        }

        try {
            ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo();
            reqInfo.setHost(request.getHeader("host"));
            reqInfo.setPath(request.getPathInfo());
            reqInfo.setReferer(request.getHeader("referer"));
            reqInfo.setClientIp(IpUtil.getClientIp(request));
            reqInfo.setUserAgent(request.getHeader("User-Agent"));

            request = this.wrapperRequest(request, reqInfo);
            // 初始化登录信息
            globalInitService.initLoginUser(reqInfo);
            ReqInfoContext.addReqInfo(reqInfo);
        } catch (Exception e) {
            log.error("init reqInfo error!", e);
        }

        return request;
    }

    private void buildRequestLog(ReqInfoContext.ReqInfo req, HttpServletRequest request, long costTime) {
        // fixme 过滤不需要记录请求日志的场景
        if (request == null
                || req == null
                || request.getRequestURI().endsWith("css")
                || request.getRequestURI().endsWith("js")
                || request.getRequestURI().endsWith("png")
                || request.getRequestURI().endsWith("ico")
                || request.getRequestURI().endsWith("svg")) {
            return;
        }

        StringBuilder msg = new StringBuilder();
        msg.append("method=").append(request.getMethod()).append("; ");
        if (StringUtils.isNotBlank(req.getReferer())) {
            msg.append("referer=").append(URLDecoder.decode(req.getReferer())).append("; ");
        }
        msg.append("remoteIp=").append(req.getClientIp());
        msg.append("; agent=").append(req.getUserAgent());

        if (req.getUserId() != null) {
            // 打印用户信息
            msg.append("; user=").append(req.getUserId());
        }

        msg.append("; uri=").append(request.getRequestURI());
        if (StringUtils.isNotBlank(request.getQueryString())) {
            msg.append('?').append(URLDecoder.decode(request.getQueryString()));
        }

        msg.append("; payload=").append(req.getPayload());
        msg.append("; cost=").append(costTime);
        REQ_LOG.info("{}", msg);

        // 保存请求计数
        statisticsSettingService.saveRequestCount(req.getClientIp());
    }


    private HttpServletRequest wrapperRequest(HttpServletRequest request, ReqInfoContext.ReqInfo reqInfo) {
        if (!HttpMethod.POST.name().equalsIgnoreCase(request.getMethod())) {
            return request;
        }
        //封装请求参数
        BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
        reqInfo.setPayload(requestWrapper.getBodyString());
        return requestWrapper;
    }

}

排除静态资源

因为pc前台的网页也是集成在项目中的,因此在我们实际的日志输出时,需要将一些静态资源访问给排除掉,主要是基request.getRequestURI后缀来进行过滤的。

    private boolean isStaticURI(HttpServletRequest request) {
        return request == null
                || request.getRequestURI().endsWith("css")
                || request.getRequestURI().endsWith("js")
                || request.getRequestURI().endsWith("png")
                || request.getRequestURI().endsWith("ico")
                || request.getRequestURI().endsWith("svg")
                || request.getRequestURI().endsWith("min.js.map")
                || request.getRequestURI().endsWith("min.css.map");
    }

上面这种方式虽然实现简单,但是也有缺陷:

  • 如静态资源请求带url参数
  • 除上述这几种静态资源资源之外还有(XML、MP3等)

请求上下文

来看一下,请求上下文的构建,主要是基于HttpServletRequest来获取相关参数

 private HttpServletRequest initReqInfo(HttpServletRequest request) {
        String uri = request.getRequestURI();
        if (uri.startsWith("/js/") || uri.startsWith("/css/") || uri.endsWith(".js") || uri.endsWith(".css")) {
            // 静态资源直接放行
            return request;
        }

        try {
            ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo();
            reqInfo.setHost(request.getHeader("host"));
            reqInfo.setPath(request.getPathInfo());
            reqInfo.setReferer(request.getHeader("referer"));
            reqInfo.setClientIp(IpUtil.getClientIp(request));
            reqInfo.setUserAgent(request.getHeader("User-Agent"));
            //传参的封装的处理: 主要是为了避免post的输入流,读取一次后无法在获取的问题
            request = this.wrapperRequest(request, reqInfo);
            // 初始化登录信息
            globalInitService.initLoginUser(reqInfo);
            ReqInfoContext.addReqInfo(reqInfo);
        } catch (Exception e) {
            log.error("init reqInfo error!", e);
        }

        return request;
    }

重点关注两个:

1.请求者的ip获取

实现类的核心如下(通用的工具类,需要注意的是若使用nginx做反向代理的话,那么请不要把用户的信息吃掉了,否则下面这个方法拿不到)
 

    /**
     * 获取请求来源的ip地址
     *
     * @param request
     * @return
     */
    public static String getClientIp(HttpServletRequest request) {
        try {
            String xIp = request.getHeader("X-Real-IP");
            String xFor = request.getHeader("X-Forwarded-For");
            if (StringUtils.isNotEmpty(xFor) && !UNKNOWN.equalsIgnoreCase(xFor)) {
                //多次反向代理后会有多个ip值,第一个ip才是真实ip
                int index = xFor.indexOf(",");
                if (index != -1) {
                    return xFor.substring(0, index);
                } else {
                    return xFor;
                }
            }
            xFor = xIp;
            if (StringUtils.isNotEmpty(xFor) && !UNKNOWN.equalsIgnoreCase(xFor)) {
                return xFor;
            }
            if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {
                xFor = request.getHeader("Proxy-Client-IP");
            }
            if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {
                xFor = request.getHeader("WL-Proxy-Client-IP");
            }
            if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {
                xFor = request.getHeader("HTTP_CLIENT_IP");
            }
            if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {
                xFor = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {
                xFor = request.getRemoteAddr();
            }

            if ("localhost".equalsIgnoreCase(xFor) || "127.0.0.1".equalsIgnoreCase(xFor) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(xFor)) {
                return getLocalIp4Address();
            }
            return xFor;
        } catch (Exception e) {
            log.error("get remote ip error!", e);
            return "x.0.0.1";
        }
    }

2.请求参数封装

首先需要理解一下为啥需要封装请求参数?

对于post之类的请求,若是传参json,那么需要从HttpServletRequest的请求流中读取,但是这个流是一次性的,如果打印日志的时候把这个参数读取出来了,那么在实际业务中,就拿不到对应的参数了,为了解决这个问题,我们需要将这个InputStream进行封装一下,所以技术派封装了一个BodyReaderHttpServletRequestWrapper类,来封装一下请求。

核心实现如下

  • 只拿post,put请求,非二进制、非文件上传、非表单数据上传的场景
  • 将请求参数读取到 byte[] body
  • 基于body封装 ServletInputStream,用于后续的传参获取。

package com.github.paicoding.forum.web.hook.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

/**
 * post 流数据封装,避免因为打印日志导致请求参数被提前消费
 *
 * todo 知识点: 请求参数的封装,避免输入流读取一次就消耗了
 *
 * @author YiHui
 * @date 2022/7/6
 */
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private static final List<String> POST_METHOD = Arrays.asList("POST", "PUT");
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final byte[] body;
    private final String bodyString;

    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);

        if (POST_METHOD.contains(request.getMethod()) && !isMultipart(request) && !isBinaryContent(request) && !isFormPost(request)) {
            bodyString = getBodyString(request);
            body = bodyString.getBytes(StandardCharsets.UTF_8);
        } else {
            bodyString = null;
            body = null;
        }
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (body == null) {
            return super.getInputStream();
        }

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }

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

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

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }

    public boolean hasPayload() {
        return bodyString != null;
    }

    public String getBodyString() {
        return bodyString;
    }

    private String getBodyString(HttpServletRequest request) {
        BufferedReader br;
        try {
            br = request.getReader();
        } catch (IOException e) {
            logger.warn("Failed to get reader", e);
            return "";
        }

        String str;
        StringBuilder body = new StringBuilder();
        try {
            while ((str = br.readLine()) != null) {
                body.append(str);
            }
        } catch (IOException e) {
            logger.warn("Failed to read line", e);
        }

        try {
            br.close();
        } catch (IOException e) {
            logger.warn("Failed to close reader", e);
        }

        return body.toString();
    }

    /**
     * is binary content
     *
     * @param request http request
     * @return ret
     */
    private boolean isBinaryContent(final HttpServletRequest request) {
        return request.getContentType() != null &&
                (request.getContentType().startsWith("image") || request.getContentType().startsWith("video") ||
                        request.getContentType().startsWith("audio"));
    }

    /**
     * is multipart content
     *
     * @param request http request
     * @return ret
     */
    private boolean isMultipart(final HttpServletRequest request) {
        return request.getContentType() != null && request.getContentType().startsWith("multipart/form-data");
    }

    private boolean isFormPost(final HttpServletRequest request) {
        return request.getContentType() != null && request.getContentType().startsWith("application/x-www-form-urlencoded");
    }
}

日志输出

最后再看一下日志输出,我们直接将上面封装的请求相关信息,按照具体的日志格式进行打印

   private void buildRequestLog(ReqInfoContext.ReqInfo req, HttpServletRequest request, long costTime) {
        if (req == null || isStaticURI(request)) {
            return;
        }

        StringBuilder msg = new StringBuilder();
        msg.append("method=").append(request.getMethod()).append("; ");
        if (StringUtils.isNotBlank(req.getReferer())) {
            msg.append("referer=").append(URLDecoder.decode(req.getReferer())).append("; ");
        }
        msg.append("remoteIp=").append(req.getClientIp());
        msg.append("; agent=").append(req.getUserAgent());

        if (req.getUserId() != null) {
            // 打印用户信息
            msg.append("; user=").append(req.getUserId());
        }

        msg.append("; uri=").append(request.getRequestURI());
        if (StringUtils.isNotBlank(request.getQueryString())) {
            msg.append('?').append(URLDecoder.decode(request.getQueryString()));
        }

        msg.append("; payload=").append(req.getPayload());
        msg.append("; cost=").append(costTime);
        REQ_LOG.info("{}", msg);

        // 保存请求计数
        statisticsSettingService.saveRequestCount(req.getClientIp());
    }

小结

上面介绍了技术派中基于Filter实现的请求日志记录,将所有外部请求,都统一写道req日志文件中,可以基于此,查看一下当前项目的请求情况,接口耗时等。

其中涉及到的知识点如下:

  • Filter基本使用
  • Filter/AOP实现请求参数记录的方案
  • 如何从HttpServletRequest中获取你需要的请求参数
  • 请求参数的封装,允许请求参数InputStream的重复读取
  • 如何获取请求者ip
  • 日志输出

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/459840.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HNU-计算机系统-实验1-原型机vspm1.0-(二周目玩家视角)

前言 二周目玩家&#xff0c;浅试一下这次的原型机实验。总体感觉跟上一年的很相似&#xff0c;但还是有所不同。 可以比较明显地感觉到&#xff0c;这个界面越来越好看了&#xff0c;可操作与可探索的功能也越来越多了。 我们HNU的SYSTEM真的越来越好了&#xff01;&#x…

图像处理与视觉感知---期末复习重点(3)

文章目录 一、空间域和频率域二、傅里叶变换三、频率域图像增强 一、空间域和频率域 1. 空间域&#xff1a;即所说的像素域&#xff0c;在空间域的处理就是在像素级的处理&#xff0c;如在像素级的图像叠加。通过傅立叶变换后&#xff0c;得到的是图像的频谱&#xff0c;表示图…

ElasticSearch 看这一篇就够了,详解!!!

目录 核心概念 索引 映射 文档 基本操作 索引 创建 查询 删除 映射 创建 查询 文档 添加文档 查询文档 删除文档 更新文档 批量操作 高级查询 说明 语法 常见检索 查询所有[match_all] 关键词查询(term) 范围查询[range] 前缀查询[prefix] 通配符查询…

【智能算法】人工水母搜索算法(JS)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.代码实现4.参考文献 1.背景 2020年&#xff0c;Chou 等人受到水母运动行为启发&#xff0c;提出了人工水母搜索算法(Artificial Jellyfish Search Optimizer, JS)。 2.算法原理 2.1算法思想 JS模拟了水母的搜索行为&#xf…

要将镜像推送到GitLab的Registry中的步骤

1、通过cli 模式登录gitlab &#xff08;命令行模式&#xff09; docker login git.asc-dede.de Username: haiyang Password: Login Succeeded 2、查看我的本地镜像&#xff1a; 3&#xff0c;推送镜像apollo_core到对应的gitlab项目的Registry 中 docker push registry.gi…

汽车电子零部件(4):行泊一体ADAS

前言: 现阶段智能汽车行业正在大规模力推无限接近于L3的L2++或L2.9自动驾驶量产落地,类似于当初智能手机替换传统手机的行业机会期。智能汽车常见的智能驾驶功能包括: 行车场景:自适应巡航控制ACC;自动变道辅助ALC;交通拥堵辅助TJA;车道居中LCC;领航辅助NOA; 泊车场…

基于R语言的水文、水环境模型优化技术及快速率定方法与多模型教程

原文链接&#xff1a;基于R语言的水文、水环境模型优化技术及快速率定方法与多模型教程https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247597847&idx7&snd71869f1290d0ef9dd7fd3f74dd7ca33&chksmfa823ef0cdf5b7e655af5e773a3d3a1b200632a5981f99fe72f0…

excel同类项合并求和怎么操作?

想必很多办公人士都熟悉excel这款软件&#xff0c;那么使用过程里&#xff0c;若想合并同类项数据并求和&#xff0c;具体是如何操作的呢&#xff1f;下面就是小编带来的excel合并同类项数据并求和的操作步骤&#xff0c;很简单哦&#xff0c;看完之后你也来试试吧! 先看一下原…

反向传播 — 简单解释

一、说明 关于反向传播&#xff0c;我有一个精雕细刻的案例计划&#xff0c;但是实现了一半&#xff0c;目前没有顾得上继续充实&#xff0c;就拿论文的叙述这里先起个头&#xff0c;我后面将修改和促进此文的表述质量。 二、生物神经元 大脑是一个由大约100亿个神经元组成的复…

关于tcp协议

目录 前言&#xff1a; 一、TCP协议的基本概念&#xff1a; 二、TCP协议的主要特点&#xff1a; 2.1面向连接&#xff1a; 2.2可靠传输&#xff1a; 2.3基于字节流&#xff1a; 三、TCP连接的建立与终止&#xff1a; 3.1连接建立&#xff1a; 3.1.1SYN&#xff1a; 3…

【SystemVerilog】结构体真是太好用了~

前言 Verilog最大的缺陷之一是没有数据结构。在SystemVerilog中可以使用struct创建结构&#xff0c;struct只是把数据组织到一起&#xff0c;是数据的集合&#xff0c;所以是可综合的。 结构体是可以通过模块接口进行传递的&#xff0c;这就是本文想描述的内容。 一、结构体的…

YOLOv7改进 | 更换主干网络之PP-LCNet

前言:Hello大家好,我是小哥谈。PP-LCNet是一个由百度团队针对Intel-CPU端加速而设计的轻量高性能网络。它是一种基于MKLDNN加速策略的轻量级卷积神经网络,适用于多任务,并具有提高模型准确率的方法。与之前预测速度相近的模型相比,PP-LCNet具有更高的准确性。此外,对于计…

字符串函数(C语言详解)

1.字符串简介 字符串是一串连续的且以\0结尾的字符 char arr[]"zhangsan";//将字符串存到数组里面 char*a"lisi";//常量字符串 char arr1[]{z,h,a,n,g};//字符数组 注意&#xff1a; 1.以第一种形式初始化字符串时&#xff0c;计算机会自动在字符串末尾加…

指针(1)

1.内存和地址 1.1 内存 生活中我们有了房间号才能够快速找到房间&#xff0c;同样&#xff0c;在计算机中CPU&#xff08;中央处理器&#xff09;在处理数据时&#xff0c;需要的数据是在内存中进行读取的&#xff0c;处理完之后又会放回内存中。 在内存空间中&#xff0c…

OJ_最长公共子序列

题干 C实现 #include <iostream> #include <stdio.h> #include <algorithm> using namespace std;int dp[1002][1002];int main() {int n,m;char s1[1001];char s2[1001];scanf("%d%d",&n,&m);scanf("%s%s",s1,s2);//dp[i][j]是…

拼多多、淘宝、抖音、小红书商家,如何轻松在1688找到靠谱货源?

无论你是做拼多多、淘宝、抖音小店、小红书或者1688运营及采购商们&#xff0c;只要想在1688上寻找靠谱货源时&#xff0c;可以按照以下几个步骤进行筛选&#xff1a; 一、明确需求 首先&#xff0c;你需要清晰地了解自己的经营方向、目标消费群体以及所需产品的具体规格、材…

可变形卷积v4 |更快更强,效果远超DCNv3

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;助力高效涨点&#xff01;&#xff01;&#xff01; 一、论文摘要 我们介绍了可变形卷积v4 (DCNv4)&#xff0c;这是一种高效的算子&#xff0c;专为广泛的视觉应用而设计。DCNv4通过两个关键增强解决了…

26.网络游戏逆向分析与漏洞攻防-网络通信数据包分析工具-实现生成日志文件的功能

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果 内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;25.利用全新的通…

PTA- - -个位数统计(C语言)

Hello,好久没更新啦&#xff0c;今天给大家讲解一下PTA平台上面的“个位数统计”这道题吧~ 题目是要统计一个数字每个位上数字出现的次数。下面是一个解决方案的思路和相应的 C 语言代码&#xff1a; 思路&#xff1a; 初始化一个大小为10的数组&#xff0c;用于计数每个数字…

【LeetCode】升级打怪之路 Day 24:回溯算法的解题框架

今日题目&#xff1a; 46. 全排列51. N 皇后78. 子集 目录 LC 46. 全排列LC 51. N 皇后LC 78. 子集 【classic】1&#xff09;思路一2&#xff09;思路二 今天学习了回溯算法的解题框架&#xff1a;回溯算法解题套路框架 | labuladong 回溯算法的整体框架都是&#xff1a; re…