三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Spring Boot中HttpServletRequestWrapper原理与应用实战

Spring Boot中HttpServletRequestWrapper原理与应用实战

1. 项目概述:为什么我们需要包装HttpServletRequest?

在Spring Boot项目中处理HTTP请求,HttpServletRequest对象是我们最熟悉的老朋友。它封装了客户端发来的所有信息:请求参数、请求头、请求体、会话信息等等。但你是否遇到过这样的场景:你想在请求到达Controller之前,先偷偷看一眼请求体里的内容,或者干脆把它替换掉?又或者,你想在请求被处理之后,记录下原始的请求参数,但发现参数已经被后续的过滤器或拦截器修改了?这时候,直接操作原始的HttpServletRequest对象就显得力不从心,甚至有些危险,因为它是一个“一次性”的流对象,读取后就不能再读了。

HttpServletRequestWrapper就是为了解决这类问题而生的“包装器”。它本质上是一个设计模式(装饰器模式)在Servlet API中的具体实现。简单来说,它允许你创建一个HttpServletRequest的“替身”。这个替身对外表现得和原对象一模一样,所有方法调用都会委托给内部包装的原始请求对象。但关键在于,你可以在委托的过程中“动手脚”——覆盖(Override)某些关键方法,实现自定义的逻辑,比如缓存请求体、修改请求参数、或者记录请求信息。

想象一下,你是一个快递分拣员(过滤器/拦截器),包裹(请求)经过你手时,你需要检查里面的物品(请求体)。但公司规定,你不能直接拆开客户的包裹(直接读取InputStream)。HttpServletRequestWrapper就像给你提供了一个透明的、可复制的扫描仪。你可以用这个扫描仪在不破坏原包裹的情况下,看到里面的内容,甚至有条件地替换掉某些物品,然后再把包裹原样(或修改后)传递给下一个环节(Controller)。这个能力,在实现请求/响应日志、参数解密、防XSS攻击、API版本控制等中间件功能时,至关重要。

2. 核心设计思路与方案选型

使用HttpServletRequestWrapper的核心思路是“拦截并增强”。我们通常在Servlet Filter或Spring的HandlerInterceptor中创建这个包装器,并将其放入请求处理链中,替换掉原始的HttpServletRequest对象。这样,后续所有处理环节拿到的都是我们增强过的“替身”。

2.1 为何选择包装器模式而非直接修改?

你可能会问,我直接在Filter里把请求参数读出来存到Attribute里,或者用反射去改HttpServletRequest的内部状态不行吗?理论上,对于简单场景或许可以,但这会带来几个严重问题:

  1. 破坏性操作HttpServletRequestgetInputStream()getReader()方法只能调用一次。一旦在某个Filter中读取了流,后续的Filter或Controller再读取就会得到空流,导致业务逻辑出错。包装器模式的核心价值之一就是解决这个“流只能读一次”的问题。
  2. 侵入性强:直接操作原生对象,需要深入了解其内部实现,代码与Servlet容器(如Tomcat)耦合度高,不同容器可能有差异,容易出错且难以维护。
  3. 功能单一:直接修改往往只能解决特定问题(如记录日志),而包装器可以集中、模块化地实现多种增强功能(如解密+缓存+日志),并且这些功能可以像乐高积木一样组合。

因此,选择HttpServletRequestWrapper是遵循了“开闭原则”(对扩展开放,对修改关闭)的最佳实践。我们通过扩展(继承包装器)来增加新功能,而不是修改Servlet容器提供的原始对象。

2.2 关键方法覆盖策略

