> 文章列表 > springboot 统一异常处理 + 日志记录

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();}@Overridepublic void execute(Runnable task) {super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}@Overridepublic <T> Future<T> submit(Callable<T> task) {return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}@Overridepublic 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)@ResponseBodypublic 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)@ResponseBodypublic 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)@ResponseBodypublic 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)@ResponseBodypublic 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})@ResponseBodypublic 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})@ResponseBodypublic 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 数据绑定访问器,集合参数校验时需要这个数据绑定*/@InitBinderprivate 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");@Getterprivate int code;@Getterprivate 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;@Setterprivate 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博客