This commit is contained in:
2026-01-22 11:07:12 +08:00
parent 4dea9c55a4
commit fe3db4cbce
14 changed files with 468 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,14 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,31 @@
package com.example.demo.config;
import com.example.demo.filter.MultiReadHttpServletFilter;
import lombok.NonNull;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author buxue
*/
@Configuration
public class LoggingFilterConfig {
/**
* 可以重复读取请求体内容,-900为高优先级
*
* @return FilterRegistrationBean<MultiReadHttpServletFilter>
*/
@Bean
public FilterRegistrationBean<@NonNull MultiReadHttpServletFilter> registerApiFilter() {
FilterRegistrationBean<@NonNull MultiReadHttpServletFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new MultiReadHttpServletFilter());
registrationBean.setName("MultiReadHttpServletFilter");
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(-900);
return registrationBean;
}
}

View File

@@ -0,0 +1,21 @@
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* @author buxue
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 禁用Spring默认的异常解析器让参数缺失等异常能抛回过滤器的catch块
*/
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.clear(); // 清空所有默认的异常解析器
}
}

View File

@@ -0,0 +1,22 @@
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author buxue
*/
//@RestController
@RequestMapping("/extend")
public class BaseController {
/**
* 验证类继承问题
*
* @return
*/
@GetMapping("testGet")
public String testGet() {
return "步雪";
}
}

View File

@@ -0,0 +1,28 @@
package com.example.demo.controller;
import com.example.demo.entity.UserDTO;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* @author buxue
*/
@Slf4j
@RestController
//@RequestMapping("/check")
public class CheckNotBlankController extends BaseController {
/**
* RequestMapping 继承 + 触发@NotBlank校验
*
* @param userDTO
*/
@PostMapping("/one")
public void check(@Valid @RequestBody UserDTO userDTO) {
UserDTO user = new UserDTO();
user.setAge(userDTO.getAge());
user.setName(userDTO.getName());
log.info("user:{}", user);
}
}

View File

@@ -0,0 +1,65 @@
package com.example.demo.controller;
import cn.hutool.core.lang.Assert;
import com.example.demo.entity.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @author buxue
*/
@Slf4j
@RestController
@RequestMapping("/test/filter")
public class FilterTestController {
/**
* get请求记录请求的入参/出参,以及请求的响应耗时。 并写到一个单独的日志文件中
*
* @return Map<String, Object>
*/
@GetMapping("/get")
public Map<String, Object> testGet(
@RequestParam String name,
@RequestParam Integer age) {
try {
log.info("进入get请求");
Thread.sleep(50);
} catch (Exception e) {
Thread.currentThread().interrupt();
log.error("进入get请求异常{}", e.getMessage());
}
return Map.of(
"code", 200,
"msg", "GET请求成功",
"data", Map.of("name", name, "age", age)
);
}
/**
* post请求记录请求的入参/出参,以及请求的响应耗时。 并写到一个单独的日志文件中
*
* @return Map<String, Object>
*/
@PostMapping("/post")
public Map<String, Object> testPost(
@RequestBody UserDTO user) {
try {
log.info("进入post请求");
Thread.sleep(80);
} catch (Exception e) {
Thread.currentThread().interrupt();
log.error("进入post请求异常{}", e.getMessage());
}
return Map.of(
"code", 200,
"msg", "POST请求成功",
"data", user
);
}
}

View File

@@ -0,0 +1,16 @@
package com.example.demo.entity;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* @author buxue
*/
@Data
public class UserDTO {
@NotBlank(message = "姓名不能为空")
private String name;
@NotBlank(message = "年纪不能为空")
private String age;
}

View File

@@ -0,0 +1,41 @@
package com.example.demo.exception;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @author buxue
*/
@Data
public class ErrorResponse {
private LocalDateTime timestamp;
private String traceId;
private Integer status;
private String exceptionType;
private String message;
private String path;
/**
* 快速构建异常响应
*/
public static ErrorResponse build(String traceId, Integer status, String exceptionType, String message, String path) {
ErrorResponse response = new ErrorResponse();
response.setTimestamp(LocalDateTime.now());
response.setTraceId(traceId);
response.setStatus(status);
response.setExceptionType(exceptionType);
response.setMessage(message);
response.setPath(path);
return response;
}
}

View File