HttpServletRequestWrapper实现了HttpServletRequest接口,并持有一个HttpServletRequest实例的引用,所有方法默认都委托给这个实例。我们需要覆盖哪些方法,取决于我们要增强什么功能:

  • 缓存请求体:必须覆盖getInputStream()getReader()。在这两个方法中,我们首次调用时,将原始流的数据读取并缓存到字节数组或字符串中。后续再调用时,直接返回基于缓存数据构造的新流。这是最经典、最常用的覆盖场景。
  • 修改请求参数:需要覆盖getParameter(String name),getParameterValues(String name),getParameterMap()等方法。我们可以在包装器内部维护一个修改后的参数映射,在这些方法被调用时返回我们处理后的值。常用于统一参数解密、过滤非法字符等。
  • 增强请求头:覆盖getHeader(String name),getHeaders(String name),getHeaderNames()等方法。可以用于添加自定义头信息,或者根据某些逻辑动态返回头值。
  • 其他属性:如getRequestURI(),getServletPath()等,在某些路由重写或API版本控制的场景下也可能需要覆盖。

注意:覆盖方法时,务必考虑线程安全性。特别是在高并发场景下,如果包装器内部有可变的缓存数据,需要确保其访问是线程安全的。通常,每个请求都会创建一个新的包装器实例,所以实例变量是线程隔离的,但也要注意不要无意中引入了静态变量等共享状态。

3. 核心细节解析与实操要点

理解了为什么用和怎么设计,我们深入到几个核心的实现细节。这些细节决定了你的包装器是否健壮、高效。

3.1 请求体缓存的正确姿势

缓存请求体是包装器最核心的功能,但如何缓存却大有讲究。一个常见的错误是,在包装器的构造函数里就去读取流。切记,不要在构造函数中读取请求体!因为Filter链可能很长,包装器被创建时,可能还没有其他Filter需要读取原始流,过早读取会破坏链式传递。

正确的做法是“懒加载”(Lazy Loading)。我们只在getInputStream()getReader()第一次被调用时,才去读取并缓存原始流的数据。

实操示例:一个标准的可重复读取的请求包装器

import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.StandardCharsets; public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { private byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); // 构造函数里只做初始化,不读流 } @Override public ServletInputStream getInputStream() throws IOException { if (cachedBody == null) { // 首次调用,缓存请求体 cacheInputStream(); } // 返回一个基于缓存字节数组的新流 return new CachedBodyServletInputStream(this.cachedBody); } @Override public BufferedReader getReader() throws IOException { // 确保字符编码正确,这里使用UTF-8,可根据实际情况调整 return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding())); } private void cacheInputStream() throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesRead; // 获取原始请求的输入流 InputStream inputStream = super.getInputStream(); while ((bytesRead = inputStream.read(buffer)) != -1) { byteArrayOutputStream.write(buffer, 0, bytesRead); } cachedBody = byteArrayOutputStream.toByteArray(); } // 提供一个获取缓存内容的方法,方便后续使用(如日志、解密) public String getBody() { return new String(cachedBody, StandardCharsets.UTF_8); } // 自定义的ServletInputStream,用于包装缓存的字节数组 static class CachedBodyServletInputStream extends ServletInputStream { private final ByteArrayInputStream byteArrayInputStream; public CachedBodyServletInputStream(byte[] cachedBody) { this.byteArrayInputStream = new ByteArrayInputStream(cachedBody); } @Override public boolean isFinished() { return byteArrayInputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { throw new UnsupportedOperationException(); } @Override public int read() throws IOException { return byteArrayInputStream.read(); } } }

要点解析

  1. cacheInputStream()方法实现了流的读取和缓存。这里使用了ByteArrayOutputStream来动态扩容,避免一次性分配过大内存。缓冲区大小1024是一个经验值,可根据典型请求体大小调整。
  2. CachedBodyServletInputStream是一个内部类,它继承了ServletInputStream并重写了必要的方法。isFinished()isReady()方法对于异步处理很重要,这里做了简单实现。setReadListener在同步场景下通常不需要,直接抛出异常或空实现即可。
  3. getBody()方法是一个工具方法,它将缓存的字节数组转换为字符串。这里有一个大坑:字符编码。我们使用了StandardCharsets.UTF_8,这是目前Web API最通用的编码。但在实际项目中,更严谨的做法是使用请求头Content-Type中指定的charset,或者使用super.getCharacterEncoding()(如果请求设置了的话)。如果编码不对,中文字符就会出现乱码。

