Spring Boot 接口请求日志(基于AOP和自定义注解)

一、需求

在Spring Boot应用中,实现接口请求日志记录功能,要求能够记录包括请求方法、接口路径及请求参数等核心信息,并提供灵活的开关配置。

二、方案概述

采用AOP(面向切面编程)结合自定义注解的方式实现。

具体步骤如下:

  1. 创建自定义注解@ApiLog,标记需要记录日志的接口。
  2. 通过AOP实现一个切面,对被@ApiLog注解修饰的方法进行前置处理,记录其请求相关信息。
  3. 提供配置项开关,控制是否开启接口日志记录。
  4. 推荐使用消息队列(例如RocketMQ)异步处理接口日志,以提升性能,但本示例仅展示简单的日志打印。使用消息队列的方法是:将接口的请求日志发送到消息队列里,由专门的日志记录服务器去处理,比如写入专门的数据库。这样可以减少接口的同步处理的时间,避免客户端等待时间过长,提升总体性能。

三、核心代码

自定义注解:@ApiLog

package com.example.core.log.annotation;

import java.lang.annotation.*;

/**
 * 接口日志注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog {
}

切面类:ApiLogAspect

package com.example.core.log.aspect;

import com.example.core.property.BaseFrameworkConfigProperties;
import com.example.core.util.JsonUtil;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
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 javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Aspect
@Order(20)
@Component
public class ApiLogAspect {

    @Value("${spring.application.name:}")
    private String applicationName;

    private final BaseFrameworkConfigProperties properties;

    public ApiLogAspect(BaseFrameworkConfigProperties properties) {
        this.properties = properties;
    }

    // 定义一个切点:所有被 ApiLog 注解修饰的方法会织入advice
    @Pointcut("@annotation(com.example.core.log.annotation.ApiLog)")
    private void pointcut() {
    }

    // Before表示 advice() 将在目标方法执行前执行
    @Before("pointcut()")
    public void advice(JoinPoint joinPoint) {

        if (!properties.getApiLog().isEnabled()) {
            return;
        }

        log.info("\n-------------------- 接口日志,开始 --------------------");

        log.info("applicationName:{}", applicationName);

        // 获取请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();

            // 用户IP
            String clientIp = request.getRemoteAddr();
            log.info("clientIp:{}", clientIp);

            // URL
            String requestURL = request.getRequestURL().toString();
            log.info("url:{}", requestURL);

            // 请求方法
            String requestMethod = request.getMethod();
            log.info("requestMethod:{}", requestMethod);

            // 接口路径
            String path = request.getServletPath();
            log.info("path:{}", path);
        }

        // 控制器方法参数列表
        Object[] args = joinPoint.getArgs();
        // 获取有效的控制器方法参数列表
        List<Object> validArgs = getValidArguments(args);
        log.info("args:{}", JsonUtil.toJson(validArgs));

        // 方法签名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        log.info("methodSignature:{}", methodSignature);

        // 方法参数名称列表
        String[] parameterNames = methodSignature.getParameterNames();
        log.info("parameterNames:{}", JsonUtil.toJson(parameterNames));

        // 获取接口的注解
        Operation operation = methodSignature.getMethod().getAnnotation(Operation.class);
        if (operation != null) {
            // 接口概述
            String summary = operation.summary();
            log.info("summary:{}", summary);

            // 接口描述
            String description = operation.description();
            log.info("description:{}", description);
        }

        log.info("\n-------------------- 接口日志,结束 --------------------\n");
    }

    /**
     * 获取有效的控制器方法参数列表
     * <p>
     * 排除 HttpServletRequest 和 HttpServletResponse 参数。
     * <p>
     * HttpServletRequest 参数,会阻塞线程,抛出异常 NestedServletException-OutOfMemoryError。
     * <p>
     * HttpServletResponse 参数,会抛出异常 NestedServletException-StackOverflowError。
     */
    private List<Object> getValidArguments(Object[] args) {
        return Stream.of(args).filter(this::isValidArgument).collect(Collectors.toList());
    }

    private Boolean isValidArgument(Object arg) {
        return isNotHttpServletRequest(arg) && isNotHttpServletResponse(arg);
    }

    /**
     * 不是 HttpServletRequest
     * <p>
     * HttpServletRequest 参数,会阻塞线程,会抛出如下异常:
     * org.springframework.web.util.NestedServletException:
     * Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space
     */
    private Boolean isNotHttpServletRequest(Object arg) {
        return !(arg instanceof HttpServletRequest);
    }

    /**
     * 不是 HttpServletResponse
     * <p>
     * HttpServletResponse 参数,会抛出如下异常:
     * org.springframework.web.util.NestedServletException:
     * Handler dispatch failed; nested exception is java.lang.StackOverflowError
     */
    private Boolean isNotHttpServletResponse(Object arg) {
        return !(arg instanceof HttpServletResponse);
    }

}