@@ -0,0 +1,47 @@
package com.example.demo.exception;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author buxue
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public ErrorResponse handleAllException(Exception e, HttpServletRequest request) {
String traceId = (String) request.getAttribute("traceId");
String exceptionType = e.getClass().getSimpleName();
String errorMsg = e.getMessage() != null ? e.getMessage() : "服务器处理异常";
String requestPath = request.getRequestURI();
HttpStatus httpStatus = getHttpStatus(e);
Integer status = httpStatus.value();
log.error("[{}] 异常捕获 | 类型:{} | 路径:{} | 提示:{}", traceId, exceptionType, requestPath, errorMsg, e);
return ErrorResponse.build(traceId, status, exceptionType, errorMsg, requestPath);
}
private HttpStatus getHttpStatus(Exception e) {
ResponseStatus responseStatus = AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class);
if (responseStatus != null) {
// 如果注解存在返回注解中定义的状态码贴合HTTP标准
return responseStatus.code();
} else {
// 无注解的异常兜底返回500服务器内部错误
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
}

View File

@@ -0,0 +1,96 @@
package com.example.demo.filter;
import com.hengspire.common.http.servlet.MultiReadHttpServletRequestWrapper;
import com.hengspire.common.http.servlet.MultiReadHttpServletResponseWrapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
/**
* @author buxue
*/
@Slf4j(topic = "httpLog")
@RequiredArgsConstructor
public class MultiReadHttpServletFilter extends OncePerRequestFilter {
/**
* 过滤器处理方法,实现请求/响应包装、链路追踪、请求耗时统计及日志记录。
*
* @param request 原生HTTP请求对象
* @param response 原生HTTP响应对象
* @param filterChain 过滤器链
* @throws ServletException Servlet相关异常
* @throws IOException io流异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
MultiReadHttpServletRequestWrapper req = new MultiReadHttpServletRequestWrapper(request);
MultiReadHttpServletResponseWrapper resp = new MultiReadHttpServletResponseWrapper(response);
String originalTraceId = MDC.get("traceId");
String traceId;
if (originalTraceId != null && !originalTraceId.isEmpty()) {
traceId = originalTraceId;
} else {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("traceId", traceId);
req.setAttribute("traceId", traceId);
long startTime = System.currentTimeMillis();
try {
filterChain.doFilter(req, resp);
} catch (Exception e) {
log.error("[{}] 请求处理发生异常,请求路径:{},请求方法:{}", traceId, request.getRequestURI(), request.getMethod(), e);
throw new ServletException(e);
} finally {
long costTime = System.currentTimeMillis() - startTime;
logRequestLog(req, resp, costTime, traceId);
MDC.remove("traceId");
}
}
/**
* 返回请求日志
*
* @param req 原生HTTP请求对象
* @param resp 原生HTTP响应对象
* @param costTime 请求耗时
* @param traceId traceId
*/
private void logRequestLog(MultiReadHttpServletRequestWrapper req,
MultiReadHttpServletResponseWrapper resp,
long costTime,
String traceId) {
String requestUrl = req.getRequestURL().toString();
String requestMethod = req.getMethod();
String requestParams = "";
if ("GET".equalsIgnoreCase(requestMethod)) {
requestParams = req.getQueryString() == null ? "" : req.getQueryString();
} else {
requestParams = req.getBodyAsString().isEmpty() ? "" : req.getBodyAsString();
}
String responseBody = resp.getBodyAsString();
String logContent = String.format(
"【请求详情】\n" +
"traceId: %s\n" +
"请求URL: %s\n" +
"请求方法: %s\n" +
"请求入参: %s\n" +
"响应出参: %s\n" +
"响应耗时: %d ms\n" +
"------------------------",
traceId, requestUrl, requestMethod, requestParams, responseBody, costTime
);
log.info(logContent);
}
}

View File

@@ -0,0 +1,4 @@
spring.application.name=demo
server.port=8082
logback.home.path=/Users/hengspire/data/demo/log/

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty scope="context" name="LOG_HOME" source="logback.home.path" />
<property name="LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%X{traceId:-}){highlight} %clr(%logger){cyan} %clr(:){faint} %m%n%ex" />
<conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/info/%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>180</maxHistory>
</rollingPolicy>
</appender>
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error/%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>180</maxHistory>
</rollingPolicy>
</appender>
<appender name="httpLogsFilter" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/httpLogs/%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>180</maxHistory>
</rollingPolicy>
</appender>
<logger name="httpLog" additivity="false" level="INFO">
<appender-ref ref="httpLogsFilter"/>
</logger>
<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="info" />
<appender-ref ref="error" />
</root>
</configuration>

View File

@@ -0,0 +1,13 @@
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
}