SpringBoot之文件操作记录存储服务

概述

应公司安全管理部门政策要求,需要实现文件上传/下载操作的日志记录,经过分析需要在目前平台上基于springboot搭建一套服务供其他应用具体业务调用,其中该服务涉及到的技术支撑:AOP实现异常处理、queue+spring-scheduler异步执行定时任务、Fegin组件进行服务间通信(通过拦截器设置请求头中token认证信息)

功能实现

AOP进行异常处理

(另外基于AOP实现了RBAC-基于角色的的访问控制)

package test.bs.web.aspect;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import test.common.exception.*;
import test.common.util.Page;
import test.common.util.R;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;import javax.validation.ConstraintViolationException;
import java.lang.reflect.Method;/*** * @ClassName: ExceptionHandlerAspect* @Description: 统一异常处理* @author test**/
@Aspect
@Component
public class ExceptionHandlerAspect {private Logger logger = LoggerFactory.getLogger(ExceptionHandlerAspect.class);private ObjectMapper om = new ObjectMapper();/*** @Title: inController* @Description: 以test开头, controller结尾的包名下任何类的所有方法*/	    @Pointcut("execution(* test..*.controller..*.*(..))")public void inController() {}/*** @Title: inViewController* @Description: 以test开头, view结尾的包及子包名下任何类的所有方法法(Controller与view 不能相互包含关键字)*/	    @Pointcut("execution(* test..*.view_controller..*.*(..))")public void inViewController() {}/*** * @Title: handleAPIControllerMethod* @Description: api 异常处理器* @param pjp* @return* @throws Throwable*/@Around("inController()")public Object handleAPIControllerMethod(ProceedingJoinPoint pjp) throws Throwable {Object r;try {r = pjp.proceed(pjp.getArgs());} catch(Exception e) {logger.error(e.getMessage());e.printStackTrace();r  = handleAPIControllerException(pjp, e);}return r;} @Around("inViewController()")public ModelAndView handleViewControllerMethod(ProceedingJoinPoint pjp) throws Throwable{ModelAndView mv;try {mv = (ModelAndView)pjp.proceed(pjp.getArgs());} catch(Exception e) {logger.error(e.getMessage());e.printStackTrace();mv = handleViewControllerException(pjp, e);}return mv;}/*** * @Title: handleAPIControllerException* @Description: 具体API异常处理* @param pjp* @param e* @return* @throws IllegalAccessException * @throws InstantiationException */@SuppressWarnings("unchecked")private Object handleAPIControllerException(ProceedingJoinPoint pjp, Throwable e) throws InstantiationException, IllegalAccessException {Object r = null;MethodSignature signature = (MethodSignature) pjp.getSignature();// 获取method对象 Method method = signature.getMethod();// 获取方法的返回值的类型Class returnType=   method.getReturnType();// 根据自定义常见的异常抛出异常信息R<String> eR = getExceptionMessage(e);if( returnType.equals(R.class) ) {r = new R(null, eR.getMsg(), eR.getCode());} else if( returnType.equals(Page.class)) {r = new Page(null, eR.getMsg(), eR.getCode());} else {r = returnType.newInstance(); // 约定必须有默认的构造函数}return r;}/*** * @Title: handleViewControllerException* @Description: 捕获ModelAndView 异常* @param pjp* @param e* @return*/private ModelAndView handleViewControllerException(ProceedingJoinPoint pjp, Throwable e) {ModelAndView mv = new ModelAndView();mv.setViewName("error");// 根据自定义常见的异常抛出异常信息R<String> eR = getExceptionMessage(e);mv.addObject("status", eR.getCode()).addObject("exception", e.getClass()).addObject("error", eR.getData())  .addObject("message", eR.getMsg());return mv;}/**** @Title: getExceptionMessage* @Description: 获取异常信息* @param e* @return*/private R<String> getExceptionMessage(Throwable e) {R<String> rst = new R<>();if( e instanceof NullPointerException ){rst.setCode(R.FAIL);rst.setData(null);rst.setMsg(e.getMessage());} else if (e instanceof RequestParameter400Exception) {rst.setCode(R.INPUT_ERROR_400);rst.setData(null);rst.setMsg("The system has rejected your operation for the following reason:" + e.getMessage());} else if( e instanceof AuthorizationFailedException) {rst.setCode(R.NO_LOGIN);rst.setData(null);rst.setMsg("The system has rejected your operation for the following reason:" + e.getMessage());} else if(e instanceof NoPermissioinException) {rst.setCode(R.NO_PERMISSION);rst.setData(null);rst.setMsg("The system has rejected your operation for the following reason:" + e.getMessage());} else if( e instanceof NotFoundException) {rst.setCode(R.NOT_FOUND);rst.setData(null);rst.setMsg("The system has rejected your operation for the following reason:" + e.getMessage());} ...return rst;}
}

queue+任务调度Scheduler