3.2 在Filter中集成包装器

创建了包装器,下一步就是把它“安装”到请求处理流程中。我们通过一个自定义Filter来实现。

import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Component @Order(1) // 指定Filter的执行顺序,数字越小优先级越高。确保它在业务逻辑Filter之前执行。 public class CachingRequestBodyFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; // 关键判断:通常我们只对需要读取Body的请求(如POST, PUT, PATCH)进行包装 String method = httpServletRequest.getMethod(); String contentType = httpServletRequest.getContentType(); boolean isRequestBodyExpected = "POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method); if (isRequestBodyExpected && contentType != null && contentType.contains("application/json")) { // 创建包装器,替换原始请求 CachedBodyHttpServletRequest cachedBodyRequest = new CachedBodyHttpServletRequest(httpServletRequest); // 将包装后的请求继续传递 filterChain.doFilter(cachedBodyRequest, servletResponse); } else { // 对于GET等请求,直接放行 filterChain.doFilter(servletRequest, servletResponse); } } }

实操心得

  • 性能考量:包装和缓存整个请求体是有开销的(内存和CPU)。因此,一定要像上面代码一样,通过请求方法和内容类型进行过滤。对于简单的GET请求或上传文件的multipart/form-data请求,通常没有必要包装。对于文件上传,包装整个流可能消耗巨大内存,需要特殊处理(例如,只包装非文件部分)。
  • Filter顺序:使用@Order注解或FilterRegistrationBean明确指定Filter的顺序至关重要。这个缓存Filter应该放在所有可能需要读取请求体的业务Filter(如日志、认证、解密Filter)之前,但可以放在一些不关心请求体的Filter(如CORS Filter)之后。
  • 异常处理:在cacheInputStream()过程中,IO异常需要妥善处理。通常,如果连请求体都无法读取,后续业务也无从谈起,可以直接抛出异常或返回错误响应。

4. 典型应用场景实战剖析

理论说再多,不如看实战。下面我们通过几个具体场景,看看HttpServletRequestWrapper如何大显身手。

4.1 场景一:全局请求/响应日志记录

这是一个刚需功能,用于问题排查和审计。没有包装器,你只能在Controller里记录,但那样会遗漏Filter中的逻辑,且代码侵入性强。

实现思路

  1. 创建包装器CachedBodyHttpServletRequest(同上)。
  2. 在Filter中,对需要记录的请求(如特定路径、非静态资源)创建包装器。
  3. filterChain.doFilter()之前,记录请求信息(URL、方法、头、缓存后的请求体)。
  4. 为了记录响应体,我们还需要一个HttpServletResponseWrapper来包装响应,缓存写出的数据。这里先聚焦请求。
  5. 将包装后的请求和响应传递下去。

日志Filter增强版

@Component @Slf4j // 使用Lombok public class LoggingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String path = httpRequest.getRequestURI(); // 排除不需要日志的请求,如健康检查、静态资源 if (path.startsWith("/actuator/health") || path.contains(".")) { chain.doFilter(request, response); return; } long startTime = System.currentTimeMillis(); CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest); // 记录请求 log.info("Request: {} {}, Headers: {}, Body: {}", httpRequest.getMethod(), path, getHeadersMap(httpRequest), cachedRequest.getBody()); // 这里可以安全地调用getBody() // 继续执行过滤器链和业务逻辑 chain.doFilter(cachedRequest, response); long duration = System.currentTimeMillis() - startTime; HttpServletResponse httpResponse = (HttpServletResponse) response; log.info("Response: Status={}, TimeTaken={}ms", httpResponse.getStatus(), duration); } private Map<String, String> getHeadersMap(HttpServletRequest request) { Map<String, String> headersMap = new HashMap<>(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); headersMap.put(headerName, request.getHeader(headerName)); } return headersMap; } }

