您现在的位置是:首页 >技术教程 >springboot 统一异常处理 + 日志记录网站首页技术教程

springboot 统一异常处理 + 日志记录

lgq2016 2023-05-15 04:00:03
简介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博客

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。