diff --git a/demo-0.0.1-SNAPSHOT.jar.original b/demo-0.0.1-SNAPSHOT.jar.original deleted file mode 100644 index 21a6d51..0000000 Binary files a/demo-0.0.1-SNAPSHOT.jar.original and /dev/null differ diff --git a/src/main/java/com/example/demo/DemoApplication.java b/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..7d84819 --- /dev/null +++ b/src/main/java/com/example/demo/DemoApplication.java @@ -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); + } + +} diff --git a/src/main/java/com/example/demo/config/LoggingFilterConfig.java b/src/main/java/com/example/demo/config/LoggingFilterConfig.java new file mode 100644 index 0000000..f524f88 --- /dev/null +++ b/src/main/java/com/example/demo/config/LoggingFilterConfig.java @@ -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 + */ + @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; + } +} diff --git a/src/main/java/com/example/demo/config/WebMvcConfig.java b/src/main/java/com/example/demo/config/WebMvcConfig.java new file mode 100644 index 0000000..2b3f768 --- /dev/null +++ b/src/main/java/com/example/demo/config/WebMvcConfig.java @@ -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 resolvers) { + resolvers.clear(); // 清空所有默认的异常解析器 + } +} diff --git a/src/main/java/com/example/demo/controller/BaseController.java b/src/main/java/com/example/demo/controller/BaseController.java new file mode 100644 index 0000000..3969c2e --- /dev/null +++ b/src/main/java/com/example/demo/controller/BaseController.java @@ -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 "步雪"; + } +} diff --git a/src/main/java/com/example/demo/controller/CheckNotBlankController.java b/src/main/java/com/example/demo/controller/CheckNotBlankController.java new file mode 100644 index 0000000..2bef956 --- /dev/null +++ b/src/main/java/com/example/demo/controller/CheckNotBlankController.java @@ -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); + + } +} diff --git a/src/main/java/com/example/demo/controller/FilterTestController.java b/src/main/java/com/example/demo/controller/FilterTestController.java new file mode 100644 index 0000000..edc8252 --- /dev/null +++ b/src/main/java/com/example/demo/controller/FilterTestController.java @@ -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 + */ + @GetMapping("/get") + public Map 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 + */ + + @PostMapping("/post") + public Map 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 + ); + } + + +} diff --git a/src/main/java/com/example/demo/entity/UserDTO.java b/src/main/java/com/example/demo/entity/UserDTO.java new file mode 100644 index 0000000..f636c83 --- /dev/null +++ b/src/main/java/com/example/demo/entity/UserDTO.java @@ -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; +} diff --git a/src/main/java/com/example/demo/exception/ErrorResponse.java b/src/main/java/com/example/demo/exception/ErrorResponse.java new file mode 100644 index 0000000..e969f4d --- /dev/null +++ b/src/main/java/com/example/demo/exception/ErrorResponse.java @@ -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; + } +} + + diff --git a/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..e942dcc --- /dev/null +++ b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java @@ -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; + } + } +} diff --git a/src/main/java/com/example/demo/filter/MultiReadHttpServletFilter.java b/src/main/java/com/example/demo/filter/MultiReadHttpServletFilter.java new file mode 100644 index 0000000..23c21dc --- /dev/null +++ b/src/main/java/com/example/demo/filter/MultiReadHttpServletFilter.java @@ -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); + } + + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..a12cfa6 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.application.name=demo +server.port=8082 +logback.home.path=/Users/hengspire/data/demo/log/ + diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..87202cb --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,70 @@ + + + + + + + + + + ${LOG_PATTERN} + + + + + + ${LOG_PATTERN} + UTF-8 + + + INFO + ACCEPT + DENY + + + ${LOG_HOME}/info/%d{yyyy-MM-dd}-%i.log + 100MB + 180 + + + + + + ${LOG_PATTERN} + UTF-8 + + + ERROR + ACCEPT + DENY + + + ${LOG_HOME}/error/%d{yyyy-MM-dd}-%i.log + 100MB + 180 + + + + + + ${LOG_PATTERN} + UTF-8 + + + ${LOG_HOME}/httpLogs/%d{yyyy-MM-dd}-%i.log + 100MB + 180 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/example/demo/DemoApplicationTests.java b/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 0000000..2778a6a --- /dev/null +++ b/src/test/java/com/example/demo/DemoApplicationTests.java @@ -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() { + } + +}