日志开关配置

配置类:BaseFrameworkConfigProperties


package com.example.core.property;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


/**
 * BaseFramework 配置文件
 *
 * @author songguanxun
 * 2019/08/27 15:40
 * @since 1.0.0
 */
@Data
@Component
@ConfigurationProperties(prefix = "base-framework")
public class BaseFrameworkConfigProperties {

    /**
     * 接口日志配置
     */
    private ApiLog apiLog = new ApiLog();

    /**
     * 接口日志配置
     */
    @Data
    public static class ApiLog {

        /**
         * 是否开启接口日志
         */
        private boolean enabled = false;

    }

}

配置文件:application.yml

# 自定义配置
base-framework:
  api-log:
    enabled: false

四、测试案例一:查询用户列表

4.1 测试代码

package com.example.web.user.controller;

import com.example.core.log.annotation.ApiLog;
import com.example.core.model.PageQuery;
import com.example.web.model.query.UserQuery;
import com.example.web.model.vo.UserVO;
import com.example.web.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("users")
@Tag(name = "用户管理")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @ApiLog
    @GetMapping
    @Operation(summary = "查询用户列表", description = "支持通过”姓名“和”手机号码“筛选用户")
    public List<UserVO> listUsers(@Valid UserQuery userQuery, PageQuery pageQuery) {
        log.info("查询用户列表。userQuery={},pageQuery={}", userQuery, pageQuery);
        return userService.listUsers(userQuery);
    }

}

package com.example.web.model.query;

import com.example.core.constant.RegexConstant;
import com.example.core.validation.phone.query.MobilePhoneQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springdoc.api.annotations.ParameterObject;

@Data
@ParameterObject
@Schema(name = "用户Query")
public class UserQuery {

    @Schema(description = "姓名", example = "张三")
    private String name;

    @MobilePhoneQuery
    @Schema(description = "手机号码", example = "18612345678", pattern = RegexConstant.NUMBERS, maxLength = 11)
    private String mobilePhone;

}

package com.example.core.model;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.FieldNameConstants;
import org.springdoc.api.annotations.ParameterObject;

@Data
@FieldNameConstants
@ParameterObject
@Schema(name = "分页参数Query")
public class PageQuery {

    @Schema(description = "当前页码", type = "Integer", defaultValue = "1", example = "1", minimum = "1")
    private Integer pageNumber = 1;

    @Schema(description = "每 1 页的数据量", type = "Integer", defaultValue = "10", example = "10", minimum = "1", maximum = "100")
    private Integer pageSize = 10;

}

4.2 接口调用效果

在这里插入图片描述

4.3 控制台日志

在这里插入图片描述

五、排除HttpServletRequest和HttpServletResponse参数

测试 HttpServletRequest、HttpServletResponse 和 HttpSession,是否在接口日志处理时堵塞线程或抛出异常?

5.1 原因

获取有效的控制器方法参数列表时,需要排除 HttpServletRequest 和 HttpServletResponse 参数。原因如下:

  1. 打印 HttpServletRequest 参数,会阻塞线程,抛出异常 NestedServletException-OutOfMemoryError。
  2. 打印 HttpServletResponse 参数,会抛出异常 NestedServletException-StackOverflowError。

HttpSession能够正常获取并打印日志,不需要特殊处理。

5.2 核心代码示例

下面图片中圈中的部分,就是排除HttpServletRequest和HttpServletResponse参数的核心代码。
在这里插入图片描述

