springboot aop + logback + 统一异常处理 打印日志

时间:2021-05-27 06:11:35

1.src/resources路径下新建logback.xml

  • 控制台彩色日志打印
  • info日志和异常日志分不同文件存储
  • 每天自动生成日志
  • 结合myibatis方便日志打印(debug模式)
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">


    <!--定义日志文件的存储地址 可以在LogBack 的配置中使用相对路径-->
    <property name="LOG_HOME" value="logs" />

    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/category-server-log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%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>-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>DENY</onMatch>
            <onMismatch>NEUTRAL</onMismatch>
        </filter>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>NEUTRAL</onMismatch>
        </filter>

    </appender>

    <!-- 出错日志 appender  -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <!-- log.dir 在maven profile里配置 -->
            <FileNamePattern>${LOG_HOME}/category-server-error-log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <!--myibatis log configure-->
    <logger name="com.apache.ibatis" level="TRACE"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>
    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
        <appender-ref ref="ERROR_FILE" />
    </root>
</configuration>

2.定义一个log实体 方便维护和后续的扩展

package com.wdcloud.categoryserver.log;

import com.wdcloud.categoryserver.common.entity.User;

/**
 * @describe: 日志打印实体
 * @author: zhuchunwang
 * @date: 2018/5/29 17:40
 * @version: 1.0
 */
public class LogEntity {
    private long id;
    /**
     * 请求地址
     */
    private String url;
    /**
     * 请求IP
     */
    private String ip;
    /**
     * 请求方式
     */
    private String httpMethod;
    /**
     * 请求类,方法
     */
    private String classMethod;
    /**
     * 方法参数
     */
    private String args;
    /**
     * 请求参数
     */
    private String reqParams;
    /**
     * 响应参数
     */
    private String respParams;
    /**
     * 响应时间
     */
    private long spendTime;
    /**
     * 日志类型(web、service)
     */
    private String logType;
    /**
     * 用户信息
     */
    private User user;


    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public String getHttpMethod() {
        return httpMethod;
    }

    public void setHttpMethod(String httpMethod) {
        this.httpMethod = httpMethod;
    }

    public String getClassMethod() {
        return classMethod;
    }

    public void setClassMethod(String classMethod) {
        this.classMethod = classMethod;
    }

    public String getArgs() {
        return args;
    }

    public void setArgs(String args) {
        this.args = args;
    }

    public String getReqParams() {
        return reqParams;
    }

    public void setReqParams(String reqParams) {
        this.reqParams = reqParams;
    }

    public String getRespParams() {
        return respParams;
    }

    public void setRespParams(String respParams) {
        this.respParams = respParams;
    }

    public long getSpendTime() {
        return spendTime;
    }

    public void setSpendTime(long spendTime) {
        this.spendTime = spendTime;
    }

    public String getLogType() {
        return logType;
    }

    public void setLogType(String logType) {
        this.logType = logType;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}

3.定义一个controller 的切面

package com.wdcloud.categoryserver.log;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wdcloud.categoryserver.common.constant.AppConstants;
import com.wdcloud.categoryserver.utils.AppUtil;
import com.wdcloud.categoryserver.utils.JsonUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @describe: 实现controller的日志切面
 * @author: zhuchunwang
 * @date: 2018/5/29 17:40
 * @version: 1.0
 */
@Aspect
@Component
@Order(1)
public class ControllerLogAspect {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    ThreadLocal<Long> startTime = new ThreadLocal<>();
    ThreadLocal<LogEntity> webLogThreadLocal = new ThreadLocal<>();
    /**
     * 定义一个切入点.
     * 解释下:
     * <p>
     * ~ 第一个 * 代表任意修饰符及任意返回值.
     * ~ 第二个 * 任意包名
     * ~ 第三个 * 代表任意方法.
     * ~ 第四个 * 定义在web包或者子包
     * ~ 第五个 * 任意方法
     * ~ .. 匹配任意数量的参数.
     */
    @Pointcut("execution(* com.wdcloud.categoryserver.business.*.controller.*.*(..))")
    public void serviceAspect() {
    }
    @Before("serviceAspect()")
    public void doBefore(JoinPoint joinPoint) {
        // 接收到请求,记录请求内容
        startTime.set(System.currentTimeMillis());
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        webLogThreadLocal.set(new LogEntity());
        webLogThreadLocal.get().setHttpMethod(request.getMethod());
        webLogThreadLocal.get().setClassMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        webLogThreadLocal.get().setUrl(request.getRequestURL().toString());
        webLogThreadLocal.get().setIp(request.getRemoteAddr());
        webLogThreadLocal.get().setArgs(AppUtil.getArgs(joinPoint));
        webLogThreadLocal.get().setLogType(AppConstants.LOG_TYPE_HTTP);
        webLogThreadLocal.get().setReqParams(JsonUtil.objectToJson(request.getParameterMap()));
        webLogThreadLocal.get().setUser(AppUtil.getUser(request));
    }

