从零搭建 Spring Boot 后端项目(十一)

简介

日志能记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题。尤其是项目线上问题,不允许远程调试的情况下,只能依赖日志定位问题,如果日志写的好,那就能快速找到问题所在。反之,日志没写好,反而会影响程序的运行性能和稳定性。日志的用途大致可以分为:

  • 问题追踪:辅助排查和定位线上问题,优化程序运行性能
  • 状态监控:通过日志分析,可以监控系统的运行状态
  • 安全审计:审计主要体现在安全上,可以发现非授权的操作

框架选择

SLF4J——Simple Logging Facade For Java,它是一个针对于各类Java日志框架的统一Facade抽象。Java日志框架众多——常用的有java.util.logging, log4j, logback,commons-logging, Spring框架使用的是Jakarta Commons Logging API (JCL)。Logback是log4j框架的作者开发的新一代日志框架,它效率更高、能够适应诸多的运行环境,同时天然支持SLF4J。所以尽管框架组合选择的方式很多,但选择SLF4J + Logback的组合,性能高,可扩展性强,很多开源项目都是此选项。SpringBoot默认情况下也是。所以我们使用默认的SLF4J + Logback组合就好

何时记录日志

  • 系统初始化: 系统或者服务的启动参数。核心模块或者组件初始化过程中往往依赖一些关键配置,根据参数不同会提供不一样的服务。务必在这里记录INFO日志,打印出参数以及启动完成态服务表述
  • 系统核心操作: 核心操作是正常系统正常运行的重要指标,建议关键点用INFO级别日志,其它考虑使用DEBUG级别,因为如果日志打印过多也会影响性能
  • 与业务流程不符时: 此时即是我们的业务场景不可能发生的情况,但是发生了,这时可以记录成日志,主要还是看开发人员的容忍程度了
  • 抛出异常时: 质量非常高的报错,根据业务的情况使用warn或者error级别,此项目需要在统一异常中添加日志记录
  • 第三方服务调用: 微服务架构体系中有一个重要的点就是第三方永远不可信,对于第三方服务远程调用建议打印请求和响应的参数,方便在和各个终端定位问题,不会因为第三方服务日志的缺失变得手足无措

日志格式

  • 日志时间:精确到毫秒,格式:yyyy-MM-dd HH:mm:ss.SSS
  • 日志级别:TRACE < DEBUG < INFO < WARN < ERROR
  • 线程ID:即PID
  • 分隔符:— 标识实际日志的开始
  • 线程名:方括号括起来(可能会截断控制台输出)
  • Logger名:通常使用源代码的类名,例如:c.e.b.BackendTemplateApplication,c.e.b是上三层的头字母
  • 日志内容
  • 异常堆栈(不一定有)

注:上面的格式其实就是Spring Boot日志默认的格式

例如:

//SpringBoot日志默认格式
2020-07-22 16:53:51.532 INFO 14912 --- [ main] c.e.b.BackendTemplateApplication: Starting BackendTemplateApplication...

日志级别

平时主要是用以下几种,分别讲一下怎么选择使用

  • TRACE追踪程序推进,如果要写的话,trace会特别多,所以一般都不输出
  • DEBUG输出调试性质内容,主要用于开发、测试阶段,这种级别的日志也特别多,因为有很多调试信息,比如参数信息,调试细节信息,返回值信息等等,主要方便开发时分析错误使用,一般生产环境下不输出,一是影响性能,二是信息太多容易掩盖重要信息
  • INFO主要记录系统关键信息,旨在保留系统正常工作期间关键运行指标,例如:初始化系统配置、业务状态变化信息,建议在生产环境中输出,并且INFO输出的信息可看作是软件产品的一部分,所以需要谨慎对待输出,不可太多,也不可无用
  • WARN主要输出警告性质的内容,WARN代表可恢复的异常,此次失败不影响下次业务的执行
  • ERROR主要针对于一些不可预知的信息,诸如:错误、异常等,输出时应该尽可能的详细,比如,入参,错误提示,异常对象等,这样才方便分析错误

其它规范

  • 日志输出需使用点位符输出,而不是用字符拼接

    //如果日志级别是 warn,上述日志不会打印,但是会执行字符串拼接操作,浪费了系统资源
    logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
    

    改成

    logger.debug("Processing trade with id: [{}] and symbol : [{}] ", id, symbol);
    
  • 不要记录太多无用信息,不利于定位错误,且容易占太多磁盘资源,甚至撑爆磁盘

  • 尽量不要在日志中调用对象的获取方法,除非肯定该对象一定不为空,否则容易出NPE异常

  • 本地开发环境,可使用日志输出到控制台上,测试、生产可再写入文件中

  • 生产还可以考虑异步写入文件,这样不会因为日志的写入,而浪费性能,但AOP+异步只能是方法或类级别,方法内日志记录不到,看个人选择

  • 一个对象中通常只使用一个Logger对象,Logger应该是static final的

    private static final Logger log = LoggerFactory.getLogger(Main.class);
    
  • 尽量不要把日志输出写在循环内,容易影响运行时间

