SpringBoot2.1版本的个人应用开发框架 - 日志自定义和全局异常处理

3,059 阅读9分钟

本篇作为SpringBoot2.1版本的个人开发框架 子章节,请先阅读SpringBoot2.1版本的个人开发框架再次阅读本篇文章

后端项目地址:SpringBoot2.1版本的个人应用开发框架

前端项目地址:ywh-vue-admin

日志自定义

在之前的章节我们测试的时候,发现控台台输出的日志是默认的,并且有很多的日志没有打印,并且不能自定义设置我们的想要输出的信息,对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。

对于日志的参考资料网上一搜一大堆,更详细的介绍可以轻松的获得,这里贴出几个参考资料:

Java有很多常用的日志框架,如Log4j、Log4j 2、Commons Logging、Slf4j、Logback等。

Commons Logging和Slf4j是日志门面,提供一个统一的高层接口,为各种loging API提供一个简单统一的接口。log4j和Logback则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用者只需要关注接口而无需关注具体的实现,做到解耦。

比较常用的组合使用方式是Slf4j与Logback组合使用,Commons Logging与Log4j组合使用,基于下面的一些优点,选用Slf4j+Logback的日志框架:

更快的执行速度,Logback重写了内部的实现,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了

自动清除旧的日志归档文件,通过设置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 属性,你就可以控制日志归档文件的最大数量

Logback拥有远比log4j更丰富的过滤能力,可以不用降低日志级别而记录低级别中的日志。

Logback必须配合Slf4j使用。由于Logback和Slf4j是同一个作者,其兼容性不言而喻。

默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台。

配置日志

由上可知我们springboot项目默认使用的就是Logback,我们可以通过设置yml文件的方式来设置日志的格式,也可以通过logback.xml的方式来设置日志管理。

yml方式: 这种方式相对于xml的方式比较简单,因为你不配置,springboot也会有默认的设置,在application-dev.yml开发环境添加以下配置即可生效,path的路径在开发环境时可以是windows下的路径,当你部署到liunx服务器时需要使用application-prod生产环境的配置文件,文件中配置的路径为liunx的路径即可。

logging:
  file:
    #存放文件的最大天数
    max-history: 15
    #存放日志最大size
    max-size: 100MB
  #存放日志文件位置
  path: E:\logs
  pattern:
    #输出到控制台的格式
    console: "YWH - %d{yyyy-MM-dd HH:mm:ss} -%-4r  [%t]  %-5level %logger{36} - %msg%n"
  #日志级别映射,可以指定包下的日志级别 也可指定root为info级别
  level:
    root: info
    com.ywh.core: debug
*************************************************
<!--    %d{HH: mm:ss.SSS}——日志输出时间   -->
<!--    %thread  [%t] ——输出日志的进程名字,这在Web应用以及异步任务处理中很有用  -->
<!--    %-4r —— "-"代表了左对齐  将输出从程序启动到创建日志记录的时间 进行左对齐 且最小宽度为4 -->
<!--    %-5level——日志级别,并且使用5个字符靠左对齐 -->
<!--    %logger{36}——日志输出者的名字   -->
<!--    %msg——日志消息  -->
<!--    %n——平台的换行符  -->

<!--    更多的详情可参考 : https://aub.iteye.com/blog/1103685  此博客最下方有解释 

以下图片来自于:aub.iteye.com/blog/110368…

xml方式:这种方式需要配置多个标签,相对与yml方式比较麻烦一点,在resources文件下创建logback-spring.xml文件,如果不想把xml文件直接放在resources下的话,需要在yml文件中配置logging.config= 指定位置

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。
                 当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration  scan="true" scanPeriod="60 seconds">
    <contextName>Y-W-H</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
    <property name="log.path" value="E:/logs/" />
 
    <!--0. 日志格式和颜色渲染 -->
    <!-- 彩色日志依赖的渲染类 -->
    <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} %contextName ) [%thread] %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}}"/>
 
 
    <!--1. 输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>
 
    。。。。。。省略代码,具体代码可前往github查看
 
</configuration>

通过以上两种方式的任意一种配置好以后启动项目以后,就会发现我们已经使用了我们自定义的输出格式来输出日志了,在我们指定下的路径下出现了日志文件。

自定义异常以及全局异常类

当日志级别设置到INFO级别后,只会输出INFO以上的日志,如INFO、WARN、ERROR,这没毛病,问题是,程序中抛出的异常堆栈(运行时异常)都没有打印了,不利于排查问题。

而且,在某些情况下,我们在Service中想直接把异常往Controller抛出不做处理,但我们不能直接把异常信息输出到客户端,这是非常不友好的,而且我们想要精准的定位错误的所在,这就要我们自己来定义异常的输出了,并且把错误的异常以我们之前封装的Result的统一格式返回给前端,所以我们需要自定义异常以及定义全局异常类,我们先定义自定义异常类然后再定义全局异常类。

根据菜鸟教程中的异常信息分类,异常分为三种情况

检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。

运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。

错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

