日志能记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题。尤其是项目线上问题,不允许远程调试的情况下,只能依赖日志定位问题,如果日志写的好,那就能快速找到问题所在。反之,日志没写好,反而会影响程序的运行性能和稳定性。日志的用途大致可以分为:
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组合就好
注:上面的格式其实就是Spring Boot日志默认的格式
例如:
//SpringBoot日志默认格式
2020-07-22 16:53:51.532 INFO 14912 --- [ main] c.e.b.BackendTemplateApplication: Starting BackendTemplateApplication...
平时主要是用以下几种,分别讲一下怎么选择使用
日志输出需使用点位符输出,而不是用字符拼接
//如果日志级别是 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
如果系统抛出异常,信息就会打印在控制台,以及储存在日志文件中,格式如下
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
十二、数据库版本控制