Spring Boot 記錄Controller接口請(qǐng)求日志七種方式,第六種性能極高
環(huán)境:SpringBoot3.2.5
1. 簡(jiǎn)介
API接口記錄請(qǐng)求相關(guān)日志是確保系統(tǒng)安全性、可維護(hù)性以及性能監(jiān)控的重要措施之一。通過(guò)記錄API請(qǐng)求的詳細(xì)信息,包括請(qǐng)求時(shí)間、來(lái)源IP地址、請(qǐng)求方法(GET、POST等)、請(qǐng)求參數(shù)、響應(yīng)狀態(tài)碼及響應(yīng)時(shí)間等,開(kāi)發(fā)者能夠有效追蹤錯(cuò)誤源頭、分析用戶行為模式、優(yōu)化系統(tǒng)性能,并保障數(shù)據(jù)安全。
接下來(lái),我將介紹七種API接口請(qǐng)求日志的記錄方式:
圖片
準(zhǔn)備接口
@RestController
@RequestMapping("/api")
public class ApiController {
private final static Logger logger = LoggerFactory.getLogger(ApiController.class) ;
@GetMapping("/query/{category}")
public ResponseEntity<Object> query(@PathVariable String category, @RequestParam String keyword) {
logger.info("查詢數(shù)據(jù), 分類: {}, 關(guān)鍵詞: {}", category, keyword) ;
return ResponseEntity.ok("success") ;
}
}
接下來(lái)的示例我們都將通過(guò)該接口進(jìn)行測(cè)試。
2. 實(shí)戰(zhàn)案例
2.1 Filter過(guò)濾
通過(guò)Filter方式可以攔截并記錄HTTP請(qǐng)求和響應(yīng)的組件。通過(guò)Filter技術(shù),開(kāi)發(fā)人員可以在請(qǐng)求到達(dá)目標(biāo)資源之前或響應(yīng)返回客戶端之前進(jìn)行日志記錄,從而實(shí)現(xiàn)訪問(wèn)控制和請(qǐng)求預(yù)處理等功能。
如果你考慮使用Filter,那么非常幸運(yùn)你不需要自己去實(shí)現(xiàn),Spring MVC給我們提供了此功能的實(shí)現(xiàn),如下類圖:
圖片
這里我們可以直接注冊(cè)CommonRequestLoggingFilter過(guò)濾器。
@Bean
FilterRegistrationBean<Filter> loggingFilter() {
FilterRegistrationBean<Filter> reg = new FilterRegistrationBean<>() ;
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter() ;
// 是否記錄客戶端信息
filter.setIncludeClientInfo(true) ;
// 記錄請(qǐng)求header數(shù)據(jù)
filter.setIncludeHeaders(true) ;
// 記錄請(qǐng)求body內(nèi)容
filter.setIncludePayload(true) ;
// 是否記錄查詢字符串
filter.setIncludeQueryString(true) ;
reg.setFilter(filter) ;
reg.addUrlPatterns("/api/*") ;
return reg ;
}
這還不夠,還需要配合下面日志級(jí)別配置:
logging:
level:
'[org.springframework.web.filter]': debug
接下來(lái),訪問(wèn)/api/query/book?keyword=java接口
圖片
成功記錄了請(qǐng)求的相關(guān)數(shù)據(jù)。
2.2 HandlerInterceptor記錄日志
HandlerInterceptor用于攔截請(qǐng)求處理也是非常的實(shí)用。它允許開(kāi)發(fā)者在請(qǐng)求到達(dá)控制器之前、之后或發(fā)生異常時(shí)執(zhí)行特定邏輯,從而實(shí)現(xiàn)對(duì)請(qǐng)求處理的全面監(jiān)控和日志記錄。
定義攔截器
@Component
public class LoggingRequestInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LoggingRequestInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
long startTime = Instant.now().toEpochMilli();
String time = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) ;
logger.info("Request URL::" + request.getRequestURL().toString() + ":: StartTime=" + time);
request.setAttribute("startTime", startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
long startTime = (Long) request.getAttribute("startTime");
logger.info("Request URL::" + request.getRequestURL().toString() + ":: TimeTaken="
+ (Instant.now().toEpochMilli() - startTime));
}
}
注冊(cè)攔截器
@Component
public class InterceptorConfig implements WebMvcConfigurer {
private final LoggingRequestInterceptor loggingRequestInterceptor ;
public InterceptorConfig(LoggingRequestInterceptor loggingRequestInterceptor) {
this.loggingRequestInterceptor = loggingRequestInterceptor;
}
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingRequestInterceptor).addPathPatterns("/api/**") ;
}
}
請(qǐng)求API接口
2.3 使用AOP技術(shù)
通過(guò)AOP記錄日志是一種強(qiáng)大的技術(shù),它允許開(kāi)發(fā)者在不修改業(yè)務(wù)邏輯代碼的情況下,將日志記錄等橫切關(guān)注點(diǎn)與業(yè)務(wù)邏輯分離。AOP能夠自動(dòng)攔截方法調(diào)用,并記錄方法執(zhí)行前后的信息,為系統(tǒng)提供全面的日志支持。
自定義注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Log {
/**模塊*/
String module() default "" ;
/**具體操作說(shuō)明*/
String desc() default "" ;
}
定義切面
@Component
@Aspect
public class LogAspect {
@Pointcut("@annotation(log)")
private void recordLog(Log log) {}
@Around("recordLog(log)")
public Object logAround(ProceedingJoinPoint pjp, Log log) throws Throwable {
String module = log.module();
String desc = log.desc();
long uid = System.nanoTime() ;
String threadName = Thread.currentThread().getName();
System.err.printf("%s - 【%d】 模塊: %s, 操作: %s, 請(qǐng)求參數(shù): %s%n", threadName, uid, module, desc, Arrays.toString(pjp.getArgs())) ;
Object ret = pjp.proceed() ;
System.err.printf("%s - 【%d】 返回值: %s%n", threadName, uid, ret) ;
return ret ;
}
}
修改Controller接口
@Log(module = "綜合查詢", desc = "查詢商品信息")
@GetMapping("/query/{category}")
public ResponseEntity<Object> query(@PathVariable String category, @RequestParam String keyword)
請(qǐng)求API接口
圖片
2.4 基于Servlet請(qǐng)求事件機(jī)制
默認(rèn)情況下,當(dāng)一個(gè)請(qǐng)求到達(dá)后,Spring MVC底層會(huì)發(fā)布一個(gè)事件ServletRequestHandledEvent,我們只需要監(jiān)聽(tīng)該事件也是可以獲取到詳細(xì)的請(qǐng)求/響應(yīng)信息。
自定義事件監(jiān)聽(tīng)器
@Component
public class RequestEventListener implements ApplicationListener<ServletRequestHandledEvent> {
private static final Logger logger = LoggerFactory.getLogger(RequestEventListener.class) ;
@Override
public void onApplicationEvent(ServletRequestHandledEvent event) {
logger.info("請(qǐng)求信息: {}", event) ;
}
}
請(qǐng)求API接口
圖片
你可以通過(guò)ServletRequestHandledEvent事件對(duì)象獲取具體的明細(xì)信息
圖片
2.5 使用第三方Logbook組件
Logbook是一個(gè)可擴(kuò)展的Java庫(kù),能夠?yàn)椴煌目蛻舳撕头?wù)器端技術(shù)實(shí)現(xiàn)完整的請(qǐng)求和響應(yīng)日志記錄。它允許開(kāi)發(fā)者記錄應(yīng)用程序接收或發(fā)送的任何HTTP流量。
引入依賴
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>3.10.0</version>
</dependency>
配置文件
logging:
level:
'[org.zalando.logbook.Logbook]': TRACE
請(qǐng)求API接口
圖片
2.6 自定義HandlerMethod
如果你僅僅是針對(duì)Controller接口進(jìn)行日志記錄處理,那么強(qiáng)烈推薦此種方式。
2.7 三方API接口調(diào)用
通常我們會(huì)使用RestTemplate/RestClient進(jìn)行第三方接口的調(diào)用;如果要記錄此種情況的日志信息,那么我們需要自定義ClientHttpRequestInterceptor攔截器。
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
logRequest(request, body) ;
ClientHttpResponse response = execution.execute(request, body);
logResponse(response);
return response;
}
private void logRequest(HttpRequest request, byte[] body) throws IOException {
if (log.isDebugEnabled()) {
log.debug("===========================request begin================================================");
log.debug("URI : {}", request.getURI());
log.debug("Method : {}", request.getMethod());
log.debug("Headers : {}", request.getHeaders());
log.debug("Request body: {}", new String(body, "UTF-8"));
log.debug("==========================request end================================================");
}
}
private void logResponse(ClientHttpResponse response) throws IOException {
if (log.isDebugEnabled()) {
log.debug("============================response begin==========================================");
log.debug("Status code : {}", response.getStatusCode());
log.debug("Status text : {}", response.getStatusText());
log.debug("Headers : {}", response.getHeaders());
log.debug("Response body: {}", StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()));
log.debug("=======================response end=================================================");
}
}
}
RestTemplate配置攔截器
@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = builder
.additionalInterceptors(
new LoggingClientHttpRequestInterceptor())
.build() ;
return restTemplate ;
}
日志打印情況如下:
圖片
如果使用的RestClient,那么可以同一個(gè)攔截器,配置如下:
@Bean
RestClient restClient() {
return RestClient
.builder()
.requestInterceptor(
new LoggingClientHttpRequestInterceptor())
.build() ;
}