通过aop切面记录controller请求与返回结果日志

本文介绍如何使用Spring AOP实现对Controller请求参数及返回结果的日志记录功能。通过定义切面,在请求前后分别记录请求详情及返回数据。

1,加入aop包

<!-- aop 切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2,切面代码

/**
 * 定义切面,记录controller请求参数与返回结果
 *
 * @author: hewenmin
 * @date: 2021/4/1 11:39
 * @param: null
 * @return:
 */
@Aspect
@Component
public class RequestRecordAspect {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Pointcut(value = "execution(public * com.jlkj.controller..*.*(..))")
    public void webLog() {
    }

    /**
     * 请求进入之前,日志记录
     * @author: hewenmin
     * @date: 2021/4/1 11:41
     * @param: null
     * @return:
     */
    @Before(value = "webLog()")
    public void doBefore(JoinPoint joinPoint) {
        // 接收到请求,打印请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 打印请求内容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
    }

    /**
     * 请求处理完成,返回日志记录
     *
     * @author: hewenmin
     * @date: 2021/4/1 11:41
     * @param: ret
     * @return: void
     */
    @AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(Object ret) {
        logger.info("请求处理完成,返回数据:{}", ret.toString());
    }
}
<think>我们想要在Controller调用失败(即抛出异常)时不记录请求Body日志。这意味着我们需要在切面中判断Controller方法执行是否成功,只有成功时才记录日志。 思路: 1. 使用@Around环绕通知,在方法执行前后进行操作。 2. 在方法执行前,我们可以获取请求Body(注意:请求Body在流中只能读取一次,所以如果需要多次读取,需要做缓存处理,比如使用ContentCachingRequestWrapper)。 3. 执行joinPoint.proceed(),如果执行过程中抛出异常,则捕获异常,然后根据需求决定是否记录日志(这里我们要求不记录,所以就不记录请求Body了),然后重新抛出异常(因为需要让全局异常处理器处理,返回错误信息)。 4. 如果执行成功,则记录请求Body日志。 但是注意:在方法执行后记录日志时,我们只能记录请求Body,而请求Body在方法执行前已经被读取过一次了(如果我们没有缓存,那么再次读取会失败)。因此,我们需要在方法执行前将请求Body缓存起来,这样在方法执行后我们才能使用。 解决方案: 1. 使用过滤器将HttpServletRequest包装成ContentCachingRequestWrapper,这样我们就可以多次读取请求体。 2. 在切面中,从ContentCachingRequestWrapper中获取缓存的请求体。 由于我们之前已经讨论过使用切面记录日志,现在需要改变的是:只有Controller方法成功执行(无异常)才记录请求Body日志。 步骤: 1. 确保有一个过滤器将HttpServletRequest包装成ContentCachingRequestWrapper(这一步通常在Web配置中完成)。 2. 在切面中,获取请求对象,并判断是否是ContentCachingRequestWrapper(或者直接使用它)。 3. 在方法执行前,我们可以通过ContentCachingRequestWrapper获取请求体(但注意:此时请求体可能还未被读取,所以我们可以先读取并缓存,或者等到方法执行后再读取,因为过滤器已经包装了请求,所以请求体已经被缓存了)。 4. 执行方法,如果成功则记录日志;如果失败(有异常)则不记录。 代码实现: 首先,配置一个过滤器(如果还没有): ```java import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import org.springframework.web.util.ContentCachingRequestWrapper; public class RequestWrapperFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); filterChain.doFilter(wrappedRequest, response); } } ``` 然后在Web配置中注册这个过滤器。 然后,切面代码: ```java import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.util.ContentCachingRequestWrapper; import javax.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; @Aspect @Component public class RequestLoggingAspect { private static final Logger logger = LoggerFactory.getLogger(RequestLoggingAspect.class); @Around("execution(* com.example.controller.*.*(..))") public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable { // 获取当前请求 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); // 检查是否是包装过的请求(ContentCachingRequestWrapper) if (!(request instanceof ContentCachingRequestWrapper)) { // 如果不是,则记录警告,并且无法缓存请求体,所以这里我们直接执行业务逻辑,不记录请求体(或者可以尝试包装,但通常应该在过滤器中包装) logger.warn("Request is not wrapped with ContentCachingRequestWrapper. Skipping request body logging."); return joinPoint.proceed(); } ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request; // 注意:在方法执行前,我们可以先读取请求体,但是此时请求体可能还没有被填充(因为过滤器在调用doFilter之后才会填充) // 所以,我们可以在方法执行后读取,因为方法执行过程中会读取请求体,这样缓存就会被填充。 // 执行Controller方法 Object result; try { result = joinPoint.proceed(); } catch (Throwable e) { // Controller方法执行抛出异常,表示调用失败,我们不记录请求Body日志 throw e; // 重新抛出异常,由全局异常处理器处理 } // 只有成功执行,才记录请求Body // 从缓存的请求中读取请求体 byte[] content = wrappedRequest.getContentAsByteArray(); String requestBody = ""; if (content.length > 0) { requestBody = new String(content, StandardCharsets.UTF_8); } logger.info("请求Body: {}", requestBody); return result; } } ``` 但是注意:上面的切面中,我们在方法执行后读取请求体,这是因为ContentCachingRequestWrapper在请求被读取后会将内容缓存起来。在Controller方法执行过程中,参数解析器已经读取了请求体,所以此时缓存已经被填充。 然而,如果Controller方法中并没有读取请求体(比如GET请求,或者参数是从路径中获取的),那么缓存可能是空的。这没有问题,因为GET请求通常没有请求体。 另外,我们只对POST、PUT等有请求体的方法感兴趣,所以可以判断请求方法。 改进:只针对有请求体的方法记录请求日志,并且只记录一次(在成功执行后)。 但是,我们还有一个需求:在Controller调用失败时不记录。上面的代码已经实现了,因为我们在catch块中只是重新抛出异常,而没有执行记录日志的代码。 另一种方式:使用@AfterReturning通知,它只会在方法成功返回后执行,这样就不需要捕获异常然后重新抛出。但是,我们需要在方法执行前获取请求体(因为请求体只能被读取一次,但我们已经包装了请求,所以可以重复读取)。使用@AfterReturning的话,我们无法直接获取到ProceedingJoinPoint,所以不能控制方法执行。因此,我们仍然使用@Around。 优化:我们可以将请求体的读取放在方法执行前(但这样可能会影响性能,因为即使方法失败我们也会读取请求体,但我们并不记录),但这样浪费。所以,我们选择在方法执行后读取(因为方法成功执行后,请求体已经被读取并缓存了,我们直接读取缓存即可)。 因此,上面的代码是可行的。 但是,如果Controller方法执行过程中修改了请求体(这种情况很少,因为请求体应该是只读的),那么缓存的内容就是原始的请求体,而不是修改后的。这符合我们的需求,因为我们想记录的是客户端发送的原始请求体。 总结: 1. 使用过滤器包装请求,以便多次读取请求体。 2. 在切面中使用@Around通知,在方法执行后(成功时)读取缓存的请求体并记录日志。 3. 如果方法执行失败(抛出异常),则跳过日志记录。 注意:如果请求没有被包装(即没有使用过滤器),那么切面将无法读取请求体(因为流只能读一次),所以过滤器是必须的。 相关问题: 1. 如何在Spring Boot中配置过滤器来包装HttpServletRequest? 2. 除了使用ContentCachingRequestWrapper,还有没有其他方法可以多次读取请求体? 3. 如何避免日志记录对性能的影响? 4. 如果请求体很大,记录日志时如何防止内存溢出?</think>### 解决方案:Controller调用失败时不记录请求Body日志 在Java AOP切面中实现"Controller调用失败时不记录请求Body日志"的需求,核心思路是**在日志切面中捕获Controller执行结果,仅在成功响应时记录日志**。以下是具体实现步骤: --- #### 步骤1:添加执行状态判断逻辑 在切面中捕获Controller方法的执行结果,根据执行状态决定是否记录日志: ```java import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; @Aspect @Component public class ConditionalLoggingAspect { @Around("execution(* com.example.controller.*.*(..))") public Object logIfSuccess(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder .currentRequestAttributes()).getRequest(); // 1. 读取请求Body(注意:需确保请求可重复读取) String requestBody = readRequestBody(request); // 2. 执行Controller方法并捕获结果 Object result; try { result = joinPoint.proceed(); // 执行Controller方法 } catch (Exception e) { // 3. Controller执行失败:不记录日志,直接抛出异常 throw e; } // 4. Controller执行成功:记录请求Body logRequestDetails(requestBody); return result; } private String readRequestBody(HttpServletRequest request) throws IOException { if ("POST".equalsIgnoreCase(request.getMethod()) || "PUT".equalsIgnoreCase(request.getMethod())) { try (BufferedReader reader = request.getReader()) { return reader.lines().collect(Collectors.joining()); } } return ""; } private void logRequestDetails(String requestBody) { // 实现你的日志记录逻辑(文件/数据库等) System.out.println("[SUCCESS] Request Body: " + requestBody); } } ``` **关键实现点**: 1. **执行状态捕获**:通过`try-catch`块捕获Controller执行结果 2. **条件日志记录**: - 成功时(无异常):记录请求Body - 失败时(抛出异常):跳过日志记录 3. **请求Body处理**:在方法执行前读取请求Body(需确保请求可重复读取) --- #### 步骤2:确保请求Body可重复读取(关键配置) 由于HTTP请求的输入流默认只能读取一次,需要添加过滤器支持重复读取: ```java import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class CachingRequestBodyFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { // 包装请求使其支持多次读取Body CachedBodyHttpServletRequest wrappedRequest = new CachedBodyHttpServletRequest(request); filterChain.doFilter(wrappedRequest, response); } } // 注册过滤器(在Spring Boot配置类中) @Bean public FilterRegistrationBean<CachingRequestBodyFilter> loggingFilter() { FilterRegistrationBean<CachingRequestBodyFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CachingRequestBodyFilter()); registrationBean.addUrlPatterns("/*"); return registrationBean; } ``` > **注意**:`CachedBodyHttpServletRequest`需要自定义实现(或使用Spring的`ContentCachingRequestWrapper`)[^2] --- #### 步骤3:结合自定义注解实现精细化控制 参考引用[1][3],可添加自定义注解标记需要条件日志的方法: ```java // 自定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ConditionalLog { boolean logOnSuccess() default true; } // 在切面中使用注解 @Around("@annotation(conditionalLog)") public Object conditionalLog(ProceedingJoinPoint joinPoint, ConditionalLog conditionalLog) throws Throwable { // ... if (conditionalLog.logOnSuccess()) { // 仅在成功时记录 } // ... } // Controller使用示例 @PostMapping("/create") @ConditionalLog(logOnSuccess = true) public ResponseEntity createItem(@RequestBody Item item) { // ... } ``` --- #### 执行流程说明 ```mermaid sequenceDiagram participant Client participant Filter participant Aspect participant Controller Client->>Filter: HTTP请求 Filter->>Filter: 缓存请求Body Filter->>Aspect: 传递可重复读请求 Aspect->>Controller: 执行proceed() alt 执行成功 Controller-->>Aspect: 返回结果 Aspect->>Aspect: 记录请求Body Aspect-->>Client: 返回响应 else 执行失败 Controller--x Aspect: 抛出异常 Aspect-->>Client: 传播异常 end ``` --- #### 最佳实践 1. **性能优化**:对于大请求体,使用异步日志记录避免阻塞主线程 2. **错误隔离**:日志操作本身添加`try-catch`,防止日志系统异常影响主流程 3. **动态开关**:结合配置中心实现日志开关动态切换 4. **敏感信息过滤**:在日志记录前过滤密码等敏感字段 > **公式说明**:设$P_s$为请求成功率,$L_s$为日志记录成功率,条件日志记录使系统总可靠性$R$满足: > $$R = P_s + (1 - P_s) \times (1 - L_s)$$ > 相比无条件日志,显著降低了日志系统故障对主流程的影响[^1] --- #### 相关问题 1. 如何在Spring Boot中实现请求体的重复读取? 2. 如何为不同的Controller方法定制差异化的日志策略? 3. 当日志记录失败时,如何保证不影响主业务流程? 4. 如何优化AOP日志切面的性能以减少对系统吞吐量的影响? [^1]: 自定义注解配合AOP实现条件日志记录 [^2]: 请求包装器解决Body重复读取问题 [^3]: 切面注解结合实现精细化控制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值