1. 簡介
日誌記錄在構建 Web 應用程序中起着至關重要的作用。它能夠實現高效的調試、性能監控和錯誤追蹤。然而,以一種乾淨、有條理的方式實施日誌記錄,尤其是在集中捕獲每個請求、響應和異常時,是一個常見的挑戰。
在本教程中,我們將在一 個 Spring Boot 應用程序中實現集中式日誌記錄。 我們將提供一份詳細的、分步的指南,涵蓋所有必要的配置,並使用實際的代碼示例演示整個過程。
2. Maven 依賴
首先,請確保我們在 <em pom.xml</em> 中包含了必要的依賴項。我們需要 Spring Web 以及可選的 Spring Boot Actuator 用於更好的監控:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>3.4.1</version>
</dependency>依賴項設置完成後,我們準備實施日誌記錄邏輯。
3. 使用 Spring Boot Actuator 進行請求日誌記錄
在創建自定義邏輯之前,請考慮使用 Spring Boot Actuator,它能夠自動記錄 HTTP 請求。 Actuator 模塊包含一個端點 /actuator/httpexchanges (適用於 Spring Boot 2.0+),它顯示了應用程序最後 100 個 HTTP 請求。 此外,我們還將添加 spring-boot-starter-actuator 依賴項,並配置應用程序屬性以暴露 httpexchanges 端點:
management:
endpoints:
web:
exposure:
include: httpexchanges我們還將添加一個基於內存的存儲庫來存儲跟蹤數據。這允許我們臨時存儲跟蹤數據,而不會影響主應用程序邏輯:
@Configuration
public class HttpTraceActuatorConfiguration {
@Bean
public InMemoryHttpExchangeRepository createTraceRepository() {
return new InMemoryHttpExchangeRepository();
}
}現在我們可以運行我們的應用程序並訪問 /actuator/httpexchanges 以查看記錄的:
4. 創建自定義日誌過濾器
創建自定義日誌過濾器允許我們根據自身需求定製日誌處理流程。雖然 Spring Boot Actuator 提供了一種便捷的方式來記錄 HTTP 請求和響應,但它可能無法覆蓋所有詳細或自定義日誌需求。自定義過濾器允許我們記錄額外的詳細信息,以特定方式格式化日誌,或將日誌集成到其他監控工具中。此外,記錄默認情況下由 Actuator 等工具未捕獲的敏感數據也很有用。例如,我們可以以任何格式記錄請求頭、請求體和響應詳情。
4.1. 實現自定義過濾器
此過濾器將成為所有傳入的 HTTP 請求和傳出的 HTTP 響應的集中式攔截器。通過實現 <em >Filter</em> 接口,我們可以記錄通過應用程序的每個請求和響應的詳細信息,從而提高調試和監控的效率:
@Override
public void doFilter(jakarta.servlet.ServletRequest request, jakarta.servlet.ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
logRequest(httpRequest);
ResponseWrapper responseWrapper = new ResponseWrapper(httpResponse);
chain.doFilter(request, responseWrapper);
logResponse(httpRequest, responseWrapper);
} else {
chain.doFilter(request, response);
}
}在我們的自定義過濾器中,我們使用兩種額外的機制來記錄請求和響應:
private void logRequest(HttpServletRequest request) {
logger.info("Incoming Request: [{}] {}", request.getMethod(), request.getRequestURI());
request.getHeaderNames().asIterator().forEachRemaining(header ->
logger.info("Header: {} = {}", header, request.getHeader(header))
);
}
private void logResponse(HttpServletRequest request, ResponseWrapper responseWrapper) throws IOException {
logger.info("Outgoing Response for [{}] {}: Status = {}",
request.getMethod(), request.getRequestURI(), responseWrapper.getStatus());
logger.info("Response Body: {}", responseWrapper.getBodyAsString());
}4.2. 自定義響應包裝器
我們將實現一個自定義的 ResponseWrapper,它允許我們在基於servlet的Web應用程序中捕獲和操作HTTP響應的主體。這個包裝器非常實用,因為默認的 HttpServletResponse 在響應主體已寫入後,不會直接提供訪問響應主體的途徑。通過攔截並存儲響應內容,我們可以對其進行日誌記錄或修改,然後再將其發送到客户端。
public class ResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
private final PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() {
return new ServletOutputStream() {
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) {
outputStream.write(b);
}
};
}
@Override
public PrintWriter getWriter() {
return writer;
}
@Override
public void flushBuffer() throws IOException {
super.flushBuffer();
writer.flush();
}
public String getBodyAsString() {
writer.flush();
return outputStream.toString();
}
}4.3. 全局處理異常
Spring Boot 提供了一種便捷的方式通過 <em @ControllerAdvice</em> 註解來管理異常,該註解定義了一個全局異常處理器。該處理器將捕獲請求處理過程中發生的任何異常,並記錄有關該異常的有用信息。
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception ex) {
logger.error("Exception caught: {}", ex.getMessage(), ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred");
}
}我們還使用了 ExceptionHandler 註解。該註解指定方法將處理特定類型的異常,在本例中是 Exception.class。這意味着該處理程序將捕獲所有異常(除非在應用程序的其他地方已進行處理)。我們捕獲所有異常、記錄它們並向客户端返回一個通用的錯誤響應。通過記錄堆棧跟蹤,確保沒有細節遺漏。
5. 測試實現
要測試日誌設置,我們可以創建一個簡單的 REST 控制器:
@RestController
@RequestMapping("/api")
public class TestController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
@GetMapping("/error")
public String error() {
throw new RuntimeException("This is a test exception");
}
}訪問 /api/hello 將會記錄請求和響應:
INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter : Incoming Request: [GET] /api/hello
NFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter : Header: host = localhost:8080
INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter : Header: connection = keep-alive
…
INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter : Outgoing Response for [GET] /api/hello: Status = 200
INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter : Response Body: Hello, World!
訪問 /api/error 將會觸發異常,並在過程中進行日誌記錄:
INFO 19561 --- [log-all-requests] [nio-8080-exec-7] c.baeldung.logallrequests.LoggingFilter : Outgoing Response for [GET] /api/error: Status = 5006. 結論
在本文中,我們已成功實施了集中式日誌記錄機制,用於請求、響應和異常。通過利用 Spring Boot Actuator 或使用 Filter 和 ControllerAdvice 創建自定義日誌邏輯,我們確保了應用程序保持清潔和可維護。