5.3 测试代码

package com.example.web.api.log.controller;

import com.example.core.log.annotation.ApiLog;
import com.example.core.model.PageQuery;
import com.example.web.model.query.UserQuery;
import com.example.web.model.vo.UserVO;
import com.example.web.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/api/log")
@Tag(name = "接口日志")
public class ApiLogController {

    private final UserService userService;

    public ApiLogController(UserService userService) {
        this.userService = userService;
    }

    @ApiLog
    @GetMapping(path = "users")
    @Operation(summary = "查询用户列表", description = "测试 HttpServletRequest、HttpServletResponse 和 HttpSession,是否在接口日志处理时堵塞线程或抛出异常")
    public List<UserVO> listUsers(@Valid UserQuery userQuery, PageQuery pageQuery,
                                  HttpServletRequest request, HttpServletResponse response, HttpSession session) {
        log.info("查询用户列表。userQuery={},pageQuery={}", userQuery, pageQuery);
        return userService.listUsers(userQuery);
    }

}

5.4 正常调用效果

在这里插入图片描述

5.5 打印HttpServletRequest参数,会阻塞线程,抛出异常

测试不排除控制器方法中的HttpServletRequest参数,直接打印的效果

打印HttpServletRequest 参数,会阻塞线程很长一段时间,大约几十秒,然后会抛出如下异常:

org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space

接口阻塞

在这里插入图片描述

抛出异常NestedServletException-OutOfMemoryError

在这里插入图片描述

接口响应

异常统一处理后,响应给前端,耗时50多秒。

在这里插入图片描述

5.6 打印HttpServletResponse参数,会抛出异常

测试不排除控制器方法中的HttpServletResponse参数,直接打印的效果

打印 HttpServletResponse 参数,会抛出如下异常:

org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.StackOverflowError

抛出异常NestedServletException-StackOverflowError

在这里插入图片描述

接口响应

异常统一处理后,响应给前端。
在这里插入图片描述

六、总结

本文实现了基于Spring Boot的接口请求日志记录方案,通过AOP与自定义注解相结合,为指定接口提供了灵活的日志记录能力,并通过配置项支持日志记录的开启与关闭,优化了系统性能。实际生产环境中,建议采用异步方式(如消息队列)处理接口日志。

  • 30
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
你可以使用 @Slf4j 注解和 AOP 统一处理打印日志,具体实现可参考以下代码: 1. 引入依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> ``` 2. 在应用主类上添加 @EnableAspectJAutoProxy 注解启用 AOP: ```java @SpringBootApplication @EnableAspectJAutoProxy(proxyTargetClass = true) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 3. 定义切面类: ```java @Aspect @Component @Slf4j public class LogAspect { @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)" + "||@annotation(org.springframework.web.bind.annotation.GetMapping)" + "||@annotation(org.springframework.web.bind.annotation.PostMapping)" + "||@annotation(org.springframework.web.bind.annotation.PutMapping)" + "||@annotation(org.springframework.web.bind.annotation.DeleteMapping)") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 记录请求内容 log.info("URL : " + request.getRequestURL().toString()); log.info("HTTP_METHOD : " + request.getMethod()); log.info("IP : " + request.getRemoteAddr()); // 记录调用方法 log.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); // 记录请求参数 log.info("ARGS : " + Arrays.toString(joinPoint.getArgs())); } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) { // 记录响应内容 log.info("RESPONSE : " + ret); } } ``` 4. 在需要打印日志接口方法上添加 @RequestMapping 等注解即可。 压缩文件的操作请参考以下代码: ```java public static void zip(Path sourcePath, Path zipPath) throws IOException { try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath)); Stream<Path> paths = Files.walk(sourcePath)) { paths.filter(p -> !Files.isDirectory(p)) .forEach(p -> { ZipEntry entry = new ZipEntry(sourcePath.relativize(p).toString()); try { zos.putNextEntry(entry); zos.write(Files.readAllBytes(p)); zos.closeEntry(); } catch (IOException e) { e.printStackTrace(); } }); } } ``` 对于中文加密的问题,我不是很确定您要表达的意思,请再提供更详细的问题描述。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宋冠巡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值