  • 保存文件操作历史记录到队列
package test.bs.internal.ws.queue;import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import test.bs.pojo.po.HistoryRecord;/*** * @ClassName: SaveHistoryRecordsQueue* @Description: 暂保存插入数据库DB的历史记录的队列**/
public class SaveHistoryRecordsQueue {/*** 队列大小 */private static int QUEUE_MAX_SIZE = 80000;/*** 共享队列,使用线程安全的Queue,设置队列最大大小=80000:若不设置则超过内存大小则会导致应用因内存不足而崩溃*/private volatile Queue<FileOpHisRecords> pendingTodoQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);private static final Object lockedObj = new Object();/*** 默认单例,使用volatile以标识获取当前变量时需要从内存获取:volatile的双重检测同步延迟载入模式*/private static volatile SaveHistoryRecordsQueue INSTANCE;private SaveHistoryRecordsQueue() {if(pendingTodoQueue == null) {pendingTodoQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);}}// volatile的双重检测同步延迟载入模式public static SaveHistoryRecordsQueue getInstance() {if(INSTANCE == null) {synchronized (lockedObj) {if(INSTANCE == null) {INSTANCE = new SaveHistoryRecordsQueue();}}}return INSTANCE;}/*** * @Title: getPendingTodoQueue* @Description: 获取待处理的队列* @return*/public Queue<FileOpHisRecords> getPendingTodoQueue() {return pendingTodoQueue;}	
}
  • 通过使用spring-schedule定时任务调度方式来异步执行队列中的任务以减少DB端的并发负载
package test.bs.internal.ws.schedule;import java.util.Queue;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import test.bs.internal.ws.queue.SaveHistoryRecordsQueue;
import test.bs.pojo.po.FileOpHisRecords;
import test.bs.service.FileOpHisRecordsService;/*** @ClassName: SaveHistoryRecordsScheduler* @Description: 实现异步保存文件操作历史记录排程以减少 DB 的并发负载*/
@Component
@RefreshScope
public class SaveHistoryRecordsScheduler {private Logger logger = LoggerFactory.getLogger(SaveHistoryRecordsScheduler.class);// 每执行一次排程最多保存操作历史记录的数量@Value("${test.bs.per.max.counter:30}")private final int maxProcessCounter = 30;@Resourceprivate FileOpHisRecordsService fileOpHisRecordsService; //实现历史记录插入DB的服务@Autowiredprivate ObjectMapper om;// 设置固定任务间隔频率(每次执行均以上次执行结束时间为参考间隔2s时间再执行)@Scheduled(fixedDelay = 2000) //设置2s秒钟執行一次protected void fixedRateSchedule() {Queue<FileOpHisRecords> queue = PendingToSaveFileOperationHistoryQueue.getInstance().getPendingTodoQueue();if (queue != null && !queue.isEmpty()) {int counter = 0;FileOpHisRecords todoItem = null;boolean insertSelective = false;while (!queue.isEmpty() && counter < maxProcessCounter) {counter++;todoItem = queue.peek(); //取队列最先添加队列中的元素(队列遵循先进先出)if (todoItem != null) {try {insertSelective = fileOpHisRecordsService.insertSelective(todoItem);if (!insertSelective) {// 打印日志logger.error("save operation historyRecord to database fail, todoItem: {}", om.writeValueAsString(todoItem));}} catch(Exception e) {logger.error("save operation historyRecord fail", e);}}queue.poll(); //移除当前元素}}}
}