注意:在生产环境,全量记录请求/响应体日志可能会产生巨大的磁盘I/O和存储成本,并可能泄露敏感信息(如密码、令牌)。务必做好以下工作:1) 采样记录,例如只记录1%的请求;2) 对敏感字段(如password,token)进行脱敏;3) 将日志级别设为DEBUG,并在生产环境关闭。

4.2 场景二:请求参数统一解密或过滤

在一些安全要求高的场景,客户端可能对请求参数或体进行加密后传输。服务端需要在进入业务逻辑前统一解密。

实现思路

  1. 创建包装器DecryptHttpServletRequestWrapper,继承自HttpServletRequestWrapper
  2. 覆盖getParameter*系列方法和getInputStream()/getReader()
  3. 在覆盖的方法中,先获取原始值,然后调用解密逻辑,返回解密后的值。
  4. 在Filter中应用此包装器。

参数解密包装器示例

public class DecryptHttpServletRequestWrapper extends HttpServletRequestWrapper { private final Map<String, String[]> decryptedParameterMap; private byte[] decryptedBody; public DecryptHttpServletRequestWrapper(HttpServletRequest request, DecryptService decryptService) throws Exception { super(request); this.decryptedParameterMap = new HashMap<>(super.getParameterMap()); // 1. 解密URL参数 (GET请求或POST的form-data) for (Map.Entry<String, String[]> entry : super.getParameterMap().entrySet()) { String[] encryptedValues = entry.getValue(); String[] decryptedValues = new String[encryptedValues.length]; for (int i = 0; i < encryptedValues.length; i++) { // 假设decrypt方法是你的解密服务 decryptedValues[i] = decryptService.decrypt(encryptedValues[i]); } this.decryptedParameterMap.put(entry.getKey(), decryptedValues); } // 2. 解密请求体 (JSON等) if ("POST".equalsIgnoreCase(request.getMethod()) || "PUT".equalsIgnoreCase(request.getMethod())) { // 先读取并缓存原始体 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ServletInputStream inputStream = super.getInputStream(); byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) > -1) { baos.write(buffer, 0, len); } baos.flush(); byte[] encryptedBodyBytes = baos.toByteArray(); // 解密整个请求体字符串 String encryptedBody = new String(encryptedBodyBytes, StandardCharsets.UTF_8); String decryptedBodyStr = decryptService.decrypt(encryptedBody); this.decryptedBody = decryptedBodyStr.getBytes(StandardCharsets.UTF_8); } } @Override public String getParameter(String name) { String[] values = decryptedParameterMap.get(name); return values != null && values.length > 0 ? values[0] : null; } @Override public Map<String, String[]> getParameterMap() { return Collections.unmodifiableMap(this.decryptedParameterMap); } @Override public ServletInputStream getInputStream() throws IOException { if (decryptedBody == null) { return super.getInputStream(); // 非POST/PUT请求,返回原始流 } return new CachedBodyServletInputStream(this.decryptedBody); // 使用之前定义的内部类 } // ... 同样需要覆盖 getParameterValues, getReader 等方法 }

关键点:这个包装器在构造函数中就完成了所有解密工作。这是因为参数解密通常需要立即进行,且解密后的参数映射是固定的。这与缓存请求体的“懒加载”模式不同。你需要评估解密操作的成本,如果非常耗时,可能需要考虑异步或延迟解密。

4.3 场景三:防御XSS攻击(输入清洗)

防止跨站脚本攻击,可以在参数入口处进行全局过滤。

实现思路

