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