步骤

  • application-dev.properties配置文件下,增加以下配置,其实正常情况不用这么多配置,这里写了很多都是默认值,给出的原因是,方便以后改配置功能。这里需要注意的是,logs//backend_template.log 这里配置的日志目录,是一个相对路径,如果在IDE中运行,则和backend_template/src平级,打包成jar包后运行,则和jar包平级,可自行配置,配置后程序会自行创建该目录及文件

    # 日志
    # 最小能打印出的日志级别,root级别即所有日志
    logging.level.root=INFO
    # 日志文件保存目录
    logging.file.name=logs//backend_template.log
    # 启动时是否清除日志
    logging.file.clean-history-on-start=false
    # 日志的最大尺寸,超过后分割
    logging.file.max-size=10MB
    # 日志总大小,0为不设限
    logging.file.total-size-cap=0
    # 限制日志保留天数,到期自动删除
    logging.file.max-history=15
    # 控制台日志输出格式
    logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:-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}
    # 文件日志输出格式
    logging.pattern.file=%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}
    # appender的log level输出样式
    logging.pattern.level=%5p
    # 滚动文件名称
    logging.pattern.rolling-file-name=${LOG_FILE} + ".%d{yyyy-MM-dd}.%i.gz"
    # 时间格式
    logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss:SSS
    
  • 接下来就可以在类中写日志了,值得注意的是,在导入Logger类的包时,不要导错了,要导slf4j包下的Logger,而不是其它的日志包,要不然到时出错了还不方便改错,示例如下

    // 导入slf4j包
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    // 类中使用Log
    private static final Logger log = LoggerFactory.getLogger(Main.class);
    log.info("The request path is: [{}]",path);
    
  • 那们我们现在开始来分别在系统初始化系统核心操作与业务流程不符抛出异常时的情况下,来打印日志,这里是因为我们没有第三方服务调用,所示没办法演示,但不是说不重要,反而第三服务相当重要,一定要记录日志,因为第三方服务永远都不可信,记录下来好判断错误

  • 首先是系统初始化时的日志,在本模板中,用到了Redis和Swagger2,在这些配置参数里,初始化时最重要的莫过于Redis 的hostname和端口是否启用Swagger当然你也可以打印其它,你觉得重要的信息,方法一致,将com.example.backend_template.config.RedisConfigurer.java改为如下即可

    package com.example.backend_template.config;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.cache.annotation.CachingConfigurerSupport;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import redis.clients.jedis.JedisPoolConfig;
    
    /**
     * @ClassName RedisConfigurer Redis的配置类
     * @Description
     * @Author L
     * @Date Create by 2020/6/26
     */
    @Configuration
    @EnableCaching      //开启缓存
    public class RedisConfigurer extends CachingConfigurerSupport {
    
        private static final Logger log = LoggerFactory.getLogger(RedisConfigurer.class);
    
        @Bean
        @ConfigurationProperties(prefix = "spring.redis")
        public JedisPoolConfig getRedisConfig() {
            JedisPoolConfig config = new JedisPoolConfig();
            return config;
        }
    
        @Bean
        @ConfigurationProperties(prefix = "spring.redis")
        public JedisConnectionFactory getConnectionFactory() {
            JedisConnectionFactory factory = new JedisConnectionFactory();
            JedisPoolConfig config = getRedisConfig();
            factory.setPoolConfig(config);
            log.info("The hostname of the redis connection is:{}, and the port is: {}",factory.getHostName(),factory.getPort());
            return factory;
        }
    
        public RedisTemplate getRedisTemplate() {
            RedisTemplate template = new StringRedisTemplate(getConnectionFactory());
            return template;
        }
    }
    
  • com.example.backend_template.config.Swagger2Config.java类改为如下代码

    package com.example.backend_template.config;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    /**
     * @ClassName Swagger2Config swagger的配置内容即是创建一个Docket实例
     * @Description 
     * @Author L
     * @Date Create by 2020/6/30
     */
    @Configuration
    @EnableSwagger2 //启用swagger2
    public class Swagger2Config {
    
        //是否开启 swagger-ui 功能,默认为false
        @Value("${swagger.enable:false}")
        private Boolean enable;
    
        private static final Logger log = LoggerFactory.getLogger(Swagger2Config.class);
    
        @Bean
        public Docket createRestApi() {
            log.info("Whether to open the Swagger service: {}",enable);
            return new Docket(DocumentationType.SWAGGER_2)
                    .enable(enable)
                    .pathMapping("/")
                    .apiInfo(apiInfo())
                    .select()
                    //需要Swagger描述的接口包路径,如果不想某接口暴露,可在接口上加@ApiIgnore注解
                    .apis(RequestHandlerSelectors.basePackage("com.example.backend_template.controller"))
                    .paths(PathSelectors.any())
                    .build();
        }
    
        //配置在线文档的基本信息
        private ApiInfo apiInfo() {
            return new ApiInfoBuilder()
                    .title("BackendTemplate项目")
                    .description("使基于SpringBoot的后端开发变得简单")
                    .version("1.0")
                    .build();
        }
    }
    
  • 完成这两个操作后,再次启动我们的项目,不论控制台和日志文件里,就会有如下两行系统初始化数据了,这样就可以很方便从日志信息中,知道系统关键配置信息

    2020-07-24 15:34:40,998  INFO 9864 --- [           main] c.e.b.config.RedisConfigurer             : The hostname of the redis connection is:localhost, and the port is: 6379
    2020-07-24 15:34:41,538  INFO 9864 --- [           main] c.e.b.config.Swagger2Config              : Whether to open the Swagger service: true
    
  • 接下来记录系统核心操作,其中controller层的操作即可认为是核心操作,可记录重要入参与返回值,根据业务的不同,可以选择是INFO级别还是DEBUG级别
    修改com.example.backend_template.config.RedisController.java类为如下代码

    package com.example.backend_template.controller;
    
    import com.example.backend_template.config.Swagger2Config;
    import com.example.backend_template.service.RedisService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiImplicitParam;
    import io.swagger.annotations.ApiOperation;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.*;
    
    import javax.annotation.Resource;
    
    /**
     * @ClassName RedisController
     * @Description 
     * @Author L
     * @Date Create by 2020/6/26
     */
    @Api(tags = "redis简单测试接口")
    @RestController
    @RequestMapping("redis")
    public class RedisController {
    
        @Resource
        private RedisService redisService;
    
        private static final Logger log = LoggerFactory.getLogger(RedisController.class);
    
        @ApiOperation("向redis中存储`name`值")
        @ApiImplicitParam(name = "name",value = "名称值",defaultValue = "L",required = true)
        @PostMapping("/setRedis")
        public Boolean setRedis(@RequestBody String name) {
            log.info("The name value stored in Redis is: {}",name);
            return redisService.set("name", name);
        }
    
        @ApiOperation("向redis中取`name`值")
        @GetMapping("/getRedis")
        public String getRedis() {
            String name = redisService.get("name");
            log.info("The name value obtained from Redis is: {}",name);
            return name;
        }
    }
    
  • 接下来启动项目,用postman发起post请求访问http://localhost:8080/redis/setRedis,其中Body值设为{“L”},并再次发起get请求访问 http://localhost:8080/redis/getRedis,两次操作都成功后,控制台和日志中就会出现如下两条记录

    2020-07-24 16:16:57,268  INFO 19660 --- [nio-8080-exec-1] c.e.b.controller.RedisController         : The name value stored in Redis is: {"L"}
    2020-07-24 16:19:33,650  INFO 6396 --- [nio-8080-exec-2] c.e.b.controller.RedisController         : The name value obtained from Redis is: {"L"}
    
  • 与业务流程不符就不演示了,主要是和自已预想状态不一致时记录日志,这里重点说一下抛出异常时的日志记录,因为日志的一大重要功能就是,在线上无法调试,只能通过日志来调试,一个好的日志可以快速的找出错误,所以这里尤为重要,因为我们是统一异常,要GlobalExceptionHandler类,我们要把可能报的错都记录下来,这样方便调试,修改com.example.backend_template.exception.GlobalExceptionHandler.java类的内容为如下

    package com.example.backend_template.exception;
    
    import com.example.backend_template.utils.ResultData;
    import com.example.backend_template.utils.ResultUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.ConversionNotSupportedException;
    import org.springframework.beans.TypeMismatchException;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.http.converter.HttpMessageNotReadableException;
    import org.springframework.http.converter.HttpMessageNotWritableException;
    import org.springframework.validation.BindException;
    import org.springframework.web.HttpMediaTypeNotAcceptableException;
    import org.springframework.web.HttpMediaTypeNotSupportedException;
    import org.springframework.web.HttpRequestMethodNotSupportedException;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.MissingPathVariableException;
    import org.springframework.web.bind.MissingServletRequestParameterException;
    import org.springframework.web.bind.ServletRequestBindingException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.context.request.WebRequest;
    import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
    import org.springframework.web.multipart.support.MissingServletRequestPartException;
    import org.springframework.web.servlet.NoHandlerFoundException;
    
    /**
     * @ClassName GlobalExceptionHandler 全局统一处理异常
     * @Description
     * @Author L
     * @Date Create by 2020/7/7
     */
    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
        /**
         * 该请求控制器存在,但请求HTTP方法与该控制器提供不符
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
        protected ResponseEntity handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(405, "Http Request Method Not Supported!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Http Request Method Not Supported",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.METHOD_NOT_ALLOWED);
        }
    
        /**
         * content-type 内容设置类型不支持
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
        protected ResponseEntity handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(415, "Http Media Type Not Supported!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Http Media Type Not Supported",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
        }
    
        /**
         * content-type 内容设置类型不能接受
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
        protected ResponseEntity handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(406, "Http Media Type Not Acceptable!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Http Media Type Not Acceptable",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.NOT_ACCEPTABLE);
        }
    
        /**
         * 缺少路径参数
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(MissingPathVariableException.class)
        protected ResponseEntity handleMissingPathVariable(MissingPathVariableException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(500, "Missing Path Variable!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Missing Path Variable",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    
        /**
         * 缺少请求参数
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(MissingServletRequestParameterException.class)
        protected ResponseEntity handleMissingServletRequestParameter(MissingServletRequestParameterException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(400, "Missing Servlet Request Parameter!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Missing Servlet Request Parameter",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST);
        }
    
        /**
         * Servlet请求绑定出错
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(ServletRequestBindingException.class)
        protected ResponseEntity handleServletRequestBindingException(ServletRequestBindingException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(400, "Servlet Request Binding!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Servlet Request Binding",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST);
        }
    
        /**
         * 转换不支持
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(ConversionNotSupportedException.class)
        protected ResponseEntity handleConversionNotSupported(ConversionNotSupportedException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(500, "Conversion Not Supported!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Conversion Not Supported",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    
        /**
         * 参数类型匹配失败
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(TypeMismatchException.class)
        protected ResponseEntity handleTypeMismatch(TypeMismatchException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(400, "Type Mismatch!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Type Mismatch",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST);
        }
    
        /**
         * HTTP 信息请求不可读
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(HttpMessageNotReadableException.class)
        protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(400, "Http Message Not Readable!", ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Http Message Not Readable",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST);
        }
    
        /**
         * HTTP 信息请求不可写
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(HttpMessageNotWritableException.class)
        protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWritableException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(500, "Http Message Not Writable!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Http Message Not Writable",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    
        /**
         * 参数校验出错
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(MethodArgumentNotValidException.class)
        protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(400, "Method Argument Not Valid!", ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Method Argument Not Valid",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST);
        }
    
        /**
         * 丢失了一部分请求信息
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(MissingServletRequestPartException.class)
        protected ResponseEntity handleMissingServletRequestPart(MissingServletRequestPartException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(400, "Missing Servlet Request Part!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Missing Servlet Request Part",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST);
        }
    
        /**
         * 参数绑定出错
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(BindException.class)
        protected ResponseEntity handleBindException(BindException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(400, "Bind Exception!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Bind Exception",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST);
        }
    
        /**
         * 根据请求url找不到控制器,即404异常
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(NoHandlerFoundException.class)
        protected ResponseEntity handleNoHandlerFoundException(NoHandlerFoundException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(404, "No Handler Found!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","No Handler Found",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.NOT_FOUND);
        }
    
        /**
         * 异步请求超时
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(AsyncRequestTimeoutException.class)
        protected ResponseEntity handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(503, "Async Request Timeout!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Async Request Timeout",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.SERVICE_UNAVAILABLE);
        }
    
        /**
         * 其它未统一的异常,都由以下handle处理,生产可写网络异常
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(Exception.class)
        protected ResponseEntity handleAllExceptions(Exception ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(500, "Server Error!",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","Server Error",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    
        //以下是自定义异常
        /**
         * 没有找到该用户
         *
         * @param ex
         * @param request
         * @return
         */
        @ExceptionHandler(UserNotFoundException.class)
        protected ResponseEntity handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
            ResultData errorBody = ResultUtils.fail(404, "User Not Found",ex.getLocalizedMessage());
            log.error("Throw [{}] exception: {}","User Not Found",ex.getLocalizedMessage());
            return new ResponseEntity<>(errorBody, HttpStatus.NOT_FOUND);
        }
    }
      
       
  • 如果系统抛出异常,信息就会打印在控制台,以及储存在日志文件中,格式如下

    2020-07-24 17:03:58,449 ERROR 13600 --- [nio-8080-exec-1] c.e.b.exception.GlobalExceptionHandler   : Throw [User Not Found] exception: This user was not found!
    
  • 至此日志规范就完成了,如果是业务开发,只需要向上面几步一样,在相应的地方添加日志记录就行了

    项目地址

    项目介绍:从零搭建 Spring Boot 后端项目
    代码地址:https://github.com/xiaoxiamo/backend-template

    下一篇

    十二、数据库版本控制

    你可能感兴趣的:(后端模板)