  1. 创建XssFilterHttpServletRequestWrapper
  2. 覆盖getParameter*,getHeader*等方法。
  3. 在这些方法返回数据前,使用如JsoupAntisamy等库对字符串进行清理,移除或转义潜在的恶意脚本标签。
public class XssFilterHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssFilterHttpServletRequestWrapper(HttpServletRequest request) { super(request); } @Override public String getParameter(String name) { String value = super.getParameter(name); return cleanXss(value); } @Override public String[] getParameterValues(String name) { String[] values = super.getParameterValues(name); if (values == null) { return null; } String[] cleanedValues = new String[values.length]; for (int i = 0; i < values.length; i++) { cleanedValues[i] = cleanXss(values[i]); } return cleanedValues; } @Override public String getHeader(String name) { String value = super.getHeader(name); return cleanXss(value); } private String cleanXss(String value) { if (value == null) { return null; } // 使用Jsoup进行简单的HTML清理,允许安全的标签(如果需要) // 更严格的安全策略建议使用OWASP Java HTML Sanitizer return Jsoup.clean(value, Whitelist.basic()); // 或者进行转义:return StringEscapeUtils.escapeHtml4(value); // Apache Commons Text } }

注意事项:XSS过滤的粒度需要仔细设计。过于严格可能会破坏合法的HTML内容(如富文本编辑器提交的内容)。通常,对于普通表单字段使用严格过滤,对于特定的、已知的富文本字段,可以设置白名单或跳过过滤。

5. 高级话题与性能优化

当你的应用流量变大时,一个不经意的包装器可能成为性能瓶颈。下面探讨几个进阶问题。

5.1 包装器与Spring MVC的集成陷阱

你可能会发现,在使用了自定义的HttpServletRequestWrapper后,Spring MVC的@RequestBody注解或者MultipartFile上传不好用了。

  • @RequestBody绑定问题:Spring MVC在解析@RequestBody时,会调用HttpServletRequestgetInputStream()。只要你按照我们上面的方式正确覆盖了该方法,并确保了流的可重复读取,就不会有问题。问题往往出在Filter链的顺序上,确保你的包装器Filter在Spring的HiddenHttpMethodFilterFormContentFilter等内置Filter之后执行(它们的顺序通常很高),但在你自己的业务Filter之前。
  • MultipartFile上传失效:这是一个经典坑。当请求内容类型是multipart/form-data时,Spring会通过MultipartResolver(如StandardServletMultipartResolver)将请求解析为MultipartHttpServletRequest。如果你在解析之前就用包装器读取了整个输入流,那么MultipartResolver就无流可读,导致getFile()返回null。
    • 解决方案:在包装器Filter中,对contentType包含multipart/form-data的请求直接放行,不做包装和缓存。或者,你可以实现更复杂的包装器,只缓存非文件部分,但这实现难度较高。通常,文件上传的日志记录可以通过其他方式实现(如监听器或AOP),而不是通过包装请求体。

5.2 异步请求(AsyncContext)下的处理

在Servlet 3.0+的异步处理中,请求和响应对象可能在Filter链结束后仍然被使用。如果你的包装器缓存了数据,需要确保这些缓存的数据在异步线程中访问是安全的。通常,每个请求的包装器实例是独立的,所以没有问题。但要特别注意,不要在包装器中缓存引用到可能被其他线程修改的对象(如ServletContext)。

5.3 内存与性能优化策略

