您现在的位置是:首页 >技术教程 >springboot 统一异常处理 + 日志记录网站首页技术教程
springboot 统一异常处理 + 日志记录
在项目的开发中,在某些情况下,比如非业务的操作,日志记录,权限认证和异常处理等。我们需要对客户端发出的请求进行拦截,常用的API拦截方式有Fliter,Interceptor,ControllerAdvice以及Aspect。先简单介绍一下不同的拦截方式。
一.拦截方式
过滤器:Filter
可以获得Http原始的请求和响应信息,但是拿不到响应方法的信息。
拦截器:Interceptor
可以获得Http原始的请求和响应信息,也拿得到响应方法的信息,但是拿不到方法响应中参数的值。
ControllerAdvice(Controller增强,自spring3.2的时候推出)
主要是用于全局的异常拦截和处理,这里的异常可以使自定义异常也可以是JDK里面的异常,用于处理当数据库事务业务和预期不同的时候抛出封装后的异常,进行数据库事务回滚,并将异常的显示给用户。
切片:Aspect
主要是进行公共方法的,可以拿得到方法响应中参数的值,但是拿不到原始的Http请求和相对应响应的方法。
二.正文
在开发过程中,有时候一个业务调用链场景,很长,调了各种各样的方法,看日志的时候,各个接口的日志穿插,确实让人头大 。我们需要把同一次的业务调用链上的日志串起来。于是就引出了日志码,在日志打印时输出一个唯一标识(比如UUID)。如下图所示:
我们可以使用 MDC(Mapped Diagnostic Context)诊断上下文映射来报错日志码,MDC是@Slf4j提供的一个支持动态打印日志信息的工具。话不多说,直接上代码。
pom.xml 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!--lombok配置-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
</dependencies>
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--日志存储路径-->
<property name="log" value="logs" />
<property name="logName" value="business" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--输出格式化-->
<pattern>[%X{TRACE_ID}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按天生成日志文件 -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件名-->
<FileNamePattern>${log}/${logName}%d{yyyy-MM-dd}.log</FileNamePattern>
<!--保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[%X{TRACE_ID}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
</configuration>
application.yml
server:
port: 8080
logging:
config: classpath:logback-spring.xml
日志切面WebLogAspect
package com.business.aop;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import com.business.util.ThreadMdcUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Aspect
@Component
@Slf4j
public class WebLogAspect {
@Pointcut("execution(public * com.business.controller..*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String tid = UUID.randomUUID().toString().replace("-", "");
//可以考虑让客户端传入链路ID,但需保证一定的复杂度唯一性;如果没使用默认UUID自动生成
if (!StringUtils.isEmpty(request.getHeader(ThreadMdcUtil.getTraceId()))){
tid=request.getHeader("TRACE_ID");
}
MDC.put(ThreadMdcUtil.getTraceId(), tid);
// 记录下请求内容
log.info("---------------request----------------");
log.info("请求路径 : " + request.getRequestURL().toString()); //URL : request.getRequestURL().toString()
log.info("请求方式 : " + request.getMethod()); //HTTP_METHOD : request.getMethod()
log.info("访问者IP : " + request.getRemoteAddr()); //IP : request.getRemoteAddr()
log.info("CLASS_METHOD:" + joinPoint.getSignature().getDeclaringTypeName() + "-" + joinPoint.getSignature().getName());
log.info("ARGS:" + Arrays.toString(joinPoint.getArgs()));
Enumeration enu = request.getParameterNames();
while (enu.hasMoreElements()) {
String name = (String) enu.nextElement();
log.info("请求参数:" + name + " - 请求值:" + request.getParameter(name));
}
}
@AfterReturning("webLog()")
public void doAfterReturning() {
MDC.remove(ThreadMdcUtil.getTraceId());
}
}
子线程丢失trackId处理
子线程会丢失trackId,需要进行处理。思路: 将父线程的trackId传递下去给子线程即可。
ThreadPoolConfig线程池
定义线程池,交给spring管理。
package com.business.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.concurrent.Executor;
/**
* Author: lgq
* Date: 2023-4-12 11:07
* Description: 定义线程池,交给spring管理
*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {
/**
* 声明一个线程池
*
* @return 执行器
*/
@Bean("MyExecutor")
public Executor asyncExecutor() {
MyThreadPoolTaskExecutor executor = new MyThreadPoolTaskExecutor();
//核心线程数5:线程池创建时候初始化的线程数
executor.setCorePoolSize(5);
//最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(5);
//缓冲队列500:用来缓冲执行任务的队列
executor.setQueueCapacity(500);
//允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("asyncSimple");
executor.initialize();
return executor;
}
}
重写MyThreadPoolTaskExecutor
package com.business.config;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import com.business.util.ThreadMdcUtil;
/**
* Author: lgq
* Date: 2023-4-12 11:07
* Description: 重写ThreadPoolTaskExecutor
*/
public final class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
public MyThreadPoolTaskExecutor() {
super();
}
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}
ThreadMdcUtil工具类
package com.business.util;
import org.slf4j.MDC;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
/**
* Author: lgq
* Date: 2023-4-12 11:07
* Description: ThreadMDC工具类
*/
public final class ThreadMdcUtil {
private static final String TRACE_ID = "TRACE_ID";
public static String getTraceId() {
return TRACE_ID;
}
// 获取唯一性标识
public static String generateTraceId() {
return UUID.randomUUID().toString();
}
public static void setTraceIdIfAbsent() {
if (MDC.get(TRACE_ID) == null) {
MDC.put(TRACE_ID, generateTraceId());
}
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param callable
* @param context
* @param <T>
* @return
*/
public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param runnable
* @param context
* @return
*/
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
统一异常处理类
需要注意是,AfterThrowing 优先于 ExceptionHandler,因此在ExceptionHandler中移除traceId。
package com.business.common;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import com.business.exception.BusinessException;
import com.business.exception.ParamException;
import com.business.pojo.response.ResponseResult;
import com.business.util.ThreadMdcUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 系统未知错误
*
* @param ex
* @return
*/
@ExceptionHandler(value = Error.class)
@ResponseBody
public ResponseResult errorHandler(Error ex) {
String logCode = MDC.get(ThreadMdcUtil.getTraceId());
if (ObjectUtils.isEmpty(logCode)) {
ThreadMdcUtil.setTraceIdIfAbsent();
logCode = MDC.get(ThreadMdcUtil.getTraceId());
}
log.error("错误码:{}, 未知错误", logCode, ex);
MDC.remove(ThreadMdcUtil.getTraceId());
return ResponseResult.fail("系统异常,请联系管理员", logCode);
}
/**
* 系统未知异常
*
* @param ex
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseResult exceptionHandler(Exception ex) {
String logCode = MDC.get(ThreadMdcUtil.getTraceId());
if (ObjectUtils.isEmpty(logCode)) {
ThreadMdcUtil.setTraceIdIfAbsent();
logCode = MDC.get(ThreadMdcUtil.getTraceId());
}
log.error("错误码:{}, 未知异常", logCode, ex);
MDC.remove(ThreadMdcUtil.getTraceId());
return ResponseResult.fail("系统异常,请联系管理员", logCode);
}
/**
* 业务异常
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public ResponseResult handleBusinessException(BusinessException e) {
String logCode = ThreadMdcUtil.generateTraceId();
log.error("错误码:{}, 业务处理异常:{}", logCode, e.getMessage(), e);
MDC.remove(ThreadMdcUtil.getTraceId());
return ResponseResult.fail(logCode, CommonStatusEnum.BUSINESS_ERROR.getCode(),
CommonStatusEnum.BUSINESS_ERROR.getValue());
}
/**
* 业务参数异常
*/
@ExceptionHandler(value = ParamException.class)
@ResponseBody
public ResponseResult handleParamException(ParamException e) {
String logCode = ThreadMdcUtil.generateTraceId();
log.error("错误码:{}, 业务参数处理异常:{}", logCode, e.getMessage(), e);
MDC.remove(ThreadMdcUtil.getTraceId());
return ResponseResult.fail(logCode, CommonStatusEnum.PARAM_ERROR.getCode(),
CommonStatusEnum.BUSINESS_ERROR.getValue());
}
/**
* 参数校验(Valid)异常
*/
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
@ResponseBody
public ResponseResult handleValidException(MethodArgumentNotValidException e) {
String logCode = ThreadMdcUtil.generateTraceId();
if (ObjectUtils.isEmpty(logCode)) {
logCode = ThreadMdcUtil.generateTraceId();
}
log.error("错误码:{},数据校验异常:{},异常类型:{}", logCode, e.getMessage(), e.getClass(), e);
MDC.remove(ThreadMdcUtil.getTraceId());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = getErrorMap(bindingResult);
return ResponseResult.fail(logCode, CommonStatusEnum.PARAM_ERROR.getCode(), CommonStatusEnum.PARAM_ERROR.getValue(), errorMap);
}
/**
* 参数绑定异常
*/
@ExceptionHandler(value = {BindException.class})
@ResponseBody
public ResponseResult handleValidException(BindException e) {
String logCode = ThreadMdcUtil.generateTraceId();
log.error("错误码:{}, 数据校验异常:{},异常类型:{}", logCode, e.getMessage(), e.getClass(), e);
MDC.remove(ThreadMdcUtil.getTraceId());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = getErrorMap(bindingResult);
return ResponseResult.fail(logCode, CommonStatusEnum.PARAM_ERROR.getCode(), CommonStatusEnum.PARAM_ERROR.getValue(), errorMap);
}
/**
* 约束校验异常
*/
@ExceptionHandler(value = {ConstraintViolationException.class})
public ResponseResult handleValidException(ConstraintViolationException e) {
String logCode = ThreadMdcUtil.generateTraceId();
log.error("错误码:{}, 数据校验异常,{},异常类型:{}", logCode, e.getMessage(), e.getClass(), e);
MDC.remove(ThreadMdcUtil.getTraceId());
List<String> violations = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage).collect(Collectors.toList());
String error = violations.get(0);
return ResponseResult.fail(logCode, CommonStatusEnum.CONSTRAINT_ERROR.getCode(), CommonStatusEnum.CONSTRAINT_ERROR.getValue(), error);
}
/**
* 获取校验失败的结果
*/
private Map<String, String> getErrorMap(BindingResult result) {
return result.getFieldErrors().stream().collect(
Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (k1, k2) -> k1)
);
}
/**
* DataBinder 数据绑定访问器,集合参数校验时需要这个数据绑定
*/
@InitBinder
private void activateDirectFieldAccess(DataBinder dataBinder) {
dataBinder.initDirectFieldAccess();
}
}
其他异常处理类
package com.business.exception;
public class BusinessException extends RuntimeException {
public BusinessException() {
super();
}
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public BusinessException(Throwable cause) {
super(cause);
}
protected BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
public class ParamException extends RuntimeException {
public ParamException() {
super();
}
public ParamException(String message) {
super(message);
}
public ParamException(String message, Throwable cause) {
super(message, cause);
}
public ParamException(Throwable cause) {
super(cause);
}
protected ParamException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
其他类
package com.business.common;
import lombok.Getter;
public enum CommonStatusEnum {
/**
* 未知异常
*/
SERVER_UNKNOW_ERROR(1001,"服务器未知异常,请联系管理员!"),
/**
* 业务异常
*/
BUSINESS_ERROR(1002,"业务逻辑异常!"),
/**
* 网络请求异常
*/
NETWORK_ERROR(1003,"网络请求异常"),
/**
* 参数校验(Valid)异常
*/
PARAM_ERROR(1004,"参数异常"),
/**
* 约束校验异常
*/
CONSTRAINT_ERROR(1005,"约束异常"),
/**
* 成功
*/
SUCCESS(200,"success"),
/**
* 失败
*/
FAIL(500,"fail")
;
@Getter
private int code;
@Getter
private String value;
CommonStatusEnum(int code, String value) {
this.code = code;
this.value = value;
}
}
package com.bussiness.pojo.response;
import com.bussiness.common.CommonStatusEnum;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class ResponseResult<T> {
private int code;
private String message;
private T data;
private String logCode;
/**
* 成功响应的方法
*
* @param <T>
* @return
*/
public static <T> ResponseResult<T> success() {
return new ResponseResult().setCode(CommonStatusEnum.SUCCESS.getCode()).setMessage(CommonStatusEnum.SUCCESS.getValue());
}
/**
* 成功响应的方法
*
* @param data
* @param <T>
* @return
*/
public static <T> ResponseResult<T> success(T data) {
return new ResponseResult().setCode(CommonStatusEnum.SUCCESS.getCode()).setMessage(CommonStatusEnum.SUCCESS.getValue()).setData(data);
}
/**
* 失败:统一的失败
*
* @param data
* @param <T>
* @return
*/
public static <T> ResponseResult<T> fail(T data) {
return new ResponseResult().setCode(CommonStatusEnum.FAIL.getCode()).setData(data);
}
/**
* 失败:统一的失败
*
* @param data
* @param <T>
* @return
*/
public static <T> ResponseResult<T> fail(T data, String logCode) {
return new ResponseResult().setCode(CommonStatusEnum.FAIL.getCode())
.setLogCode(logCode).setData(data);
}
/**
* 失败:统一的失败
*
* @param message
* @return
*/
public static ResponseResult fail(String message) {
return new ResponseResult().setCode(CommonStatusEnum.FAIL.getCode()).setMessage(message);
}
/**
* 失败:统一的失败
*
* @param message
* @return
*/
public static ResponseResult fail(String message, String logCode) {
return new ResponseResult().setCode(CommonStatusEnum.FAIL.getCode())
.setLogCode(logCode).setMessage(message);
}
/**
* 失败:自定义失败 错误码和提示信息
*
* @param code
* @param message
* @return
*/
public static ResponseResult fail(int code, String message) {
return new ResponseResult().setCode(code).setMessage(message);
}
/**
* 失败:自定义失败 错误码和提示信息
*
* @param code
* @param message
* @return
*/
public static ResponseResult fail(String logCode, int code, String message) {
return new ResponseResult().setCode(code).setLogCode(logCode).setMessage(message);
}
/**
* 失败:自定义失败 错误码、提示信息、具体错误
*
* @param code
* @param message
* @param data
* @return
*/
public static <T> ResponseResult<T> fail(int code, String message, T data) {
return new ResponseResult().setCode(code).setMessage(message).setData(data);
}
/**
* 失败:自定义失败 错误码、提示信息、具体错误
*
* @param code
* @param message
* @param data
* @return
*/
public static <T> ResponseResult<T> fail(String logCode, int code, String message, T data) {
return new ResponseResult().setCode(code).setMessage(message).setData(data).setLogCode(logCode);
}
}
package com.business.pojo.request;
import javax.validation.constraints.Min;
import lombok.Getter;
import lombok.Setter;
public class PageQuery {
@Getter
@Setter
@Min(value = 1, message = "当前页码不合法")
private int pageNo = 1;
@Getter
@Setter
@Min(value = 1, message = "每页展示数量不合法")
private int pageSize = 10;
@Setter
private int offset;
public int getOffset() {
return (pageNo - 1) * pageSize;
}
}
package com.business.pojo.request;
import lombok.Data;
import org.hibernate.validator.constraints.NotBlank;
import java.util.List;
@Data
public class DemoReq {
/**
* 领域编码
*/
@NotBlank(message = "领域编码不能为空")
private String domainKey;
/**
* 模型编码
*/
@NotBlank(message = "模型编码不能为空")
private String modelKey;
/**
* 时间
*/
private List<String> startTime;
/**
* 开始时间
*/
private String beginTime;
/**
* 结束时间
*/
private String endTime;
}
参考文章:
拦截机制中Aspect、ControllerAdvice、Interceptor、Fliter之间的区别详解 - 简书
Springboot 同一次调用日志怎么用ID串起来,方便最终查找_日志id开线程后还能用吗_小目标青年的博客-CSDN博客
hibernate-validator校验参数(统一异常处理)_无法访问org.hibernate.validator.constraints.range_鱼找水需要时间的博客-CSDN博客