<think>我们正在处理一个JavaWeb开发问题:使用异步和拦截器实现HTTP请求和响应的日志记录。目标:通过拦截器(Interceptor)拦截请求和响应,并异步地记录日志(避免阻塞主线程)。步骤:1.创建拦截器类,实现HandlerInterceptor接口。2.在拦截器中,我们需要获取请求和响应的信息,然后将其记录到日志中。3.为了不阻塞请求处理,我们使用异步方式记录日志(例如,使用线程池或Spring的异步支持)。注意:由于日志记录可能涉及I/O操作(如写入文件或数据库),异步处理可以提升性能。设计思路:-在preHandle方法中,我们可以获取请求信息,并记录请求日志(如URL、参数、请求体等)。-在afterCompletion方法中,我们可以获取响应信息,并记录响应日志(如状态码、响应体等)。-但是,请求体和响应体在流中只能读取一次,因此我们需要使用包装类(如ContentCachingRequestWrapper和ContentCachingResponseWrapper)来缓存内容,以便重复读取。然而,需要注意的是:-在preHandle中读取请求体:使用ContentCachingRequestWrapper包装原始请求,这样我们就可以在后续读取缓存的请求体。-在afterCompletion中读取响应体:使用ContentCachingResponseWrapper包装原始响应,并在记录日志前将缓存的内容写回响应(因为包装类缓存了响应体,但需要手动将其内容复制到原始响应流中,或者我们在记录日志后复制)。异步实现:-我们可以将日志记录操作放入一个异步任务中执行。可以使用Spring的@Async注解,或者使用CompletableFuture,或者使用线程池。考虑到我们已经在拦截器中,如果使用@Async,需要确保异步方法在另一个Bean中(因为同一个类内调用异步方法可能不会生效)。另一种简单的方式:使用一个独立的日志记录服务,该服务使用线程池来提交日志记录任务。具体步骤:1.创建请求和响应的包装类(在拦截器的preHandle方法中)。2.在拦截器的afterCompletion方法中,获取包装类中的缓存数据,然后异步记录日志。但是,注意:请求的包装应该在拦截器之前的过滤器中完成,因为拦截器可能无法多次读取请求体。因此,我们通常会使用一个过滤器来包装请求和响应。因此,我们可能需要:-创建一个过滤器(Filter),用于将请求和响应包装成可缓存的版本。-创建一个拦截器(Interceptor),在afterCompletion方法中获取包装后的请求和响应,然后异步记录日志。流程:请求->过滤器(包装请求和响应)->拦截器(记录请求日志)->控制器->拦截器(记录响应日志)->过滤器(将响应包装的内容写回)注意:ContentCachingResponseWrapper需要在请求处理完成后将缓存的内容写入原始响应,否则客户端收不到响应。实现:第一步:创建过滤器,用于包装请求和响应。第二步:创建拦截器,在afterCompletion中异步记录日志(包括请求和响应信息)。第三步:创建异步日志记录服务。下面我们逐步实现:1.创建请求包装过滤器:```java@ComponentpublicclassCachingRequestBodyFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{//包装请求和响应ContentCachingRequestWrapperwrappingRequest=newContentCachingRequestWrapper(request);ContentCachingResponseWrapperwrappingResponse=newContentCachingResponseWrapper(response);filterChain.doFilter(wrappingRequest,wrappingResponse);//将响应缓存的内容写回原始响应wrappingResponse.copyBodyToResponse();}}```2.创建拦截器,在afterCompletion中记录日志(注意:此时请求和响应都已经被包装,并且响应内容已经缓存,但是要注意在afterCompletion中响应内容可能已经被复制回原始响应了?所以我们在复制之前记录?但是过滤器是在拦截器之后执行的,所以这里不能直接读取响应体?)实际上,过滤器的doFilter方法会执行整个处理链(包括拦截器、控制器等),然后在过滤器最后执行copyBodyToResponse。而拦截器的afterCompletion是在过滤器之后执行的(因为afterCompletion是在请求完成后回调,而过滤器的doFilter内部执行完chain.doFilter后,会执行copyBodyToResponse,然后才返回,所以afterCompletion在copyBodyToResponse之后执行)。因此,在afterCompletion中,响应包装类的缓存已经被清空(因为已经复制回原始响应了)。所以,我们需要在拦截器的preHandle方法中记录请求日志,在afterCompletion中记录响应日志,但是响应体无法获取?因此,我们需要在拦截器的afterCompletion之前获取响应体。另一种做法:不在过滤器中复制响应体,而是在拦截器中复制。但这样需要确保拦截器在过滤器的包装之后执行。修改:我们可以将复制响应体的操作放在拦截器的afterCompletion中,但是这样需要确保在记录日志后复制。或者,我们可以在控制器的处理之后,在拦截器的postHandle方法中记录响应日志?但是postHandle方法中,响应体可能还没有被写入(因为视图渲染可能还未完成)。因此,更可靠的做法是:在过滤器中,我们不立即复制响应体,而是等到拦截器的afterCompletion中再复制。但是这样需要将响应包装类传递到afterCompletion。我们可以这样:在过滤器中,将响应包装类设置为请求属性,然后在拦截器的afterCompletion中获取并复制。但是这样比较复杂。参考方案:使用ContentCachingResponseWrapper,我们可以在拦截器的preHandle中记录请求,在afterCompletion中记录响应,但是响应体的读取需要在复制之前。而过滤器中我们执行了filterChain.doFilter,然后复制响应体,这样在拦截器的afterCompletion中响应体已经被复制,缓存被清空。所以,我们需要在过滤器中不复制响应体,而是将复制操作延迟到拦截器的afterCompletion之后?这不太可行。因此,我们调整:在拦截器中记录请求和响应,但响应记录在postHandle方法中?但是postHandle方法中,响应体可能还没有被完全写入。正确的做法:在过滤器中,我们使用ContentCachingResponseWrapper,然后在过滤器中,我们在执行完filterChain.doFilter之后,先记录日志(同步或异步),然后再复制响应体。这样,在记录日志的时候,响应体已经被缓存,且还没有被复制回原始响应。但是这样日志记录就放在过滤器中了,而不是拦截器。那么,我们是否可以直接在过滤器中记录日志?可以,但是拦截器通常用于处理控制器相关的逻辑,而过滤器更底层。我们这里可以选择在过滤器中记录日志,并实现异步。但是问题要求使用拦截器,所以我们还是尽量用拦截器。另一种方案:在过滤器中,我们不立即复制响应体,而是将响应包装类存储在请求属性中,然后在拦截器的afterCompletion中,从请求属性中获取响应包装类,然后记录日志(读取响应体),再复制响应体。步骤:过滤器:1.包装请求和响应。2.将响应包装类存储为请求属性:request.setAttribute("cachingResponse",wrappingResponse);3.执行filterChain.doFilter(wrappingRequest,wrappingResponse);注意:此时响应还没有被复制回原始响应。拦截器的afterCompletion方法:1.从请求属性中获取响应包装类:ContentCachingResponseWrapperresponseWrapper=(ContentCachingResponseWrapper)request.getAttribute("cachingResponse");2.读取响应包装类的缓存内容(响应体)。3.记录日志(异步)。4.调用responseWrapper.copyBodyToResponse();//将响应复制回原始响应但是,注意:afterCompletion方法中如果发生异常,会导致复制响应体失败,所以需要异常处理。但是,这样处理的话,如果拦截器在afterCompletion中复制了响应体,那么过滤器中的响应包装类已经复制过了,在过滤器中就不应该再次复制。所以我们需要避免重复复制。因此,我们可以在过滤器中不复制响应体,完全交给拦截器。但是,如果请求没有被拦截器处理(比如preHandle返回false),那么就不会进入afterCompletion,这样响应体就不会被复制。所以,我们需要在过滤器中确保响应体被复制。所以,更安全的方式:在过滤器中,我们执行完filterChain.doFilter后,先不复制响应体,而是等待拦截器去复制。但是如果拦截器没有执行afterCompletion(比如异常情况),那么我们需要在过滤器中有一个兜底的复制操作?这样会复杂。权衡后,我们决定在过滤器中记录日志(这样更简单),但是问题要求拦截器。所以我们采用拦截器记录请求日志(在preHandle中,使用ContentCachingRequestWrapper),而响应日志我们可以在postHandle中记录?但是postHandle中响应体可能还未写入。或者,我们只记录请求日志和响应状态码,不记录响应体?如果必须记录响应体,那么就需要使用ContentCachingResponseWrapper,并且需要处理响应体复制的问题。考虑到复杂性,我们这里采用一个折中方案:-在过滤器中包装请求和响应(使用ContentCachingRequestWrapper和ContentCachingResponseWrapper)。-在过滤器中,执行filterChain.doFilter之后,异步记录日志(包括请求和响应),然后复制响应体。这样,我们就用过滤器实现了日志记录,但问题要求使用拦截器。因此,我们重新考虑:拦截器如何记录响应体?我们可以在拦截器的preHandle方法中记录请求日志(此时请求已经被包装,可以读取请求体),然后在afterCompletion中记录响应日志,但是响应体如何获取?如果我们能在afterCompletion中获取到响应包装类,那么就可以读取响应体。我们可以将响应包装类存储在请求属性中,然后在afterCompletion中获取。同时,在过滤器中,我们只包装请求和响应,并不复制响应体,而是将响应包装类存储在请求属性中,然后在拦截器的afterCompletion中复制响应体。具体:过滤器:```javapublicclassCachingRequestBodyFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{ContentCachingRequestWrapperrequestWrapper=newContentCachingRequestWrapper(request);ContentCachingResponseWrapperresponseWrapper=newContentCachingResponseWrapper(response);//将响应包装类存储在请求属性中,以便拦截器中使用request.setAttribute("cachingResponse",responseWrapper);filterChain.doFilter(requestWrapper,responseWrapper);//注意:这里我们不复制响应体,由拦截器负责}}```拦截器:```javapublicclassLoggingInterceptorimplementsHandlerInterceptor{@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{//从请求属性中获取响应包装类ContentCachingResponseWrapperresponseWrapper=(ContentCachingResponseWrapper)request.getAttribute("cachingResponse");//记录响应日志(异步)//注意:这里我们读取响应体,然后复制响应体if(responseWrapper!=null){//获取响应体内容byte[]responseBody=responseWrapper.getContentAsByteArray();StringresponseContent=newString(responseBody,response.getCharacterEncoding());//异步记录日志(包括响应状态码、响应体等)//调用异步服务记录日志//将响应包装类的内容复制回原始响应responseWrapper.copyBodyToResponse();}}}```但是,这里有一个问题:过滤器的doFilterInternal方法执行后,会继续执行,然后返回,而拦截器的afterCompletion是在请求处理完成后才执行。所以,在过滤器中,我们执行了filterChain.doFilter,然后过滤器方法结束,此时响应还没有被复制(因为我们没有在过滤器中复制)。然后,在拦截器的afterCompletion中,我们复制了响应体。但是,如果请求没有进入拦截器(比如被其他过滤器拦截了),那么就不会执行afterCompletion,响应体就不会被复制,导致客户端收不到响应。所以,我们需要确保即使没有进入拦截器,也能复制响应体。因此,我们需要在过滤器中确保响应体被复制,即使发生异常。我们可以使用try-finally:修改过滤器:```javapublicclassCachingRequestBodyFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{ContentCachingRequestWrapperrequestWrapper=newContentCachingRequestWrapper(request);ContentCachingResponseWrapperresponseWrapper=newContentCachingResponseWrapper(response);try{//将响应包装类存储在请求属性中request.setAttribute("cachingResponse",responseWrapper);filterChain.doFilter(requestWrapper,responseWrapper);}finally{//确保响应体被复制,即使发生异常//但是注意:如果拦截器已经复制了,这里再复制就会重复(导致异常)//所以,我们需要一个标记,避免重复复制//可以在拦截器中复制后设置一个标记,表示已经复制//这里我们使用请求属性来标记if(request.getAttribute("responseCopied")==null){responseWrapper.copyBodyToResponse();}}}}```然后在拦截器的afterCompletion中,复制响应体后设置标记:```javapublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{ContentCachingResponseWrapperresponseWrapper=(ContentCachingResponseWrapper)request.getAttribute("cachingResponse");if(responseWrapper!=null){//记录日志(异步)//...异步记录//复制响应体responseWrapper.copyBodyToResponse();//设置标记,避免过滤器再次复制request.setAttribute("responseCopied",true);}}```这样,在正常情况下,由拦截器复制响应体;在异常情况下(没有进入拦截器),由过滤器复制。但是,如果拦截器复制了响应体,然后过滤器再次复制,就会抛出异常(因为响应流已经关闭)。所以,我们通过标记避免重复复制。这样,我们就可以在拦截器中记录响应日志(包括响应体)了。3.异步日志记录服务我们可以创建一个日志服务,使用线程池或Spring的@Async来异步执行日志记录。例如,使用@Async:在Spring配置中启用异步支持(@EnableAsync)。创建LogService类:```java@ServicepublicclassLogService{@AsyncpublicvoidlogRequest(HttpServletRequestrequest){//记录请求日志ContentCachingRequestWrapperrequestWrapper=(ContentCachingRequestWrapper)request;//获取请求信息StringrequestBody=newString(requestWrapper.getContentAsByteArray(),request.getCharacterEncoding());//记录日志(这里只是示例,可以记录到文件或数据库)System.out.println("Request:"+request.getRequestURI()+",Body:"+requestBody);}@AsyncpublicvoidlogResponse(HttpServletResponseresponse,StringresponseContent){//记录响应日志System.out.println("Responsestatus:"+response.getStatus()+",Body:"+responseContent);}}```注意:在拦截器的preHandle中,我们可以记录请求日志,在afterCompletion中记录响应日志。但是,请求日志的异步记录:在preHandle方法中,我们调用logService.logRequest(requestWrapper)(注意:requestWrapper是ContentCachingRequestWrapper类型,我们在过滤器中已经包装了,所以在拦截器中可以直接使用)。因此,拦截器可以这样写:```javapublicclassLoggingInterceptorimplementsHandlerInterceptor{privatefinalLogServicelogService;publicLoggingInterceptor(LogServicelogService){this.logService=logService;}@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{//记录请求日志if(requestinstanceofContentCachingRequestWrapper){logService.logRequest((ContentCachingRequestWrapper)request);}returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{ContentCachingResponseWrapperresponseWrapper=(ContentCachingResponseWrapper)request.getAttribute("cachingResponse");if(responseWrapper!=null){//获取响应体StringresponseContent=newString(responseWrapper.getContentAsByteArray(),response.getCharacterEncoding());//记录响应日志logService.logResponse(response,responseContent);//复制响应体到原始响应responseWrapper.copyBodyToResponse();request.setAttribute("responseCopied",true);}}}```但是,注意:请求体在preHandle中读取时,可能还没有被填充(因为ContentCachingRequestWrapper是在读取输入流后才会缓存)。所以,我们需要确保在控制器处理之前,请求体已经被读取并缓存。而ContentCachingRequestWrapper的缓存是在我们调用getContentAsByteArray时,如果还没有读取,它会读取并缓存。所以,在preHandle中读取请求体是可行的,因为此时请求还没有被控制器读取,但是包装类会缓存请求体,这样控制器处理时就会从缓存中读取,而不会丢失。但是,ContentCachingRequestWrapper的getContentAsByteArray方法在请求体没有被读取时,会读取并缓存。所以,我们在preHandle中调用logService.logRequest时,会读取请求体并缓存,然后控制器处理时就可以正常读取。因此,这个方案是可行的。总结步骤:1.创建过滤器:包装请求和响应,并将响应包装类存储在请求属性中,并确保在最后(无论是否异常)复制响应体(如果拦截器没有复制的话)。2.创建拦截器:在preHandle中异步记录请求日志(使用请求包装类),在afterCompletion中异步记录响应日志(使用响应包装类),并复制响应体(同时设置标记,避免过滤器重复复制)。3.创建异步日志服务。注册过滤器和拦截器:过滤器注册:使用FilterRegistrationBean注册,并设置顺序(确保在最前)。拦截器注册:使用WebMvcConfigurer的addInterceptors方法。注意:过滤器的顺序很重要,要确保在其他过滤器之前(这样我们的包装可以生效)。下面给出代码实现:由于代码较长,我们分步骤:第一步:过滤器```javaimportorg.springframework.stereotype.Component;importorg.springframework.web.filter.OncePerRequestFilter;importjavax.servlet.*;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importorg.springframework.web.util.ContentCachingRequestWrapper;importorg.springframework.web.util.ContentCachingResponseWrapper;@ComponentpublicclassCachingRequestBodyFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{//包装请求和响应ContentCachingRequestWrapperrequestWrapper=newContentCachingRequestWrapper(request);ContentCachingResponseWrapperresponseWrapper=newContentCachingResponseWrapper(response);try{//将响应包装类存储为请求属性,以便拦截器使用request.setAttribute("cachingResponse",responseWrapper);filterChain.doFilter(requestWrapper,responseWrapper);}finally{//如果拦截器没有复制响应体,则在这里复制if(request.getAttribute("responseCopied")==null){responseWrapper.copyBodyToResponse();}}}}```第二步:拦截器```javaimportorg.springframework.stereotype.Component;importorg.springframework.web.servlet.HandlerInterceptor;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importorg.springframework.web.util.ContentCachingRequestWrapper;importorg.springframework.web.util.ContentCachingResponseWrapper;@ComponentpublicclassLoggingInterceptorimplementsHandlerInterceptor{privatefinalLogServicelogService;publicLoggingInterceptor(LogServicelogService){this.logService=logService;}@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){//记录请求日志if(requestinstanceofContentCachingRequestWrapper){logService.logRequest((ContentCachingRequestWrapper)request);}returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){ContentCachingResponseWrapperresponseWrapper=(ContentCachingResponseWrapper)request.getAttribute("cachingResponse");if(responseWrapper!=null){//获取响应内容StringresponseContent=newString(responseWrapper.getContentAsByteArray(),response.getCharacterEncoding());//记录响应日志logService.logResponse(response,responseContent);try{//复制响应体到原始响应responseWrapper.copyBodyToResponse();//设置标记,表示已经复制,避免过滤器重复复制request.setAttribute("responseCopied",true);}catch(IOExceptione){//处理异常e.printStackTrace();}}}}```第三步:日志服务(异步)```javaimportorg.springframework.scheduling.annotation.Async;importorg.springframework.stereotype.Service;importjavax.servlet.http.HttpServletResponse;importorg.springframework.web.util.ContentCachingRequestWrapper;@ServicepublicclassLogService{@AsyncpublicvoidlogRequest(ContentCachingRequestWrapperrequest){//获取请求信息StringrequestBody=newString(request.getContentAsByteArray(),request.getCharacterEncoding());//这里可以记录更多信息,如URL、请求头等System.out.println("RequestURL:"+request.getRequestURI());System.out.println("RequestMethod:"+request.getMethod());System.out.println("RequestBody:"+requestBody);}@AsyncpublicvoidlogResponse(HttpServletResponseresponse,StringresponseContent){System.out.println("ResponseStatus:"+response.getStatus());System.out.println("ResponseBody:"+responseContent);}}```第四步:配置类(注册拦截器)```javaimportorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;@ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{privatefinalLoggingInterceptorloggingInterceptor;@AutowiredpublicWebConfig(LoggingInterceptorloggingInterceptor){this.loggingInterceptor=loggingInterceptor;}@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(loggingInterceptor);}}```第五步:主启动类启用异步```javaimportorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.scheduling.annotation.EnableAsync;@SpringBootApplication@EnableAsyncpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}```注意:-这个实现中,请求日志和响应日志都是异步记录的。-由于使用了@Async,需要确保异步配置正确(比如线程池配置,否则默认线程池可能不够用)。-请求体可能很大,需要注意内存使用。-在记录日志时,可能需要排除某些请求(如健康检查),可以在拦截器中添加排除路径。以上代码仅供参考,实际应用中需要根据需求调整。相关问题:1.如何避免记录某些请求(如健康检查)的日志?2.如果请求体或响应体很大,如何避免内存溢出?3.如何配置异步日志记录的线程池?4.如何将日志记录到文件或数据库,而不是控制台?我们可以在拦截器中通过请求的URL来判断是否记录,例如:```java@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){Stringuri=request.getRequestURI();if(uri.startsWith("/health")){returntrue;//跳过日志记录}//...记录日志}```对于大请求体,我们可以设置一个阈值,超过阈值则不记录请求体(或者只记录部分)。线程池配置:在配置类中定义TaskExecutor。记录到文件或数据库:可以使用日志框架(如Logback)或数据库操作。由于篇幅限制,这里不展开。最后,请注意:以上代码需要SpringBoot和ServletAPI的支持。引用说明:我们参考了Spring的ContentCachingRequestWrapper和ContentCachingResponseWrapper的使用,以及拦截器和过滤器的配合[^1][^2][^3]。</think>### Java 使用异步和拦截器实现 HTTP 请求/响应日志记录
在 Java 中通过异步和拦截器实现 HTTP 请求/响应日志记录,需要结合以下关键技术:
1. **拦截器(Interceptor)**:拦截 HTTP 请求/响应
2. **异步处理**:防止日志 I/O 操作阻塞主线程
3. **请求/响应包装类**:解决 Servlet 输入流只能读取一次的问题
#### 完整实现方案
##### 1. 创建请求/响应包装类(过滤器)
```java
@Component
public class CachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// 包装请求和响应
ContentCachingRequestWrapper requestWrapper =
new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper(response);
chain.doFilter(requestWrapper, responseWrapper);
// 确保响应内容写回客户端
responseWrapper.copyBodyToResponse();
}
}
```
##### 2. 实现异步日志拦截器
```java
@Component
public class AsyncLogInterceptor implements HandlerInterceptor {
private final LogService logService;
@Autowired
public AsyncLogInterceptor(LogService logService) {
this.logService = logService;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 异步记录请求日志
logService.logRequestAsync((ContentCachingRequestWrapper) request);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// 异步记录响应日志
logService.logResponseAsync(
(ContentCachingResponseWrapper) response,
(ContentCachingRequestWrapper) request
);
}
}
```
##### 3. 异步日志服务(核心)
```java
@Service
public class LogService {
// 使用线程池处理异步日志
private final ExecutorService logExecutor =
Executors.newFixedThreadPool(4);
public void logRequestAsync(ContentCachingRequestWrapper request) {
logExecutor.execute(() -> {
try {
// 获取请求信息
String requestBody = new String(
request.getContentAsByteArray(),
request.getCharacterEncoding()
);
// 构建日志对象(示例)
LogEntry log = new LogEntry(
UUID.randomUUID().toString(),
request.getRequestURI(),
request.getMethod(),
requestBody,
null,
System.currentTimeMillis()
);
// 存储日志(实际可替换为数据库/文件操作)
System.out.println("[REQUEST] " + log);
} catch (Exception e) {
// 异常处理
}
});
}
public void logResponseAsync(ContentCachingResponseWrapper response,
ContentCachingRequestWrapper request) {
logExecutor.execute(() -> {
try {
// 获取响应信息
String responseBody = new String(
response.getContentAsByteArray(),
response.getCharacterEncoding()
);
// 构建日志对象
LogEntry log = new LogEntry(
// 通过自定义ID关联请求
request.getHeader("X-Request-ID"),
request.getRequestURI(),
request.getMethod(),
null,
responseBody,
System.currentTimeMillis()
);
// 存储日志
System.out.println("[RESPONSE] " + log);
} catch (Exception e) {
// 异常处理
} finally {
try {
// 确保响应内容写回
response.copyBodyToResponse();
} catch (IOException ex) {
// 处理异常
}
}
});
}
}
```
##### 4. 日志实体类
```java
public class LogEntry {
private String requestId;
private String uri;
private String method;
private String requestBody;
private String responseBody;
private long timestamp;
// 构造方法/getters/setters
}
```
##### 5. 注册拦截器
```java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AsyncLogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor)
.addPathPatterns("/api/**"); // 配置拦截路径
}
}
```
#### 关键优化点
1. **线程池管理**:
```java
// 推荐使用 ThreadPoolTaskExecutor
@Bean
public Executor logTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Log-Executor-");
executor.initialize();
return executor;
}
```
2. **日志关联**:
- 在过滤器中生成唯一请求 ID:
```java
requestWrapper.setHeader("X-Request-ID", UUID.randomUUID().toString());
```
- 在日志记录中使用相同 ID 关联请求/响应
3. **性能优化**:
- 限制记录的最大 body 大小
- 使用缓冲队列防止日志积压
- 敏感数据脱敏处理
#### 处理流程
```mermaid
sequenceDiagram
participant Client
participant Filter
participant Interceptor
participant Controller
participant LogService
Client->>Filter: HTTP 请求
Filter->>Interceptor: 包装请求/响应
Interceptor->>LogService: 提交请求日志任务
LogService-->>线程池: 异步处理
Interceptor->>Controller: 传递请求
Controller->>Interceptor: 返回响应
Interceptor->>LogService: 提交响应日志任务
LogService-->>线程池: 异步处理
Filter->>Client: 返回响应
```
#### 注意事项
1. **流复制问题**:必须使用 `ContentCachingRequestWrapper` 和 `ContentCachingResponseWrapper` 解决输入流单次读取限制
2. **异步边界**:确保在 `afterCompletion` 完成前不关闭响应流
3. **异常处理**:添加降级机制防止日志模块导致主流程崩溃
4. **资源释放**:正确关闭线程池(通过 `@PreDestroy`)
> **最佳实践**:对于生产环境,建议集成日志框架(如 Logback)并将日志输出到独立文件,同时添加请求过滤规则排除健康检查等端点[^1][^2][^3]。