不改动接口参数和返回,打印接口ip/url/时间,之后加到拦截器或者AOP中
注意事项
-
IP:
request.getRemoteAddr()
获取的可能是代理服务器IP而非真实客户端IP,真实IP需要从X-Forwarded-For
等header中获取。 -
线程安全:
RequestContextHolder
是基于ThreadLocal实现的,在异步方法中可能无法获取到request -
日志框架:建议使用SLF4J等日志框架替代System.out.println
-
异常处理:确保在catch块中也记录结束时间,否则会丢失异常请求的日志
方法1:在入参中添加HttpServletRequest
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;@RestController
@RequestMapping("/api")
public class YourController {@GetMapping("/your-endpoint")public ResponseEntity<?> yourMethod(HttpServletRequest request) {// 获取请求信息String ip = request.getRemoteAddr();String methodName = "yourMethod";Date startTime = new Date();System.out.println("请求开始 - IP: " + ip + ", 方法: " + methodName + ", 时间: " + startTime);try {// 你的业务逻辑代码// ...Date endTime = new Date();long duration = endTime.getTime() - startTime.getTime();System.out.println("请求结束 - 方法: " + methodName + ", 时间: " + endTime + ", 耗时: " + duration + "ms");return ResponseEntity.ok("Success");} catch (Exception e) {Date endTime = new Date();System.out.println("请求异常 - 方法: " + methodName + ", 时间: " + endTime);throw e;}}
}
方法2:使用RequestContextHolder获取Request
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;@RestController
@RequestMapping("/api")
public class YourController {@GetMapping("/your-endpoint")public ResponseEntity<?> yourMethod() {// 获取当前请求对象HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String ip = request.getRemoteAddr();String methodName = "yourMethod";Date startTime = new Date();System.out.println("请求开始 - IP: " + ip + ", 方法: " + methodName + ", 时间: " + startTime);try {// 你的业务逻辑代码// ...Date endTime = new Date();long duration = endTime.getTime() - startTime.getTime();System.out.println("请求结束 - 方法: " + methodName + ", 时间: " + endTime + ", 耗时: " + duration + "ms");return ResponseEntity.ok("Success");} catch (Exception e) {Date endTime = new Date();System.out.println("请求异常 - 方法: " + methodName + ", 时间: " + endTime);throw e;}}
}...
private String getClientIP(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr();} return ip.split(",")[0]; // 处理多级代理的情况 }}String apiName = request.getRequestURI();
方法3:方法2封装成工具类
日志工具类代码复用
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;public class RequestLogUtil {public static void logRequestStart(String methodName) {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String ip = request.getRemoteAddr();Date startTime = new Date();System.out.println("请求开始 - IP: " + ip + ", 方法: " + methodName + ", 时间: " + startTime);}public static void logRequestEnd(String methodName) {Date endTime = new Date();System.out.println("请求结束 - 方法: " + methodName + ", 时间: " + endTime);}
}
然后在控制器中使用:
@GetMapping("/your-endpoint")
public ResponseEntity<?> yourMethod() {RequestLogUtil.logRequestStart("yourMethod");try {// 业务逻辑// ...RequestLogUtil.logRequestEnd("yourMethod");return ResponseEntity.ok("Success");} catch (Exception e) {RequestLogUtil.logRequestEnd("yourMethod");throw e;}
}
后期
方式一:拦截器(Interceptor)
1. 创建日志拦截器类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.time.Instant;public class RequestLogInterceptor implements HandlerInterceptor {private static final Logger LOG = LoggerFactory.getLogger(RequestLogInterceptor.class);private static final ThreadLocal<Instant> startTimeThreadLocal = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {startTimeThreadLocal.set(Instant.now());return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {Instant startTime = startTimeThreadLocal.get();Instant endTime = Instant.now();long duration = Duration.between(startTime, endTime).toMillis();String clientIP = getClientIP(request);String apiName = request.getRequestURI();LOG.info("接口请求日志 - IP: {}, 接口: {}, 开始时间: {}, 结束时间: {}, 耗时: {}ms",clientIP, apiName, startTime, endTime, duration);startTimeThreadLocal.remove();}private String getClientIP(HttpServletRequest request) {String ip = request.getHeader("X-Forwarded-For");if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip;}
}
2. 注册拦截器
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new RequestLogInterceptor()).addPathPatterns("/api/**"); // 拦截所有/api路径的请求}
}
方式二:AOP
1. 添加AOP依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 创建切面类
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 javax.servlet.http.HttpServletRequest;
import java.time.Instant;@Aspect
@Component
public class RequestLogAspect {private static final Logger LOG = LoggerFactory.getLogger(RequestLogAspect.class);@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();Instant startTime = Instant.now();String clientIP = getClientIP(request);String apiName = request.getRequestURI();try {Object result = joinPoint.proceed(); // 执行目标方法return result;} finally {Instant endTime = Instant.now();long duration = endTime.toEpochMilli() - startTime.toEpochMilli();LOG.info("接口请求日志 - IP: {}, 接口: {}, 开始时间: {}, 结束时间: {}, 耗时: {}ms",clientIP, apiName, startTime, endTime, duration);}}private String getClientIP(HttpServletRequest request) {String ip = request.getHeader("X-Forwarded-For");if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip.split(",")[0]; // 处理多级代理的情况}
}
说明
-
获取客户端IP
- 优先从
X-Forwarded-For
头获取真实IP(适用于通过代理的情况)。 - 直接使用
request.getRemoteAddr()
作为备用方案。
- 优先从
-
接口名称
- 使用
request.getRequestURI()
获取请求路径(如/api/user
)。 - 若需更精细的接口名(如方法名),可在AOP中通过
joinPoint.getSignature().getName()
获取。
- 使用
-
耗时计算
- 使用
Instant
记录时间戳,确保高精度。 - 在拦截器的
afterCompletion
或AOP的finally
块中计算总耗时。
- 使用
-
线程安全
- 拦截器中通过
ThreadLocal
存储开始时间,避免并发问题。
- 拦截器中通过
日志输出示例
2023-10-05 14:20:00 INFO RequestLogAspect - 接口请求日志 - IP: 192.168.1.100, 接口: /api/user, 开始时间: 2023-10-05T14:20:00.123Z, 结束时间: 2023-10-05T14:20:00.456Z, 耗时: 333ms
扩展建议
- 异步日志:若日志频繁,可使用异步Appender(如Logback的
AsyncAppender
)提升性能。 - JSON格式:输出为JSON便于ELK采集:
LOG.info("{}", new ObjectMapper().writeValueAsString(logMap));
- 自定义注解:通过注解标记需要记录日志的接口:
@Around("@annotation(com.example.YourLoggableAnnotation)")