Fegin组件进行服务调用

  • 涉及保存操作历史记录业务的客户端应用需要添加fegin组件以实现远程调用
    • 添加Fegin接口
package test.bs.service.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import test.bs.dto.request.FileOpHisDTO;
import test.bs.service.feign.impl.FileOpHisRecordFeignFallbackImpl;
import test.bs.service.feign.interceptor.TokenInterceptor;
import test.common.util.R;/*** @ClassName: FileOpHisRecordFeign* @Description: Fegin接口*             configuration = TokenInterceptor.class:TokenInterceptor作为请求拦截器,用于服* * 务间请求时进行安全认证(token)* * 注意:SaveHistoryRecordsScheduler保存操作记录方法位于test-internal-ws服务中,并且请求的url* *       是/test/fileHistoryRecord/addHistoryRecord
*/@FeignClient(value = "test-internal-ws", fallback = FileOpHisRecordFeignFallbackImpl.class, configuration = TokenInterceptor.class)
public interface FileOpHisRecordFeign {@PostMapping("/test/fileHistoryRecord/addHistoryRecord")R<Boolean> addFileHistoryRecordsOperate(@RequestBody FileOpHisDTO dto);}
  • 对于平台系统内部web应用(Web应用会自动带token访问),则可以通过Feign关联具体的微服务直接访问,此时Feign发送请求时会先通过拦截器类TokenInterceptor携带Token进行安全认证; 其他情景下发送请求时,则需要在Http请求过程中设置授权头(指定Token)
    • package test.bs.service.feign.interceptor;import java.util.Enumeration;
      import java.util.LinkedHashMap;
      import java.util.Map;import javax.servlet.http.HttpServletRequest;import org.apache.commons.lang3.StringUtils;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Component;
      import org.springframework.web.context.request.RequestContextHolder;
      import org.springframework.web.context.request.ServletRequestAttributes;
      import feign.RequestInterceptor;
      import feign.RequestTemplate;/*** * @ClassName: TokenInterceptor* @Description: Feign默认请求拦截器*				请求 Header*					Key: Authorization*					Value: Bearer + 有效的JWT-Token。*/
      @Component
      public class TokenInterceptor implements RequestInterceptor {private final static Logger logger = LoggerFactory.getLogger(TokenInterceptor.class);@AutowiredITokenService tokenService;@Overridepublic void apply(RequestTemplate template) {forwordIpInfo(template); //fegin请求时在请求头添加客户端IP信息// 判断请求头中是否含有tokenif (template.headers().containsKey("Authorization")|| template.headers().containsKey("authorization")|| template.headers().containsKey("AUTHORIZATION")) {logger.info("token existed");return;}try {String token = null;HttpServletRequest httpServletRequest = getHttpServletRequest();if( httpServletRequest != null ) { //从请求上下文获取tokentoken = getHeaders(getHttpServletRequest()).get("authorization"); //使用小写}if(StringUtils.isBlank(token) ) {// SSO(Single Sign On)单点登录逻辑token = tokenService.getOauthTokenStr();if( token != null ) {String authHeaderStr = "Bearer " + token;template.header("Bearer ", authHeaderStr); //请求头加入token} else {logger.error("get token fail , the token is null");}logger.info("get token from sso, token: {}", token);} else {//上下文获取到的token直接加入到请求头template.header("Authorization", token);logger.info("get token from current user");}} catch(Exception e) {e.printStackTrace();logger.error("Get token Exception", e);}}/*** * @Title: forwordIpInfo* @Description: 请求头添加客户端IP信息* @param template*/private void forwordIpInfo(RequestTemplate template) {HttpServletRequest httpServletRequest = getHttpServletRequest();if( httpServletRequest != null ) {Map<String, String> headers = getHeaders(getHttpServletRequest());template.header("x-forwarded-for", headers.get("x-forwarded-for"));template.header("x-real-ip", headers.get("x-real-ip"));}}private HttpServletRequest getHttpServletRequest() {  try {  return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();  } catch (Exception e) {  return null;  }  }  private Map<String, String> getHeaders(HttpServletRequest request) {  Map<String, String> map = new LinkedHashMap<>();  if( request == null ) {return map;}Enumeration<String> enumeration = request.getHeaderNames();  while (enumeration.hasMoreElements()) {  String key = enumeration.nextElement();  String value = request.getHeader(key);  map.put(key, value);  }  return map;  }  }
  • 实现Fegin接口备用类
    • package test.bs.service.feign.impl;import test.bs.dto.request.FileOpHisDTO;
      import test.bs.service.feign.FileOpHisRecordFeign;
      import test.common.util.R;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.stereotype.Component;/*** @ClassName: FileOpHisRecordFeignFallbackImpl* @Description: 备用类--当Feign客户端调用远程服务失败时,会使用这个备用类*/
      @Component
      public class FileOpHisRecordFeignFallbackImpl implements FileOpHisRecordFeign {private final Logger logger = LoggerFactory.getLogger(FileOpHisRecordFeignFallbackImpl.class);@Overridepublic R<Boolean> addFileHistoryRecordsOperate(FileOpHisDTO dto) {logger.error("Call【test-internal-ws -> addFileHistoryRecordsOperate】exception");return new R<Boolean>(false, "Call【test-internal-ws -> addFileHistoryRecordsOperate】exception", R.FAIL);}
      }
  • Feign调用

在需要调用该方法的业务直接调fileOpHisFeign.addFileHistoryRecordsOperate(historyRecordDto)即可实现将文件操作记录存储到数据库,后续可以通过查询将记录以web方式进行展示~

扩展

  • Fegin如何实现负载均衡?

    • Fegin的负载均衡是通过集成Ribbon来实现的,Ribbon是Netflix开源的一个客户端负载均衡器,可以与Fegin无缝集成,为Fegin提供负载均衡能力。
      • Ribbon发起请求流程
        • 首先Ribbon在发起请求前,会从"服务中心"获取服务列表,然后按照一定的负载均衡策略发起请求,从而实现客户端的负载均衡.Ribbon本身也会缓存一份"服务提供者"清单并维护他的有效性,若发现"服务提供者"不可用,则会重新从"服务中心"获取有效的"服务提供者"清单来及时更新
  • Fegin如何实现认证传递?
    • 因为微服务之间通信是不会携带请求相关信息的,所以当我们需要在服务间传递安全认证信息时,常见做法是使用拦截器传递认证信息,我司是通过实现RequestInterceptor接口来定义TokenInterceptor拦截器,在拦截器里将认证信息添加到请求头中,然后将其注册到Fegin的配置中来实现的(详细可见上面添加Fegin接口处代码描述!!!)
  • Fegin如何设置超时和重试?
feign:hystrix:enabled: true    httpclinet: #use okhttp will betterenabled: falseokhttp: enabled: trueribbon:  ReadTimeout: 15000  # 15s 读超时ConnectTimeout: 15000 # 15s 连接超时MaxAutoRetries: 0     #重试次数,我司不允许重试,因为有可能涉及到幂等性问题MaxAutoRetriesNextServer: 1MaxTotalConnections: 200  # okhttp及http-client最大连接数量默认为200MaxConnectionsPerHost: 50 # http-client下默认每台主机默认连接数量为50, okhttp无此配置

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TouchGFX 总结

文章目录 使用中文字体多屏幕间交换数据UI to MCUMCU to UI API文档参考横竖屏切换 使用中文字体 添加一个textArea&#xff0c;默认的英文文本可见&#xff0c;输入中文字体后就看不见了&#xff0c;是因为这个默认的字体不支持中文&#xff0c;改一下字体就可以了&#xff1…

全方位解析Node.js:从模块系统、文件操作、事件循环、异步编程、性能优化、网络编程等高级开发到后端服务架构最佳实践以及Serverless服务部署指南

Node.js是一种基于Chrome V8引擎的JavaScript运行环境&#xff0c;专为构建高性能、可扩展的网络应用而设计。其重要性在于革新了后端开发&#xff0c;通过非阻塞I/O和事件驱动模型&#xff0c;实现了轻量级、高并发处理能力。Node.js的模块化体系和活跃的npm生态极大加速了开发…

网络基础-网络设备介绍

本系列文章主要介绍思科、华为、华三三大厂商的网络设备 网络设备 网络设备是指用于构建和管理计算机网络的各种硬件设备和设备组件。以下是常见的网络设备类型&#xff1a; 路由器&#xff08;Router&#xff09;&#xff1a;用于连接不同网络并在它们之间转发数据包的设备…

深入理解网络原理2----UDP协议

文章目录 前言一、UDP协议协议段格式&#xff08;简图&#xff09;校验和 二、UDP与TCP 前言 随着时代的发展&#xff0c;越来越需要计算机之间互相通信&#xff0c;共享软件和数据&#xff0c;即以多个计算机协同⼯作来完成业务&#xff0c;就有了⽹络互连。 一、UDP协议 协…

java发送请求-http和https

http和https区别 1、http是网络传输超文本协议&#xff0c;client---- http------ server 2、httpshttpssl证书&#xff0c;让网络传输更安全 &#xff0c;client---- httpssl------ server 3、ssl证书是需要客户端认可的&#xff0c;注意官方证书和jdk生成的证书的用户来使…

【Github】将github仓库作为图床使用

创建github仓库 首先创建一个github仓库专门用于存储图片&#xff0c;具体步骤如下&#xff1a; 1.点击新的仓库按钮 2.初始配置&#xff1a;随便填写一个仓库名&#xff1b;这里的仓库状态一定要是public公开的&#xff0c;不然后面访问不了图片 下载PicGo PicGo官网 在A…

获取淘宝商品销量数据接口

淘宝爬虫商品销量数据采集通常涉及以下几个步骤&#xff1a; 1、确定采集目标&#xff1a;需要明确要采集的商品类别、筛选条件&#xff08;如天猫、价格区间&#xff09;、销量和金额等数据。例如&#xff0c;如果您想了解“小鱼零食”的销量和金额&#xff0c;您需要设定好价…

【面试经典 150 | 数组】文本左右对齐

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;模拟 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等内容进行回顾…

C语言 | Leetcode C语言题解之第61题旋转链表

题目&#xff1a; 题解&#xff1a; struct ListNode* rotateRight(struct ListNode* head, int k) {if (k 0 || head NULL || head->next NULL) {return head;}int n 1;struct ListNode* iter head;while (iter->next ! NULL) {iter iter->next;n;}int add n…

【LeetCode刷题记录】230. 二叉搜索树中第K小的元素

230 二叉搜索树中第K小的元素 给定一个二叉搜索树的根节点 root &#xff0c;和一个整数 k &#xff0c;请你设计一个算法查找其中第 k 个最小元素&#xff08;从 1 开始计数&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,1,4,null,2], k 1 输出&#xff1…

屏蔽罩材质和厚度对屏蔽效能的影响

​ 一&#xff0e;屏蔽效能的影响因素 屏蔽效能的影响因素主要有两个方面&#xff1a;屏蔽材料的特性和厚度&#xff1b;如下图所示&#xff0c;电磁波经过不同媒介时&#xff0c;会在分界面形成反射&#xff0c;穿过界面的电磁波一部分被反射回去&#xff0c;这部分能量损失…

音视频开发之旅——实现录音器、音频格式转换器和播放器(PCM文件转换为WAV文件、使用LAME编码MP3文件)(Android)

本文主要讲解的是实现录音器、音频转换器和播放器&#xff0c;在实现过程中需要把PCM文件转换为WAV文件&#xff0c;同时需要使用上一篇文章交叉编译出来的LAME库编码MP3文件。本文基于Android平台&#xff0c;示例代码如下所示&#xff1a; AndroidAudioDemo Android系列&am…

Leetcode—163. 缺失的区间【简单】Plus

2024每日刷题&#xff08;126&#xff09; Leetcode—163. 缺失的区间 实现代码 class Solution { public:vector<vector<int>> findMissingRanges(vector<int>& nums, int lower, int upper) {int n nums.size();vector<vector<int>> an…

docker部署nginx并配置https

1.准备SSL证书&#xff1a; 生成私钥&#xff1a;运行以下命令生成一个私钥文件。 生成证书请求&#xff08;CSR&#xff09;&#xff1a;运行以下命令生成证书请求文件。 生成自签名证书&#xff1a;使用以下命令生成自签名证书。 openssl genrsa -out example.com.key 2048 …

目标检测——铁路轨道故障数据集

一、重要性及意义 安全性保障&#xff1a;铁路作为重要的交通工具&#xff0c;其安全性能直接关系到乘客和货物的安全。铁路轨道故障&#xff0c;如裂缝、变形、错位、缺失紧固件等&#xff0c;都可能引发列车脱轨、倾覆等严重事故。因此&#xff0c;及时发现和修复这些故障&a…

【LLM第二篇】stable diffusion扩散模型、名词解释

最近在整理大模型的相关资料&#xff0c;发现了几个名词&#xff0c;不是很懂&#xff0c;这里整理一下&#xff1a; stable diffusion&#xff08;SD)模型&#xff1a; 扩散模型&#xff08;Diffusion model&#xff09;的一种&#xff0c;主要用于生成高质量的图像&#xf…

论文阅读笔记(AAAI 20)Order Matters

个人博客地址 注&#xff1a;部分内容参考自GPT生成的内容 论文笔记&#xff1a;Order Matters&#xff08;AAAI 20&#xff09; 用于二进制代码相似性检测的语义感知神经网络 论文:《Order Matters: Semantic-Aware Neural Networks for Binary Code Similarity Detection》…

MongoDB的分片集群

MongoDB分片技术 介绍 ​ 分片&#xff08;sharding&#xff09;是MongoDB用来将大型集合分割到不同服务器上采用的方法。分片这种说法起源于关系型数据库。但是实际上非关系型数据库在分片方面相比于传统的关系型数据库更有优势。 ​ 与MySQL分库方案对比&#xff0c;MongoDB…

Hibernate的QBC与HQL查询

目录 1、Hibernate的QBC查询 2、Hibernate的HQL查询 3、NatvieSQL原生查询 1、Hibernate的QBC查询 Hibernate具有一个直观的、可扩展的条件查询API public class Test { /** * param args */ public static void main(String[] args) { Session sessio…

Deep Learning Part Eight--Attention 24.5.4

01.在翻译、语音识别等将一个时序数据转换为另一个时序数据的任务中&#xff0c;时序数据之间常常存在对应关系 引入了Attention的概念&#xff0c;介绍了Attention的注意力机制&#xff1a; 困难出现&#xff0c;seq2seq的问题引入&#xff1a;固定化长度问题&#xff08;过于…