  1. 选择性包装:如前所述,通过请求方法、路径、内容类型等条件,严格限制需要包装的请求范围。对于GETHEADOPTIONS等方法以及静态资源请求,坚决不包装。
  2. 控制缓存大小:可以为缓存设置一个上限。如果请求体超过这个上限(比如10MB),则可以选择不缓存,或者只记录元数据(如大小),避免OOM。
    private void cacheInputStream() throws IOException { int maxCacheSize = 10 * 1024 * 1024; // 10MB ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; // 稍大的缓冲区可能更好 int bytesRead; InputStream is = super.getInputStream(); while ((bytesRead = is.read(buffer)) != -1) { baos.write(buffer, 0, bytesRead); if (baos.size() > maxCacheSize) { // 超过限制,可以抛出异常、清空缓存或采取其他策略 cachedBody = null; throw new IOException("Request body is too large to cache."); } } cachedBody = baos.toByteArray(); }
  3. 使用更高效的数据结构:对于非常大的请求体,如果只是为了日志,可以考虑使用临时文件而不是内存。但这会增加磁盘I/O,需要权衡。
  4. 采样与降级:在高并发压力下,可以考虑动态采样,比如只对1%的请求进行全量日志记录,或者根据请求的特定特征(如错误状态码)决定是否记录详细内容。

6. 常见问题排查与调试技巧

在实际集成HttpServletRequestWrapper时,你肯定会遇到一些“诡异”的问题。下面是我踩过的一些坑和解决方法。

6.1 问题:包装后获取不到请求参数或请求体为空

排查步骤

  1. 检查Filter顺序:这是最常见的原因。确保你的包装器Filter在Spring Boot中注册的顺序是正确的。使用@Order注解或通过FilterRegistrationBean手动注册并设置setOrder(int)。记住,order值越小,优先级越高,越先执行。你的包装器需要在任何可能读取请求体的组件之前执行。
  2. 检查请求类型:在Filter的doFilter方法开始处打印请求的Content-Type和方法。确认你正在处理的请求确实是你期望的类型(如application/json)。有些前端框架或网关可能会添加或修改Content-Type
  3. 调试包装器:在包装器的getInputStream()和构造函数中打上断点或日志,看它是否被调用,以及缓存过程是否成功。
  4. 确认包装器被传递:在Filter中,确保你调用的是filterChain.doFilter(wrappedRequest, response),而不是filterChain.doFilter(originalRequest, response)

6.2 问题:中文乱码

原因与解决: 乱码几乎总是字符编码不一致导致的。

  1. 请求体编码:在将缓存的字节数组cachedBody转换为字符串时(如在getBody()方法中),必须使用正确的编码。优先使用request.getCharacterEncoding(),如果为null,则回退到UTF-8。但更可靠的是从Content-Type头中解析charset
    public String getBody() { if (cachedBody == null) { return null; } String charset = super.getCharacterEncoding(); if (charset == null) { charset = StandardCharsets.UTF_8.name(); } return new String(cachedBody, Charset.forName(charset)); }
  2. 响应编码:如果你也包装了响应并修改了内容,同样需要注意设置正确的Content-Type和字符编码。

6.3 问题:性能开销明显,接口响应变慢

分析与优化

  1. 使用性能分析工具:用Arthas、JProfiler或Spring Boot Actuator的metrics端点,定位耗时最长的Filter。
  2. 审查缓存条件:你的条件判断是否足够严格?是否包装了大量本不需要包装的请求(如图片、CSS、JS)?添加更精确的路径排除逻辑。
  3. 检查缓存大小:是否缓存了过大的请求体(如文件上传)?实现大小限制逻辑。
  4. 日志级别:确保生产环境中,全量请求/响应体日志的级别是DEBUGTRACE,而不是INFO

6.4 一个简易的调试Filter

当你怀疑包装器没生效时,可以临时添加一个最简化的调试Filter来验证流程:

@Component @Order(Integer.MIN_VALUE) // 确保它第一个执行 public class DebugFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; System.out.println("=== Debug Filter ==="); System.out.println("URI: " + req.getRequestURI()); System.out.println("Content-Type: " + req.getContentType()); System.out.println("Class: " + req.getClass().getName()); // 查看是否是包装类 // 尝试读取参数(谨慎,可能消费流) System.out.println("Param: " + req.getParameter("test")); chain.doFilter(request, response); } }

通过这个Filter,你可以看到请求最初的样子,以及它经过你的包装器Filter后是否变成了包装类的实例。

HttpServletRequestWrapper是一个强大的工具,但它不是银弹。它增加了请求处理的复杂度,并带来一定的性能开销。我的经验是,在明确需要拦截并修改请求信息时才使用它,并且要像对待数据库连接池一样,谨慎地管理其生命周期和资源消耗。对于简单的属性记录,使用HttpServletRequestsetAttribute/getAttribute方法在Filter链中传递信息,往往是更轻量级的选择。理解其原理,明确其边界,才能让这个“包装器”在Spring Boot架构中恰到好处地发挥作用。

← 返回列表