You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Boot分步指导:存储请求响应数据、异常及响应耗时至数据库

嘿,作为Spring Boot新手,这个需求其实非常适合用**AOP(面向切面编程)**来实现——不用在每个接口里重复写日志逻辑,统一拦截处理就行。下面是一步步的实操指南:

分步实现接口请求/响应/异常/耗时记录

1. 先把基础依赖配好

确保你的pom.xml(Maven)里包含这些必要依赖,Gradle用户对应转成Gradle格式就行:

<dependencies>
    <!-- Spring Web核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Data JPA(操作数据库) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- MySQL驱动(如果用其他数据库就换对应的驱动) -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- Spring AOP(核心的切面编程依赖) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- Lombok(可选,帮你省掉getter/setter的代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 配置数据库连接

application.properties里填上你的数据库信息,开发阶段用update模式可以自动帮你建表:

spring.datasource.url=jdbc:mysql://localhost:3306/your_db_name
spring.datasource.username=your_db_user
spring.datasource.password=your_db_pwd
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

3. 创建对应数据库表的实体类

写一个ApiLog类,和你说的数据库字段一一对应:

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Table(name = "api_logs") // 表名可以自己改
@Data // Lombok注解,自动生成getter/setter/toString
public class ApiLog {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(columnDefinition = "TEXT") // 请求数据可能很长,用TEXT类型存
    private String requestData;

    @Column(columnDefinition = "TEXT")
    private String responseData;

    @Column(columnDefinition = "TEXT")
    private String exception;

    private Long timeTakenToRespond; // 响应耗时,单位毫秒
}

如果没装Lombok,就手动写所有字段的getter和setter就行。

4. 编写Repository操作数据库

Spring Data JPA帮我们封装了数据库操作,只需要写一个接口:

import org.springframework.data.jpa.repository.JpaRepository;

public interface ApiLogRepository extends JpaRepository<ApiLog, Long> {
}

这样就自带了save()等基础方法,用来存日志足够了。

5. 核心:用AOP切面统一处理日志

这一步是关键,通过切面拦截所有Controller的请求,自动记录请求、响应、异常和耗时:

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.nio.charset.StandardCharsets;

@Aspect
@Component
public class ApiLoggingAspect {

    @Autowired
    private ApiLogRepository apiLogRepository;

    @Autowired
    private ObjectMapper objectMapper; // 用来把对象转成JSON字符串

    // 拦截你所有Controller包下的方法,记得把包路径改成你自己的!
    @Around("execution(* com.your_package.controller..*(..))")
    public Object logApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
        ApiLog apiLog = new ApiLog();
        long startTime = System.currentTimeMillis();

        try {
            // 1. 获取并记录请求数据(用ContentCachingRequestWrapper解决@RequestBody重复读流问题)
            ContentCachingRequestWrapper request = (ContentCachingRequestWrapper) getCurrentRequest();
            String requestData = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8);
            // 重要:敏感数据(比如密码)要屏蔽!
            requestData = maskSensitiveData(requestData);
            apiLog.setRequestData(requestData);

            // 2. 执行接口方法,获取响应
            Object response = joinPoint.proceed();
            String responseData = objectMapper.writeValueAsString(response);
            apiLog.setResponseData(responseData);

            return response;
        } catch (Exception e) {
            // 3. 捕获异常,记录异常信息
            apiLog.setException(e.getMessage() + "\n" + getStackTrace(e));
            throw e; // 继续抛出异常,让Spring正常返回给客户端错误
        } finally {
            // 4. 计算耗时,保存日志到数据库
            long endTime = System.currentTimeMillis();
            apiLog.setTimeTakenToRespond(endTime - startTime);
            apiLogRepository.save(apiLog);
        }
    }

    // 获取当前请求对象
    private HttpServletRequest getCurrentRequest() {
        return ((org.springframework.web.context.request.ServletRequestAttributes) 
                org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
    }

    // 把异常栈转成字符串
    private String getStackTrace(Exception e) {
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : e.getStackTrace()) {
            sb.append(element.toString()).append("\n");
        }
        return sb.toString();
    }

    // 屏蔽敏感数据,比如密码
    private String maskSensitiveData(String requestData) {
        try {
            if (requestData.contains("password")) {
                com.fasterxml.jackson.databind.JsonNode node = objectMapper.readTree(requestData);
                ((com.fasterxml.jackson.databind.node.ObjectNode) node).put("password", "***");
                return objectMapper.writeValueAsString(node);
            }
        } catch (Exception e) {
            // 解析失败就原样返回,不影响主逻辑
        }
        return requestData;
    }
}

额外补充:请求体缓存Filter

为了避免@RequestBody重复读流的问题,还要加一个Filter包装请求:

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.io.IOException;

@Component
public class RequestCachingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 包装请求,让输入流可以重复读取
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrappedRequest, response);
    }
}

6. 测试你的接口

比如写一个你提到的/authenticateUser接口:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {

    @PostMapping("/authenticateUser")
    public UserResponse authenticateUser(@RequestBody UserRequest request) {
        // 模拟认证逻辑
        UserResponse response = new UserResponse();
        response.setFirstName(request.getUsername());
        response.setLastName("swamy");
        response.setEmail("manteswamy@gmail.com");
        // 可以故意抛异常测试:throw new RuntimeException("认证失败,请重试");
        return response;
    }

    // 内部请求/响应类
    static class UserRequest {
        private String username;
        private String password;
        // getter和setter
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }

    static class UserResponse {
        private String firstName;
        private String lastName;
        private String email;
        // getter和setter
        public String getFirstName() { return firstName; }
        public void setFirstName(String firstName) { this.firstName = firstName; }
        public String getLastName() { return lastName; }
        public void setLastName(String lastName) { this.lastName = lastName; }
        public String getEmail() { return email; }
        public void setEmail(String email) { this.email = email; }
    }
}

启动项目后调用接口,去数据库的api_logs表看看,请求数据(密码已经被屏蔽)、响应数据、耗时都能查到;如果故意抛出异常,exception字段也会记录异常信息。

新手注意事项

  • 数据库字段一定要用TEXT类型,JSON数据可能很长,VARCHAR存不下
  • 生产环境下一定要屏蔽敏感数据(比如密码、token),别直接存明文
  • 如果你的接口返回非JSON格式(比如文件),要在切面里加判断,避免objectMapper报错

内容的提问来源于stack exchange,提问作者mantelinga r

火山引擎 最新活动