日志可以帮我们在开发过程中乃至于生产上快速有效的定位问题的好帮手,所以如何记录有效的日子变得更加重要,在Spring Boot中,已经为我们默认添加了logback
和log4j2
,默认情况下,Spring Boot采用的是logback
。关于日志组件的性能,我并没有亲自验证过,只是在logback log4j log4j2 性能实测中说log4j2
性能是最高的。
这里咱们就选用log4j2
来作为我们的日志组件,由于默认是使用的logback
,所以在使用之前需要排除掉,并且引入log4j2
,pom.xml配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions><!-- 去掉springboot默认配置 --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!--log4j2--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
|
在spring-boot-starter-log4j2
中,我们可以看到依赖包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.12.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-jul</artifactId> <version>2.12.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> <version>1.7.29</version> <scope>compile</scope> </dependency> </dependencies>
|
接下来,我们正式开始启用吧,首先在resources
目录下新建一个log4j2.xml
配置文件,配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| <?xml version="1.0" encoding="UTF-8"?> <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--status="WARN" :用于设置log4j2自身内部日志的信息输出级别,默认是OFF--> <!--monitorInterval="30" :间隔秒数,自动检测配置文件的变更和重新配置本身--> <configuration status="WARN" monitorInterval="30"> <Properties> <!--自定义一些常量,之后使用${变量名}引用--> <Property name="logFilePath">D://log</Property> <Property name="logFileName">test.log</Property> <Property name="logPattern"> %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %l ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex </Property> </Properties> <!--appenders:定义输出内容,输出格式,输出方式,日志保存策略等,常用其下三种标签[console,File,RollingFile]--> <appenders> <!--console :控制台输出的配置--> <console name="Console" target="SYSTEM_OUT"> <!--PatternLayout :输出日志的格式,LOG4J2定义了输出代码,详见第二部分--> <PatternLayout pattern="${logPattern}"/> </console> <!--File :同步输出日志到本地文件--> <!--append="false" :根据其下日志策略,每次清空文件重新输入日志,可用于测试--> <!-- <File name="log" fileName="${logFilePath}/logs/info.log" append="false">--> <!-- <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>--> <!-- </File>--> <!--SMTP :邮件发送日志--> <!-- <SMTP name="Mail" subject="****SaaS系统正式版异常信息" to="message@message.info" from="message@lengjing.info" smtpUsername="message@message.info" smtpPassword="LENG****1234" smtpHost="mail.lengjing.info" smtpDebug="false" smtpPort="25" bufferSize="10">--> <!-- <PatternLayout pattern="[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />--> <!-- </SMTP>--> <!-- ${sys:user.home} :项目路径 --> <RollingFile name="RollingFileInfo" fileName="${logFilePath}/logs/info.log" filePattern="${logFilePath}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log"> <!--ThresholdFilter :日志输出过滤--> <!--level="info" :日志级别,onMatch="ACCEPT" :级别在info之上则接受,onMismatch="DENY" :级别在info之下则拒绝--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <!-- Policies :日志滚动策略--> <Policies> <!-- TimeBasedTriggeringPolicy :时间滚动策略,默认0点小时产生新的文件,interval="6" : 自定义文件滚动时间间隔,每隔6小时产生新文件, modulate="true" : 产生文件是否以0点偏移时间,即6点,12点,18点,0点--> <TimeBasedTriggeringPolicy interval="6" modulate="true"/> <!-- SizeBasedTriggeringPolicy :文件大小滚动策略--> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 --> <DefaultRolloverStrategy max="20"/> </RollingFile>
<RollingFile name="RollingFileWarn" fileName="${logFilePath}/logs/warn.log" filePattern="${logFilePath}/logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log"> <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <Policies> <TimeBasedTriggeringPolicy/> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> </RollingFile> <RollingFile name="RollingFileError" fileName="${logFilePath}/logs/error.log" filePattern="${logFilePath}/logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log"> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <Policies> <TimeBasedTriggeringPolicy/> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> </RollingFile> </appenders> <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效--> <loggers> <!--过滤掉spring和mybatis的一些无用的DEBUG信息--> <!--Logger节点用来单独指定日志的形式,name为包路径,比如要为org.springframework包下所有日志指定为INFO级别等。 --> <logger name="org.springframework" level="INFO"></logger> <logger name="org.mybatis" level="INFO"></logger> <!-- Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出 --> <Root level="ALL"> <appender-ref ref="Console"/> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> </Root> <!--AsyncLogger :异步日志,LOG4J有三种日志模式,全异步日志,混合模式,同步日志,性能从高到底,线程越多效率越高,也可以避免日志卡死线程情况发生--> <!--additivity="false" : additivity设置事件是否在root logger输出,为了避免重复输出,可以在Logger 标签下设置additivity为”false”--> <!-- <AsyncLogger name="AsyncLogger" level="ALL" includeLocation="true" additivity="false">--> <!-- <appender-ref ref="RollingFileError"/>--> <!-- <appender-ref ref="RollingFileInfo"/>--> <!-- </AsyncLogger>--> </loggers> </configuration>
|
以上配置类源于LOG4J2 完整配置详解,在代码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) { if (logFile != null) { loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile); } else { loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile); } } @Override protected String[] getStandardConfigLocations() { return getCurrentlySupportedConfigLocations(); } private String[] getCurrentlySupportedConfigLocations() { List<String> supportedConfigLocations = new ArrayList<>(); supportedConfigLocations.add("log4j2.properties"); if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) { Collections.addAll(supportedConfigLocations, "log4j2.yaml", "log4j2.yml"); } if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) { Collections.addAll(supportedConfigLocations, "log4j2.json", "log4j2.jsn"); } supportedConfigLocations.add("log4j2.xml"); return StringUtils.toStringArray(supportedConfigLocations); }
|
大概可以看出Spring Boot是可以自动识别log4j2.xml
或者log4j2.yaml
等配置文件,当然,如果是其他命名方式,可以在application.properties
配置文件中配置,如:logging.config=classpath:log4j2-config.xml
,yaml配置类似。接下来就是在我们的程序中使用,
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RequestMapping("/exception") @RestController @Api(tags = "制造bug") public class ExceptionController { private static final Logger logger = LogManager.getLogger(ExceptionController.class); @GetMapping("err") public String getErr(){ logger.info("制造一个bug"); int calc= 1/0; return "error"; } }
|
调用结果:
1 2
| 2019-12-25 09:49:31.199 DEBUG springfox.documentation.spring.web.PropertySourcedRequestMappingHandlerMapping.lookupHandlerMethod(PropertySourcedRequestMappingHandlerMapping.java:108) 2CNU7X5OLAUE004 --- [nio-8080-exec-1] pertySourcedRequestMappingHandlerMapping : looking up handler for path: /exception/err 2019-12-25 09:49:31.205 INFO com.eyiadmin.demo.controller.ExceptionController.getErr(ExceptionController.java:17) 2CNU7X5OLAUE004 --- [nio-8080-exec-1] c.e.d.c.ExceptionController : 制造一个bug
|
这样一来,我们就可以在之前说的GlobalExceptionHandler
来记录我们的错误日志了,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LogManager.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(BindException.class) public ResponseResult<?> handleValidationException(BindException ex) { BindingResult validResult = ex.getBindingResult(); if (validResult.hasErrors()) { logger.error(validResult.getAllErrors().stream().map(err->err.getDefaultMessage()).collect(Collectors.joining("|"))); return ResponseResult.error(validResult.getAllErrors().get(0).getDefaultMessage()); } else { return ResponseResult.error("操作失败"); } }
//捕捉所有Exception异常,这里可以捕捉自定义的业务异常 @ExceptionHandler(Exception.class) ResponseResult<?> handleException(Exception ex){ logger.error(ex.getMessage()); logger.error(ex.getStackTrace()); return ResponseResult.error(ex.getMessage()); } }
|
最后效果:
1
| 2019-12-25 10:14:38.086 ERROR com.eyiadmin.demo.handler.GlobalExceptionHandler.handleValidationException(GlobalExceptionHandler.java:21) 2CNU7X5OLAUE004 --- [nio-8080-exec-2] c.e.d.h.GlobalExceptionHandler : 学生名字不能为空
|
至此, log4j2.xml
整合完毕,但是我们的工作还没有完,我们在需要记录日志的每个类文件都要写一个 private static final Logger logger = LogManager.getLogger(GlobalExceptionHandler.class);
,虽然工作量不大,但有时候可能会马虎的忽视掉GlobalExceptionHandler.class
,那么我们有没有更简便的使用方式呢?这里我们可以借助强大的Lombok
插件为我们提供好的@Log4j2
注解,在需要打印日志的类上加上这个注解即可。
1 2 3 4 5
| @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE}) public @interface Log4j2 { String topic() default ""; }
|
我们可以看到@Log4j2
有一个参数,它用于标识我们记录器的类别,默认情况下,它会将使用注解的类型,接下来我们来尝试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RestControllerAdvice @Log4j2 public class GlobalExceptionHandler { @ExceptionHandler(BindException.class) public ResponseResult<?> handleValidationException(BindException ex) { BindingResult validResult = ex.getBindingResult(); if (validResult.hasErrors()) { log.error(validResult.getAllErrors().stream().map(err->err.getDefaultMessage()).collect(Collectors.joining("|"))); return ResponseResult.error(validResult.getAllErrors().get(0).getDefaultMessage()); } else { return ResponseResult.error("操作失败"); } }
//捕捉所有Exception异常,这里可以捕捉自定义的业务异常 @ExceptionHandler(Exception.class) ResponseResult<?> handleException(Exception ex){ log.error(ex.getMessage()); log.error(ex.getStackTrace()); return ResponseResult.error(ex.getMessage()); } }
|
最后打印的日志如下:
1
| 2019-12-25 10:21:33.684 ERROR com.eyiadmin.demo.handler.GlobalExceptionHandler.handleValidationException(GlobalExceptionHandler.java:22) 2CNU7X5OLAUE004 --- [nio-8080-exec-1] c.e.d.h.GlobalExceptionHandler : 学生名字不能为空
|
如果我们给@Log4j2
带上topic
会输出什么呢?:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RequestMapping("/exception") @RestController @Api(tags = "制造bug") @Log4j2(topic = "exception") public class ExceptionController {
@GetMapping("err") public String getErr(){ log.info("制造一个bug"); int calc= 1/0; return "error"; } }
|
效果:
1
| 2019-12-25 10:22:57.078 INFO com.eyiadmin.demo.controller.ExceptionController.getErr(ExceptionController.java:19) 2CNU7X5OLAUE004 --- [nio-8080-exec-3] exception : 制造一个bug
|
这里只是简单的说了一下日志组件的使用,我们在实际使用过程中,也会结合AOP来应用。以及在分布式、微服务中的链路追踪,