1
This commit is contained in:
Binary file not shown.
14
src/main/java/com/example/demo/DemoApplication.java
Normal file
14
src/main/java/com/example/demo/DemoApplication.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/example/demo/config/WebMvcConfig.java
Normal file
21
src/main/java/com/example/demo/config/WebMvcConfig.java
Normal 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(); // 清空所有默认的异常解析器
|
||||
}
|
||||
}
|
||||
@@ -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 "步雪";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
16
src/main/java/com/example/demo/entity/UserDTO.java
Normal file
16
src/main/java/com/example/demo/entity/UserDTO.java
Normal 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;
|
||||
}
|
||||
41
src/main/java/com/example/demo/exception/ErrorResponse.java
Normal file
41
src/main/java/com/example/demo/exception/ErrorResponse.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
4
src/main/resources/application.properties
Normal file
4
src/main/resources/application.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
spring.application.name=demo
|
||||
server.port=8082
|
||||
logback.home.path=/Users/hengspire/data/demo/log/
|
||||
|
||||
70
src/main/resources/logback-spring.xml
Normal file
70
src/main/resources/logback-spring.xml
Normal 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>
|
||||
13
src/test/java/com/example/demo/DemoApplicationTests.java
Normal file
13
src/test/java/com/example/demo/DemoApplicationTests.java
Normal 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() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user