我们都知道日志对于一个系统的重要性,在编码调试阶段我们可以通过控制台的输出来找出并解决bug,当向系统发一个请求时,我们可以通过日志来查看代码的执行流程是否与预期相符从而做出调整。
下文中的Log指的是log4j、logback等日志工具。用到的IDE工具是IDEA。
一.不用System.out.print
1.System.out.print/println
java.Lang.System类下有个静态PrintStream变量out,如下图:
再来看看这个PrintStream类
特点:方便,直观,是jre包中类,随时可用。
2.Log
通常情况下在实际项目中,我们用一套专门的框架来记录系统运行时的信息。基于spring AOP的思想,业务代码和日志代码隔离开,降低他们之间的耦合性。业务代码的运行与日志的打印并非同步。各自执行各自的部分。
特点:异步、解耦、灵活、策略多
3.结论
作用,用System.out和Log几乎是样的。
System.out 是java先天带来的,只要能java程序能运行,System.out就可用;而Log要引入一些包,做一些配置后,整个项目一直都能用。可是如果在项目中如果使用了System.out作为输出,它就是业务逻辑代码的一部分,大量的System.out就会产生大量的IO,无疑会对系统性能产生影响。
下面来看大佬的一些观点:
- System.out.print和java运行程序运行在同一线程,也就是说,业务程序会等待System.out的动作,导致资源被占用,log4j、logback等日志工具进行调试信息的打印,这类工具是异步线程的,不会使程序处于等待状态。
- System.out.print是在控制台输出,只能输出到控制台,功能上线后,总不能一直盯着控制台吧,而且日志文件需要保留,以供日后分析,是需要一套管理规范的,即便使用tomcat服务器,System.out会输出到catalina.out文件,catalina.out文件也不会一直存在,需要定期清空,如果不清空,大文件的读写也是挺影响性能的。说到这里,System.out.print写入的文件只有一个,对于一个文件的读写,这个io肯定会排队写,且System.out.print在当前线程,肯定对性能会有稍微的影响。
- 程序中充斥着大量的System.out.print打印代码是相当不规范的。
综上所述,我们在实际项目中不使用System.out来打印日志。
二.日志框架
1.概况
根据jdbc——数据驱动的思想,写一个统一的接口层;日志门面(日志的一个抽象层);loging-abstract.jar,给项目中导入具体的日志实现就行了。我们之前的日志框架都是实现的抽象层。
日志门面(日志的抽象层) | 日志实现 |
JCL(Jakarta Commons Logging) 、 SLF4J(Simple Logging Facade for Java) 、 Jboss-logging | log4J、log4J2、 logback、 JUL(java.util.logging) |
左边选一个门面(抽象层),右边选一个来实现。
日志门面:
JCL(Jakarta Commons Logging) : 提供统一的日志记录接口,也常称为commons-logging。2014年更新的,太旧不用了。
Jboss-logging: 一般我们用不到。
SLF4J:与JCL一样提供统一的日志记录接口,可以方便地切换看具体的实现框架。
日志实现:
JUL:JDK中的日志记录工具,也常称为JDKLog、jdk-logging。
LOG4J1:一个具体的日志实现框架。
LOG4J2:一个具体的日志实现框架,是LOG4J1的下一个版本。
LOGBACK:一个具体的日志实现框架,但其性能更好。
2.组合
springboot:底层是spring框架,spring框架默认是JCL
springboot选中的是:SLF4J 和 logback
LogBack 被分为3个组件:logback-core、logback-classic 和 logback-access。
- logback-core 提供了 LogBack 的核心功能,是另外两个组件的基础。
- logback-classic 则实现了 SLF4J 的API,所以当想配合 SLF4J 使用时,需要将 logback-classic 引入依赖中。
- logback-access 是为了集成Servlet环境而准备的,可提供HTTP-access的日志接口。
三.springboot使用日志
1.springboot日志关系
在任意一个springboot的web项目中,在IDEA中打开pom文件,看到下面的依赖(点进去spring-boot-starter依赖就可以看到
spring-boot-starter-logging依赖),如下图:
紧接着在pom文件中任意一处右键,点击Diagrams后,选中Show Dependenes…,如下图:
可以看到spring-boot-starter-logging底层依赖关系,如下图:
2.一个简单示例
在springboot项目的测试类中,我们简单的使用下Log,代码如下:
package com.example.demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);
@Test
public void contextLoads() {
// System.out.println("不用这种方式打印日志了");
/**
* 日志级别:由低到高,trace -> debug -> info -> warn -> error
* 以后可以调整日志级别,只产生当前日志级别及其以上级别的日志
* springboot默认使用的是info级别的日志
*/
log.trace("...............trace............");
log.debug("..........debug.............");
log.info(".............info...........");
log.warn("...............warn................");
log.error("...............error............");
//参数使用
String car = "taxi";
String color = "yellow";
log.info("这个{},是{}的",car,color);
}
}
运行该测试类,可以看到日志输出,的确只打印了info、warn、error级别的日志,如下图:
3.日志配置文件
LogBack的日志记录数据流是从 Class(Package)到 Logger,再从Logger到Appender,最后从Appender到具体的输出终端。
如下图所示:
LogBack配置文件可以分为几个节点,其中 Configuration 是根节点,Appender、Logger、Root是Configuration的子节点。
(1).appender节点
是Configuration 的子节点,是负责写日志的组件。appender有两个必要属性name、class 。name指定appender的名称,class指定appender的全限定名。
class,主要包括:
- ch.qos.logback.core.ConsoleAppender 控制台输出
- ch.qos.logback.core.FileAppender 文件输出
- ch.qos.logback.core.RollingFileAppender 文件滚动输出
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration debug="true" scan="true" scanPeriod="2">
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
</appender>
<!-- conf file out -->
<appender name="file_out" class="ch.qos.logback.core.FileAppender">
</appender>
<!-- conf file out -->
<appender name="file_out" class="ch.qos.logback.core.RollingFileAppender">
</appender>
<root></root>
<logger></logger>
</configuration>
a.ch.qos.logback.core.ConsoleAppender
把日志添加到控制台,有如下节点:
<encoder>
: 对日志进行格式化。<target>
: 字符串System.out 或者 System.err, 默认 System.out;
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console_out" />
</root>
</configuration>
b.FileAppender
把日志添加到文件,有如下节点:
<file>
:被写入的文件名,可以是相对目录 , 也可以是绝对目录 , 如果目录不存在则会自动创建。<append>
:如果是true , 日志被追加到文件结尾 , 如果是false,清空现存文件 , 默认是true。<encoder>
:对日志进行格式化 [具体的转换符说明请参见官网.]
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appender name="file_out" class="ch.qos.logback.core.FileAppender">
<file>logs/debug.log</file>
<encoder>
<pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
</encoder>
</appender>
</configuration>
c.RollingFileAppender
滚动纪录文件,先将日志记录到指定文件,当符合某种条件时,将日志记录到其他文件,有如下节点:
<file>
:被写入的文件名,可以是相对目录,也可以解决目录,如果目录不存在则自动创建。<append>
:如果是true,日志被追加到文件结尾,如果是false,清空现存文件,默认是true。<encoder>
:对日志进行格式化。<rollingPolicy>
:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。
<rollingPolicy>节点有如下策略:
TimeBaseRollingPolicy :最常用的滚动策略,根据时间来制定滚动策略,即负责滚动也负责触发滚动。有如下节点;
<fileNamePattern>
:必要节点,包含文件及“%d” 转换符,“%d”可以包含一个java.text.SimpleDateFormat 制定的时间格式,如:%d{yyyy-MM},如果直接使用%d ,默认格式是 yyyy-MM-dd。<maxHistory>
:可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件,假设设置每个月滚动,且 是 6,则只保存最近6个月的文件,删除之前的旧文件,注意:删除旧文件是哪些为了归档而创建的目录也会被删除。<filenamePattern>
:必须包含“%i” 例如:设置最小值,和最大值分别为1和2,命名模式为 log%i.log,会产生归档文件log1.log和log2.log,还可以指定文件压缩选项,例如:log%i.log.gz 或者 log%i.log.zip
triggeringPolicy:告知RollingFileAppender,激活RollingFileAppender滚动。
示例如下:
<!-- 03:conf errorAppender out -->
<appender name="errorAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/error.log</file>
<!-- 设置滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--设置日志命名模式-->
<fileNamePattern>errorFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 超过150MB时,触发滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
(2).logger节点
logger是的子节点,来设置某一个包或者具体的某一个类的日志打印级别,以及指定。logger仅有一个name属性,两个可选属性 level/addtivity。
- name:用来指定受此loger约束的某一个包或者具体的某一个类。
- level:用来设置打印级别,大小写无关。可选值有TRACE、DEBUG、INFO、WARN、ERROR、ALL和OFF。还有一个特俗值INHERITED 或者 同义词NULL,代表强制执行上级的级别。如果未设置此属性,那么当前logger将会继承上级的级别。
- addtivity:是否向上级logger传递打印信息,默认为true;
<logger>可以包含零个或多个<appender-ref>元素,表示这个appender将会添加到logger。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤掉非INFO级别 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- conf infoAppender out -->
<appender name="infoAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/info.log</file>
<!-- 设置滚动策略 -->
<rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--设置日志命名模式-->
<fileNamePattern>infoFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPoliy>
<!-- 超过150MB时,触发滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
<!-- 添加两个appender节点 -->
<logger name="logback.olf.log" level="info">
<appender-ref ref = "console_out"/>
<appender-ref ref = "infoAppender"/>
</logger>
</configuration>
(3).root节点
元素配置根logger。该元素有一个level属性,没有name属性,因为已经被命名 为root。Level属性的值大小写无关,其值为下面其中一个字符串:TRACE、DEBUG、INFO、 WARN、ERROR、ALL 和 OFF。如果 root 元素没 有引用任何 appender,就会失去所有 appender。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤掉非INFO级别 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 01:conf infoAppender out -->
<appender name="infoAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/info.log</file>
<!-- 设置滚动策略 -->
<rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--设置日志命名模式-->
<fileNamePattern>infoFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPoliy>
<!-- 超过150MB时,触发滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
<!-- 02:conf debugAppender out -->
<appender name="debugAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/debug.log</file>
<!-- 设置滚动策略 -->
<rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--设置日志命名模式-->
<fileNamePattern>debugFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPoliy>
<!-- 超过150MB时,触发滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
<!-- 03:conf errorAppender out -->
<appender name="errorAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/error.log</file>
<!-- 设置滚动策略 -->
<rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--设置日志命名模式-->
<fileNamePattern>errorFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPoliy>
<!-- 超过150MB时,触发滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
<root level="ALL">
<appender-ref ref="infoAppender"/>
<appender-ref ref="debugAppender"/>
<appender-ref ref="errorAppender"/>
</root>
</configuration>
(4).filter过滤节点
a.级别过滤器(LevelFilter)
LevelFilter 根据记录级别对记录事件进行过滤。如果事件的级别等于配置的级别,过滤 器会根据 onMatch 和 onMismatch 属性接受或拒绝事件。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤掉非INFO级别 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
b.临界值过滤器(ThresholdFilter)
ThresholdFilter过滤掉低于指定临界值的事件。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 过滤掉TRACE和DEBUG级别的日志 -->
<level>INFO</level>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
c.求值过滤器(EvaluatorFilter)
评估是否符合指定的条件
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.EvaluatorFilter">
<evaluator>
<!--过滤掉所有日志中不包含hello字符的日志-->
<expression>
message.contains("hello")
</expression>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</evaluator>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
d.匹配器(Matchers)
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.EvaluatorFilter">
<evaluator>
<matcher>
<Name>odd</Name>
<!-- 过滤掉序号为奇数的语句-->
<regex>statement [13579]</regex>
</matcher>
<expression>odd.matches(formattedMessage)</expression>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</evaluator>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
4.日志框架切换
在实际开发过程中,我们的项目可能用了不止一种框架,例如:spring(common-logging)、hibernate(Jboss-logging)、mybatis、xxxx等等其他框架,括号中是各框架用的日志实现,那么如何统一使用slf4j进行输出呢?
(1).概述
下面是slf4j官网的一个图解说明
从日志框架转向SLF4J
- jul-to-slf4j:jdk-logging到slf4j的桥梁
- log4j-over-slf4j:log4j1到slf4j的桥梁
- jcl-over-slf4j:commons-logging到slf4j的桥梁
从SLF4J转向具体的日志框架
- slf4j-jdk14:slf4j到jdk-logging的桥梁
- slf4j-log4j12:slf4j到log4j1的桥梁
- log4j-slf4j-impl:slf4j到log4j2的桥梁
- logback-classic:slf4j到logback的桥梁
- slf4j-jcl:slf4j到commons-logging的桥梁
(2).示例
SLF4J+Logback 切换成 slf4j+log4J
上面的示例中所用的项目日志门面和日志实现组合是(SLF4J+Logback,springboot项目都是这种组合)。
a.先把logback-classic和log4j-over-slf4j依赖从依赖树中移除,移除方式如下图:
b.在pom文件中添加slf4j-log4j12的依赖,如下图:
再次打开依赖树,可以看到slf4j-log4j12以及log4j依赖已经添加上了,如下图:
c.启动项目,查看控制台日志,红色字体的warn是因为项目中没有log4j日志框架相关的配置文件,如下图:
d.接下来我们给项目的类路径下添加log4j日志框架的配置文件log4j.properties,如下图:
e.再次启动项目,查看控制台输出,日志多了。且没有刚才的警告了
slf4j+logback 换成 slf4j+log4j2
a.将依赖树中的spring-boot-starter-logging依赖先排除,如下图:
b.接着在pom.xml文件中添加上spring-boot-starter-log4j2依赖,如下图:
·
c.接下来再看依赖树,如下图,spring-boot-log4j2依赖已经添加进来了。
d.接下来启动项目,查看控制台输出如下:
注: 这里的改动参考spring官方文档springboot官方文档starter部分 ,如下图:
本文是通过查阅了很多大佬的日志相关文章和观看了一些教学视频后总结的,其中有很多地方都是前人整理的,例如,配置文件部分,拿到这里一是方便我自己看,而是希望这部分知识能够传的更远,如有侵权,联系可删。其余部分均为我自己所得,欢迎大家批评指正,一起进步!
参考资料:
https://www.cnblogs.com/han-1034683568/p/6637914.html
https://www.cnblogs.com/chanshuyi/p/something_about_java_log_framework.html
https://www.imooc.com/article/details/id/21908#
https://docs.spring.io/spring-boot/docs/2.0.7.RELEASE/reference/htmlsingle/#using-boot-starter