    /**
     * 异常通知 用于拦截service层记录异常日志
     *
     * @param
     * @param
     */
    @AfterReturning(returning = "result", pointcut = "serviceAspect()")
    public void doAfterReturning(Object result) {
        // 处理完请求,返回内容
        ObjectMapper mapper = new ObjectMapper();
        try {
            webLogThreadLocal.get().setRespParams(mapper.writeValueAsString(result));
        } catch (JsonProcessingException e) {
            logger.error(AppUtil.getExceptionDetail(e));
        }
        webLogThreadLocal.get().setSpendTime(System.currentTimeMillis() - startTime.get());
        try {
            logger.info(">>>"+mapper.writeValueAsString(webLogThreadLocal.get()));
        } catch (JsonProcessingException e) {
            logger.error(AppUtil.getExceptionDetail(e));
        }
    }
}

4.定义一个供dubbo调用的service切面

package com.wdcloud.categoryserver.log;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wdcloud.categoryserver.common.constant.AppConstants;
import com.wdcloud.categoryserver.utils.AppUtil;
import com.wdcloud.categoryserver.utils.JsonUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @describe: 实现controller的日志切面
 * @author: zhuchunwang
 * @date: 2018/5/29 17:40
 * @version: 1.0
 */
@Aspect
@Component
@Order(1)
public class ServiceLogAspect {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    ThreadLocal<Long> startTime = new ThreadLocal<>();
    ThreadLocal<LogEntity> webLogThreadLocal = new ThreadLocal<>();
    /**
     * 定义一个切入点.
     * 解释下:
     * <p>
     * ~ 第一个 * 代表任意修饰符及任意返回值.
     * ~ 第二个 * 任意包名
     * ~ 第三个 * 代表任意方法.
     * ~ 第四个 * 定义在web包或者子包
     * ~ 第五个 * 任意方法
     * ~ .. 匹配任意数量的参数.
     */
    @Pointcut("execution(* com.wdcloud.categoryserver.front.*.service.*.*(..))")
    public void serviceAspect() {
    }
    @Before("serviceAspect()")
    public void doBefore(JoinPoint joinPoint) {
        // 接收到请求,记录请求内容
        startTime.set(System.currentTimeMillis());
        webLogThreadLocal.set(new LogEntity());
        webLogThreadLocal.get().setClassMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        webLogThreadLocal.get().setArgs(AppUtil.getArgs(joinPoint));
        webLogThreadLocal.get().setLogType(AppConstants.LOG_TYPE_DUBBO);
    }

    /**
     * 异常通知 用于拦截service层记录异常日志
     *
     * @param
     * @param
     */
    @AfterReturning(returning = "result", pointcut = "serviceAspect()")
    public void doAfterReturning(Object result) {
        // 处理完请求,返回内容
        ObjectMapper mapper = new ObjectMapper();
        try {
            webLogThreadLocal.get().setRespParams(mapper.writeValueAsString(result));
        } catch (JsonProcessingException e) {
            logger.error(AppUtil.getExceptionDetail(e));
        }
        webLogThreadLocal.get().setSpendTime(System.currentTimeMillis() - startTime.get());
        try {
            logger.info(">>>"+mapper.writeValueAsString(webLogThreadLocal.get()));
        } catch (JsonProcessingException e) {
            logger.error(AppUtil.getExceptionDetail(e));
        }
    }
}

5.统一异常处理

package com.wdcloud.categoryserver.common.exception;

import com.wdcloud.categoryserver.common.constant.AppConstants;
import com.wdcloud.categoryserver.common.constant.CodeConstants;
import com.wdcloud.categoryserver.common.entity.BaseView;
import com.wdcloud.categoryserver.log.LogEntity;
import com.wdcloud.categoryserver.utils.AppUtil;
import com.wdcloud.categoryserver.utils.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;

import javax.servlet.http.HttpServletRequest;

/**
 * @describe: 全局异常处理
 * @author: zhuchunwang
 * @date: 2018/5/29 17:40
 * @version: 1.0
 */
@ControllerAdvice(annotations = {RestController.class})
public class GlobalExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    /**
     * 默认未知异常
     * @param request
     * @param e
     * @return
     * @throws Exception
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public BaseView defaultErrorHandler(HttpServletRequest request, Exception e) throws Exception {
        BaseView baseView = new BaseView(CodeConstants.SYSTEM_EXCEPTION,CodeConstants.SYSTEM_EXCEPTION_MSG);
        printLog(request,e,baseView);
        return baseView;
    }

    /**
     * 参数异常
     * @param request
     * @param e
     * @return
     * @throws Exception
     */
    @ExceptionHandler(value = {HttpMessageNotReadableException.class, MissingServletRequestPartException.class ,MissingServletRequestParameterException.class, MultipartException.class})
    @ResponseBody
    public BaseView httpMessageNotReadableExceptionErrorHandler(HttpServletRequest request, Exception e) throws Exception {
        BaseView baseView = new BaseView(CodeConstants.PARAMETER_ERROR,CodeConstants.PARAMETER_ERROR_MSG);
        printLog(request,e,baseView);
        return baseView;
    }
    /**
     * contentType异常
     * @param request
     * @param e
     * @return
     * @throws Exception
     */
    @ExceptionHandler(value = {HttpMediaTypeNotSupportedException.class})
    @ResponseBody
    public BaseView httpMediaTypeNotSupportedExceptionHandler(HttpServletRequest request, Exception e) throws Exception {
        BaseView baseView =  new BaseView(CodeConstants.CONTENTTYPE_ERROR,CodeConstants.CONTENTTYPE_ERROR_MSG);
        printLog(request,e,baseView);
        return baseView;
    }

    /**
     * 异常信息打印日志
     * @param request
     * @param e
     * @param baseView
     */
    private void printLog(HttpServletRequest request,Exception e,BaseView baseView){
        logger.error(AppUtil.getExceptionDetail(e));
        LogEntity logEntity = new LogEntity();
        logEntity.setHttpMethod(request.getMethod());
        logEntity.setUrl(request.getRequestURL().toString());
        logEntity.setIp(request.getRemoteAddr());
        logEntity.setArgs(AppUtil.getRequestBody(request));
        logEntity.setLogType(AppConstants.LOG_TYPE_HTTP);
        logEntity.setReqParams(JsonUtil.objectToJson(request.getParameterMap()));
        logEntity.setRespParams(JsonUtil.objectToJson(baseView));
        logger.error(">>>"+JsonUtil.objectToJson(logEntity));
    }

}