而我们所要做的就是继承运行时异常,对此类异常进行自定义处理,在common下exception包中创建MyException类继承RuntimeException。

package com.ywh.common.exception;
 
/**
 * CreateTime: 2018-11-21 19:07
 * ClassName: MyXiyiException
 * Package: com.ywh.common.exception
 * Describe:
 * 自定义异常,可以throws的时候用自己的异常类
 *
 * @author YWH
 */
public class MyException extends RuntimeException {
 
    public MyException(String msg) {
        super(msg);
    }
 
    public MyException(String message, Throwable throwable) {
        super(message, throwable);
    }
 
    public MyException(Throwable throwable) {
        super(throwable);
    }
}

在common下的utils包中创建MyExceptionUtil工具类快速创建异常类

package com.ywh.common.utils;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.ywh.common.exception.MyException;

/**
 * CreateTime: 2018-12-18 22:32
 * ClassName: MyExceptionUtil
 * Package: com.ywh.common.utils
 * Describe:
 * 异常工具类
 *
 * @author YWH
 */
public class MyExceptionUtil {

    public MyExceptionUtil() {
    }

    public static MyException mxe(String msg, Throwable t, Object... params){
        return new MyException(StringUtils.format(msg, params),t);
    }

    public static MyException mxe(String msg, Object... params){
        return new MyException(StringUtils.format(msg, params));
    }

    public static MyException mxe(Throwable t){
        return new MyException(t);
    }

}

创建完自定义异常以后我们要对自定义异常进行捕获然后处理,这就需要我们定义全局异常类来进行捕获后进行处理了,在common下exception中添加GlobalExceptionHandler类。

package com.ywh.common.exception;
 
/**
 * @Author: YWH
 * @Description: 全局异常处理类,拦截controller  RestControllerAdvice此注解为ResponseBody和ControllerAdvice混合注解
 * @Date: Create in 17:16 2018/11/17
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     *
     * 全局异常类中定义的异常都可以被拦截,只是触发条件不一样,如IO异常这种必须抛出异常到
     * controller中才可以被拦截,或者在类中用try..catch自己处理
     * 绝大部分不需要向上抛出异常即可被拦截,返回前端json数据,如数组下标越界,404 500 400等错误
     * 如果自己想要写,按着以下格式增加异常即可
     *HttpMessageNotReadableException
     */
 
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     *   启动应用后,被 @ExceptionHandler@InitBinder@ModelAttribute 注解的方法,
     *   都会作用在 被 @RequestMapping 注解的方法上。
     * @param binder
     */
    @InitBinder
    public void initWebBinder(WebDataBinder binder){
 
    }
 
    /**
     * 系统错误,未知的错误   已测试
     * @param ex 异常信息
     * @return 返回前端异常信息
     */
    @ExceptionHandler({Exception.class})
    public Result exception(Exception ex){
        log.error("错误详情:" + ex.getMessage(),ex);
        return Result.errorJson(BaseEnum.SYSTEM_ERROR.getMsg(),BaseEnum.SYSTEM_ERROR.getIndex());
    }
 
  。。。。。。省略代码,具体代码请前往github查看
 
    /**
     * 自定义异常信息拦截
     * @param ex 异常信息
     * @return 返回前端异常信息
     */
    @ExceptionHandler(MyException.class)
    public Result myCustomizeException(MyException ex){
        log.warn("错误详情:" + ex);
        return Result.errorJson(BaseEnum.CUSTOMIZE_EXCEPTION.getMsg(),BaseEnum.CUSTOMIZE_EXCEPTION.getIndex());
    }
 
}

在GlobalExceptionHandler中我们对很多异常进行了拦截后自定义处理,并把我们上边自定义的运行时异常进行拦截,我在类中的方法上都写了注释,并根据网上的资料应该很好理解,我对大部分的异常都做了测试,都是可以进行拦截成功的。

测试示例

我们用postman通过post方式请求一个get的方法,可以看到返回了我们自定义的json格式,并且告诉我们这是因为接口类型所导致的错误,这样我们很快就能定位到错误进行解决。

异常信息拦截

以上错误都是系统替我们捕获并且通过全局异常类进行了拦截之后返回自定义的json格式,而我们的自定义异常如何使用呢,自定义异常需要我们手动捕获异常,并且抛出异常,这样我们的全局异常类才能拦截到。

我们在ExampleServiceImpl中定义一个方法,并在Controller层中调用此方法,用postman调用此接口

    /**
     * 测试自定义异常
     * @return 返回字符串
     */
    @Override
    public String myException() {
        int i = 0;
        int a = 10;
        if( i > a){
            System.out.println("测试!!!");
        }else{
            throw MyExceptionUtil.mxe("出错了,比他小啊!!");
        }
        return "没有进行拦截,失败了";
    }
    @Autowired
    private ExampleService exampleService;
    
    @GetMapping("myExceptionTest")
    public Result myExceptionTest(){
        return Result.successJson(exampleService.myException());
    }

自定义一场拦截

控制台信息

可以看到我们的自定义异常被拦截到并且在控制台中打印了我们想要的信息。