很重要,但又可以忽略,但又不得不提的东西。
这里面有很多的思想,如果你有设计模式基础,你应该看着会很舒服。
这里不再是简单的使用,而是有源码的解读,会对比各种日志(门面、实现)的优缺点,不同之处吧算是了。
.
“”””””””””””””
“”” 文章待整理 “””
“”””””””””””””
1、SpringBoot Controller 包打印日志:请求参数,返回值
2、SpringBoot 使用 AOP 来抽取日志
3、SpringBoot 日志问题整理总结
4、Java 执行 main 方法时关掉日志打印
5、【SpringBoot】①入门,配置,日志
.
2021-11-06 00:13:42
最近到的新公司,为了第一版的新功能上线,也是有在忙,今天已经是周六了。
日志的概念
2021-11-05 17:14:55
概述
日志文件是用于记录系统操作事件的文件集合,可分为事件日志和消息日志。具有处理历史数据、诊断问题的追踪以及理解系统的活动等重要作用。
在计算机中,日志文件是记录在操作系统或其他软件运行中发生的事件或在通信软件的不同用户之间的消息的文件。记录是保持日志的行为。在最简单的情况下,消息被写入单个日志文件。
日志的作用
调试
在Java项目调试时,查看栈信息可以方便地知道当前程序的运行状态, 输出的日志便于记录程序在之前的运行结果。如果你大量使用System.out或者System.err,这是一种最方便最有效的方法,但显得不够专业。错误定位
不要以为项目能正确跑起来就可以高枕无忧,项目在运行一段时候后,可能由于数据问题,网络问题,内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置,提出解决方案。数据分析
大数据的兴起,使得大量的日志分析成为可能,ELK也让日志分析门槛降低了很多。日志中蕴含了大量的用户数据,包括点击行为,兴趣偏好等,用户画像对于公司下一步的战略方向有一定指引作用。
接触过的日志
最简单的日志输出方式,我们每天都在使用:
1 | System.out.println("这个数的结果是:"+ num); |
以及错误日志:
1 | System.err.println("此处发生了异常"); |
此类代码在程序的执行过程中没有什么实质的作用,但是却能打印一些中间变量,辅助我们调试和错误的排查。
日志系统我们也见过:
在tomcat中
当我们的程序无法启动或者运行过程中产生问题,会有所记录,比如我的catalina.log中查看,发现确实有错误信息,这能帮我们迅速定位:
而我们的System.err只能做到控制台打印日志,所以我们需要更强大日志框架来处理:
主流日志框架
日志实现(具体干活的)
实现类
:JUL(java util logging)、logback、
log4j、log4j2日志门面(指定规则的)
接口
:JCL(Jakarta Commons Logging)、
slf4j( Simple Logging Facade for Java)
JUL日志框架
2021-11-05 17:21:23
JUL全称Java util Logging是java原生的日志框架,使用时不需要另外引用第三方类库,相对其他日志框 架使用方便,学习简单,能够在小型应用中灵活使用。
在JUL中有以下组件,我们先做了解,慢慢学习:
Loggers:被称为记录器,应用程序通过获取Logger对象,调用其API来来发布日志信息。Logger 通常时应用程序访问日志系统的入口程序。
Appenders:也被称为Handlers,每个Logger都会关联一组 Handlers,Logger会将日志交给关联 Handlers处理,由Handlers负责将日志做记录。Handlers在此是一个抽象,其具体的实现决定了 日志记录的位置可以是控制台、文件、网络上的其他日志服务或操作系统日志等。
Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了 数据在一条日志记录中的最终形式。
Level:每条日志消息都有一个关联的日志级别。该级别粗略指导了日志消息的重要性和紧迫,我 可以将Level和Loggers,Appenders做关联以便于我们过滤消息。
Filters:过滤器,根据需要定制哪些信息会被记录,哪些信息会被放过。
总结一下就是:
用户使用Logger来进行日志记录,Logger持有若干个Handler,日志的输出操作是由Handler完成的。
在Handler在输出日志前,会经过Filter的过滤,判断哪些日志级别过滤放行哪些拦截,Handler会将日志内容输出到指定位置(日志文件、控制台等)。Handler在输出日志时会使用Layout,将输出内容进行排版。
入门案例
1 | public static void main(String[] args) { |
日志的级别
jul中定义的日志级别,从上述例子中我们也看到使用info和warning打印出的日志有不同的前缀,通过给日志设置不同的级别可以清晰的从日志中区分出哪些是基本信息,哪些是调试信息,哪些是严重的异常。
java.util.logging.Level中定义了日志的级别:
1、SEVERE(最高值)
2、WARNING
3、INFO (默认级别)
4、CONFIG
5、FINE
6、FINER
7、FINEST(最低值)
再例如:我们查看tomcat的日志,能明显的看到不同级别的日志,其实tomcat默认使用的就是JUL:
还有两个特殊的级别:
- OFF,可用来关闭日志记录。
- ALL,启用所有消息的日志记录。
虽然我们测试了7个日志级别
1 |
|
我们发现能够打印的只有三行,这是为什么呢?
我们找一下这个文件,下图是jdk11的日志配置文件:
或者在jdk1.8中:
就可以看到系统默认在控制台打印的日志级别了,系统配置我们暂且不动,一会我们独立创建配置文件完成修改。
但是我们可以简单的看看这个日志配置了哪些内容:
1 | INFO = |
在日志中我们发现了,貌似可以给这个日志对象添加各种handler就是处理器,比如ConsoleHandler专门处理控制台日志,FileHandler貌似可以处理文件,同时我们确实发现了他有这么一个方法:
日志配置
1 |
|
文件中也输出了正确的结果:
Logger之间的父子关系
JUL中Logger之间存在父子关系,这种父子关系通过树状结构存储,JUL在初始化时会创建一个顶层 RootLogger作为所有Logger父Logger,存储上作为树状结构的根节点。并父子关系通过名称来关联。默认子Logger会继承父Logger的属性。
所有的logger实例都是由LogManager统一管理,不妨我们点进getLogger方法:
1 | private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) { |
我们可以看到LogManager是单例的:
1 | public static LogManager getLogManager() { |
1 |
|
1 |
|
日志格式化
我们可以独立的实现日志格式化的Formatter,而不使用SimpleFormatter,我们可以做如下处理,最后返回的结果我们可以随意拼写:
1 | Formatter myFormatter = new Formatter() { |
结果为:
当然我们参考一下SimpleFormatter的该方法的实现:
1 | // format string for printing the log record |
这个写法貌似比我们的写法高级一点,所以我们必须好好学一下String的format方法了。
String的format方法
String类的format()方法用于创建格式化的字符串以及连接多个字符串对象。
format()方法有两种重载形式:
1 | public static String format(String format, Object... args) { |
在这个方法中我们可以定义字符串模板,然后使用类似填空的方式将模板格式化成我们想要的结果字符串:
1 | String java = String.format("hello %s", "world"); |
得到的结果就是hello world,我们可以把第一个参数当做模板, %s当做填空题,后边的可变参数当做答案。
常用的转换符
当然不同数据类型需要不同转换符完成字符串的转换,以下是不同类型的转化符列表:
小例子:
1 | System.out.printf("过年了,%s今年%d岁了,今天收了%f元的压岁 钱!", "小明",5,88.88); |
这要比拼写字符串简单多了。
特殊符号
接下来我们看几个特殊字符的常用搭配,可以实现一些高级功能:
1 | System.out.printf("过年了,%s今年%03d岁了,今天收了%,f元的压 岁钱!", "小明",5,8888.88); |
默认情况下,我们的可变参数是安装顺序依次替换,但是我想重复利用可变参数那该怎么处理呢?
1 | // 我们可以采用 在转换符中加数字$完成匹配: |
日期处理
第一个例子中有说到 %tx
x代表日期转换符 我也顺便列举下日期转换符
我们可以使用以下三个类去进行格式化,其中可能存在不支持的情况,比如LocalDateTime不支持c:
1 | System.out.printf("%tc",new Date()); |
此时我们使用debug查看,默认情况下的fomat,我们不妨来读一读:
1 | 10月 21, 2021 2:23:42 下午 com.taopanfeng.entity.LoggerTest testLogParent |
配置文件
我们不妨看看一个文件处理器的源码是怎么读配置项的:
1 | private void configure() { |
可以从以下源码中看到配置项:
1 | public class FileHandler extends StreamHandler { |
我们已经知道系统默认的配置文件的位置,那我们能不能自定义呢?当然可以了,我们从jdk中赋值一个配置文件过来:
1 | INFO = |
1 | static File generate(String pat, int count, int generation, int unique) throws IOException { |
我们将拷贝的文件稍作修改:
1 | .level= INFO |
1 |
|
配置文件:
1 | handlers= java.util.logging.ConsoleHandler,java.util.logging.Fil eHandler |
文件中也出现了:
打开日志发现是xml,因为这里用的就是XMLFormatter:
上边我们配置了两个handler给根Logger,我们还可以给其他的Logger做独立的配置:
1 | handlers=java.util.logging.ConsoleHandler |
执行发现控制台没有内容,文件中有了,说明没有问题OK了:
日志出现以下内容:
LOG4J 日志框架
Log4j是Apache下的一款开源的日志框架。官方网站:http://logging.apache.org/log4j/1.2/,这是一款比较老的日志框架,目前新的log4j2做了很大的改动,仍然有一些项目在使用log4j。
入门案例
1、建立maven工程
2、添加依赖
1 | <dependencies> |
3、java代码
1 |
|
发现会有一些警告,JUL可以直接在控制台输出是因为他有默认的配置文件,而这个独立的第三方的日志框架却没有配置文件:
1 | log4j:WARN No appenders could be found for logger (com.taopanfeng.entity.Log4jTest). |
我们在执行代码之前,加上以下代码,他会初始化一个默认配置:
1 | BasicConfigurator.configure(); |
结果:
1 | 0 [main] INFO com.taopanfeng.entity.Log4jTest - hello log4j |
从源码看,这一行代码给我们的RootLogger加入一个控制台的输出源,就和jul中的handler一样:
1 | public static void configure() { |
log4j定义了以下的日志的级别,和JUL的略有不同:
1、fatal 指出每个严重的错误事件将会导致应用程序的退出。
2、error 指出虽然发生错误事件,但仍然不影响系统的继续运行。
3、warn 表明会出现潜在的错误情形。
4、info 一般和在粗粒度级别上,强调应用程序的运行全程。
5、debug 一般用于细粒度级别上,对调试应用程序非常有帮助。
6、trace 是程序追踪,可以用于输出程序运行中的变量,显示执行的流程。
和JUL一样:还有两个特殊的级别:OFF,可用来关闭日志记录。 ALL,启用所有消息的日志记录。
一般情况下,我们只使用4个级别,优先级从高到低为 ERROR > WARN > INFO > DEBUG
。
组件讲解
Log4J 主要由 Loggers (日志记录器)、Appenders(输出端)和Layout(日志格式化器)组成。其中 Loggers 控制日志的输出级别与日志是否输出;Appenders 指定日志的输出方式(输出到控制台、文件 等);Layout 控制日志信息的输出格式。
Loggers
日志记录器:负责收集处理日志记录,实例的命名就是类“XX”的fullquailied name(类的全限定名), Logger的名字大小写敏感,其命名有继承机制:例如:name为com.taopanfeng.service的logger会继承 name为com.taopanfeng的logger,和JUL一致。
Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接 或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。 JUL是不是也有一个名为.
的根。
Appenders
Appender和JUL的Handler很像,用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。Log4j 常用的输出目的地 有以下几种:
1 | // 配置一个控制台输出源 |
Layouts
1 | Layout layout = new Layout() { |
有一些默认的实现类:
1 | Layout layout = new SimpleLayout(); |
他的实现太简单了:
1 | public String format(LoggingEvent event) { |
还有一个比较常用的Layout,就是PatternLayout这个实现类,能够根据特定的占位符进行转化,和JUL很像,但是又不一样,我们庖丁解牛研究一番,首先看他的构造器,构造器中如果传入一个pattern字符串,他会根据这个pattern创建一个链表,这个链表具体干什么咱们慢慢往后看:
1 | public PatternLayout(String pattern) { |
将步骤拆解开来看,首先创建了一个解析器:
1 | protected PatternParser createPatternParser(String pattern) { |
查看parse方法,这个方法比较复杂我们简化来看:
1 | public PatternConverter parse() { |
而finalizeConverter做的工作大家就能看的很清楚了:
1 | protected void finalizeConverter(char c) { |
下边就是一个典型的链表结构的构建了:
1 | protected void addConverter(PatternConverter pc) { |
1 | private void addToList(PatternConverter pc) { |
构建完转化器链表之后,就是循环这个链表,一次处理对应的占位符了,他的核心的格式化的方法也是format方法,在format方法中是通过一个转化器链来完成转化的:
1 | public String format(LoggingEvent event) { |
这里就是通过一个pattern字符串,这个字符串可能张这个样子%-d{yyyy-MM-dd HH:mm:ss} [%t:%r] -[%p] %m%n
,使用createPatternParser().parse()方法构建一个处理器的链表,这个每个处理器处理一个占位符比如(%d)。
进入c.format()方法,我们会进入一个抽象类PatternConverter中的format方法,里边的核心就是如下代码:
1 | public void format(StringBuffer sbuf, LoggingEvent e) { |
log4j 其实采用类似 C 语言的 printf 函数的打印格式格式化日志信息,源码已经看过了,具体的占位符及其含义如下:
1 | %m 输出代码中指定的日志信息 |
举一个例子:
1 | %-d{yyyy-MM-ddHH:mm:ss}[%t:%r]-[%p]%m%n |
尝试写一个:
1 |
|
配置一个jdbcAppender
1 | JDBCAppenderjdbcAppender = newJDBCAppender(); |
数据表
1 | CREATE TABLE ` log ` ( |
依赖
1 | <dependency> |
配置
log4j不仅仅可以在控制台,文件文件中输出日志,甚至可以在数据库中,我们先使用配置的方式完成日志的输入:
1 | # 指定日志的输出级别与输出端 |
有了这个配置文件我们些代码就简单了一些:
1 |
|
文件也有了:
内容:
我们查看了可是确实没有问题。
当然日志配置文件是什么时候读取的呢?每一个logger都是LogManager创建的,而LogManager有一个静态代码块帮助我们解析配置文件,细节就不需要了解了:
1 | public class LogManager { |
还有更有意思的,我们可以直接添加一个数据源,讲日志输出到数据库中,就是一个和数据库链接的输出源而已:
加入一个数据库的日志输出源:
1 | # mysql |
需要
1 | CREATE TABLE `log` ( |
pom中添加驱动:
1 | <dependency> |
再次执行:
发现除了控制台,文件,数据库中也有了日志了:
自定义Logger
1 | # RootLogger配置 |
由此我们发现,我们可以很灵活的自定义,组装不同logger的实现,接下来我们写代码测试:
1 |
|
我们发现logger1的日志级别成了warn,并且在数据库中有了日志,logger2级别成了error,他们其实都继承了根logger的一些属性。
日志门面
当我们的系统变的复杂的之后,难免会集成其他的系统,不同的系统之间可能会使用不同的日志系统。那么在一个系统中,我们的日志框架可能会出现多个,会出现混乱,而且随着时间的发展,可能会出现新的效率更高的日志系统,如果我们想切换代价会非常的大。如果我们的日志系统能和jdbc一样,有一套自己的规范,其他实现均按照规范去实现,就能很灵活的使用日志框架了。
日志门面就是为了解决这个问题而出现的一种技术,日志门面是规范,其他的实现按照规范实现各自的日志框架即可,我们程序员基于日志门面编程即可。举个例子:日志门面就好比菜单,日志实现就好比厨师,我们去餐馆吃饭按照菜单点菜即可,厨师是谁其实不重要,但是有一个符合我口味的厨师当然会更好。
常见的日志门面(接口): JCL、slf4j
常见的日志实现(实现类): JUL、log4j、logback、log4j2
日志框架出现的历史顺序:log4j –> JUL –> JCL –> slf4j –> logback –> log4j2
SLF4J日志门面
简单日志门面(Simple Logging Facade For Java) SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。
当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架 会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。
官方网站: https://www.slf4j.org/
SLF4J是目前市面上最流行的日志门面。现在的项目中,基本上都是使用SLF4J作为我们的日志系统。
SLF4J日志门面主要提供两大功能:
1、日志框架的绑定
2、日志框架的桥接
阿里日志规约
- 应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API。使用门面模式的日志框架,有利于维护和各个类的日志处理方法统一。
- 日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。
- 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType为日志类型,推荐分类有stats/monitor/visit 等;
- logName为日志描述。这种命名的好处:通过文件名就可以知道日志文件属于哪个应用,哪种类型,有什么目的,这也有利于归类查找。
- 对trace/debug/info级别的日志输出,必须使用条件输出形式或者占位符的方式。
- 避免重复打印日志,否则会浪费磁盘空间。务必在日志配置文件中设置additivity=false。 7. 异常信息应该包括两类:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字向上抛出。
- 谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免吧服务器磁盘撑爆,并及时删除这些观察日志。
- 可以使用warn日志级别记录用户输入参数错误的情况,避免当用户投诉时无所适从。
SLF4J实战
(1)添加依赖
1 | <!--slf4j core 使用slf4j必須添加--> |
(2)编写代码
1 | public class TestSlf4j { |
slf4j支持占位符
绑定其他日志的实现(Binding)
如前所述,SLF4J支持各种日志框架。SLF4J发行版附带了几个称为“SLF4J绑定”的jar文件,每个绑定对应一个受支持的框架。
使用slf4j的日志绑定流程:
1 | 1. 添加slf4j-api的依赖 |
绑定jul的实现
1 | <dependency> |
绑定log4j的实现
1 | <!--slf4j core 使用slf4j必須添加--> |
要切换日志框架,只需替换类路径上的slf4j绑定。例如,要从java.util.logging切换到log4j,只需将 slf4j-jdk14-1.7.27.jar替换为slf4j-log4j12-1.7.27.jar即可。
SLF4J不依赖于任何特殊的类装载。实际上,每个SLF4J绑定在编译时都是硬连线的, 以使用一个且只有 一个特定的日志记录框架。例如,slf4j-log4j12-1.7.27.jar绑定在编译时绑定以使用log4j。
桥接旧的日志框架(Bridging)
通常,您依赖的某些组件依赖于SLF4J以外的日志记录API。您也可以假设这些组件在不久的将来不会切换到SLF4J。为了解决这种情况,SLF4J附带了几个桥接模块,这些模块将对log4j,JCL和 java.util.logging API的调用重定向,就好像它们是对SLF4J API一样。
1 | 2022-06-26 12:06:56 补:鹊桥相会 |
就是你还用log4j的api写代码,但是具体的实现给你抽离了,我们依赖了一个中间层,这个层其实是用旧的api操作slf4j,而不是操作具体的实现。
桥接解决的是项目中日志的遗留问题,当系统中存在之前的日志API,可以通过桥接转换到slf4j的实现
1、先去除之前老的日志框架的依赖,必须去掉。
2、添加SLF4J提供的桥接组件,这个组件就是模仿之前老的日志写了一套相同的api,只不过这个api是在调用slf4j的api。
3、为项目添加SLF4J的具体实现。
迁移的方式:
1 | <!-- 桥接的组件 --> |
SLF4J提供的桥接器:
1 | <!-- log4j--> |
注意问题:
1、jcl-over-slf4j.jar和 slf4j-jcl.jar不能同时部署。前一个jar文件将导致JCL将日志系统的选择委托给 SLF4J,后一个jar文件将导致SLF4J将日志系统的选择委托给JCL,从而导致无限循环。
2、log4j-over-slf4j.jar和slf4j-log4j12.jar不能同时出现
3、jul-to-slf4j.jar和slf4j-jdk14.jar不能同时出现
4、所有的桥接都只对Logger日志记录器对象有效,如果程序中调用了内部的配置类或者是 Appender,Filter等对象,将无法产生效果。
SLF4J原理解析
1、SLF4J通过LoggerFactory加载日志具体的实现对象。
2、LoggerFactory在初始化的过程中,会通过performInitialization()方法绑定具体的日志实现。
3、在绑定具体实现的时候,通过类加载器,加载org/slf4j/impl/StaticLoggerBinder.class
4、所以,只要是一个日志实现框架,在org.slf4j.impl包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J所加载
在slf4j中创建logger的方法是:
1 | public static Logger getLogger(String name) { |
继续进入查看,核心就是performInitialization();:
1 | public static ILoggerFactory getILoggerFactory() { |
继续进入查看,核心就是bind(),这个方法应该就能绑定日志实现了:
1 | private final static void performInitialization() { |
来到这里,看看绑定的方法:
1 | private final static void bind() { |
每一个日志实现的中间包都有一个StaticLoggerBinder:
1 | public class StaticLoggerBinder implements LoggerFactoryBinder { |
JCL 日志门面
全称为Jakarta Commons Logging,是Apache提供的一个通用日志API。 改日志门面的使用并不是很广泛。
它是为 “所有的Java日志实现”提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常常弱 (SimpleLog)。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk 自带的日志(JUL)。
JCL 有两个基本的抽象类:Log(基本记录器)和LogFactory(负责创建Log实 例)。
JCL入门
1、建立maven工程
2、添加依赖
1 | <dependency> |
3、入门代码
1 | public class JULTest { |
我们为什么要使用日志门面:
- 面向接口开发,不再依赖具体的实现类。减少代码的耦合
- 项目通过导入不同的日志实现类,可以灵活的切换日志框架
- 统一API,方便开发者学习和使用
- 统一配置便于项目日志的管理
JCL原理
1、通过LogFactory动态加载Log实现类
2、日志门面支持的日志实现数组
1 | private static final String[] classesToDiscover = new String[]{ |
3、获取具体的日志实现
1 | for (int i = 0; i < classesToDiscover.length && result == null; ++i) { |
日志生态图
历史插曲:现在聊聊历史:一人顶一个军队 — Java日志系统历史从入门到崩溃 - SegmentFault 思否 2021-11-05 23:46:23
.
.
.
“一人顶一个军队” 我好喜欢这句话。好霸气,有木有!
*Logback的使用
引入上面↑↑↑ 历史插曲 ↑↑↑
中的一段话。Logback:“没有人比我更懂 Slf4j”(完美适配!)。
Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好。
(log4j, slf4j, logback 出自同一人 Ceki Gülcü
)
官方网站:https://logback.qos.ch/index.html
Logback主要分为三个模块:
- logback-core:其它两个模块的基础模块
- logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
- logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能 后续的日志代码都是通过SLF4J日志门面搭建日志系统,所以在代码是没有区别,主要是通过修改配置文件和pom.xml依赖
logback入门
添加依赖
1
2
3
4
5<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>java代码
1
2
3
4
5
6
7
8
9
10
11
12public class TestLogback {
private final static Logger logger = LoggerFactory.getLogger(TestLog4j.class);
public void testLogback() { //打印日志信息
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
}
}其实我们发现即使项目中没有引入slf4j我们这里也是用的slf4j门面进行编程。
1、
2、从logback’的pom依赖中我们看到slf4j,依赖会进行传递
源码解析
spi机制
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。他是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
主要是使用,java.util包下的ServiceLoader实现:
1 | public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { |
源码解析
源码看一下启动过程:
1、我们从日志工厂的常见看起,这里是slf4j的实现:
1 | private final static Logger logger = LoggerFactory.getLogger(TestLog4j.class); |
核心方法只有一句:
1 | public static Logger getLogger(Class<?> clazz) { |
看一下getLogger方法,这里是先获取日志工厂,在从工厂中提取日志对象,我们不考虑日志对象,主要看看日志工厂的环境怎么初始化的:
1 | public static Logger getLogger(String name) { |
日志工厂的创建方法:
1 | public static ILoggerFactory getILoggerFactory() { |
这里就进入了,StaticLoggerBinder这个对象,这是日志实现用来和slf4j进行绑定的类,从此就进入日志实现中了。
StaticLoggerBinder.getSingleton()这里看到出来是一个单例,来到这个类当中,我们看到,直接返回了defaultLoggerContext
1 | public ILoggerFactory getLoggerFactory() { |
这是个日志上下文,一定保存了我们的环境,配置内容一定在这个里边,那么哪里初始化他了呢,我们能想到的就是静态代码块了:
我们发现这个类中还真有:
1 | static { |
我们看到init()方法中,有一个autoConfig(),感觉就像在自动配置:
1 | void init() { |
默认配置:ContextInitializer类是初始化的关键:
自动配置是这么玩的,先找配置文件
1 | public void autoConfig() throws JoranException { |
寻找配置文件的过程:
1 | final public static String GROOVY_AUTOCONFIG_FILE = "logback.groovy"; |
1 | public void configureByResource(URL url) throws JoranException { |
基础配置的代码:
1 | public class BasicConfigurator extends ContextAwareBase implements Configurator { |
我们先不说配置的事情,从源码中我们可以看出有几种配置,因为有了我们先模仿BasicConfigurator写一个类,只做略微的改动:
1 | public class MyConfigurator extends ContextAwareBase implements Configurator { |
在resource中新建META-INF目录,下边在新建services文件夹,再新建一个名字我ch.qos.logback.classic.spi.Configurator
的文件,
内容是:com.taopanfeng.MyConfigurator
*logback工作流程
2022-06-26 12:35:37 补图片
*logback xml 配置
Logback 官网 — xml 日志配置 Chapter 3: Configuration 2022-06-26 19:18:26
*logback layout配置
Logback 官网 — 日志格式 Chapter 6: Layouts 2022-06-26 15:35:37
*logback MDC
Logback官网 — MDC traceId 日志追踪 Chapter 8: Mapped Diagnostic Context 2022-06-26 15:36:17
*三大组件
2022-06-26 12:35:55 补图片
1、appender,输出源,一个日志可以后好几个输出源
2、encoder,一个appender有一个encoder,负责将一个event事件转换成一组byte数组,并将转换后的字节数据输出到文件中。
Encoder负责把事件转换为字节数组,并把字节数组写到合适的输出流。
因此,encoder可以控制在什么时候、把什么样的字节数组写入到其拥有者维护的输出流中。Encoder接口有两个实现类,LayoutWrappingEncoder与PatternLayoutEncoder。
注意:在logback 0.9.19 版之前没有 encoder。
在之前的版本里,多数 appender 依靠 layout 来把事件转换成字符串并用java.io.Writer 把字符串输出。在之前的版本里,用户需要在FileAppender里嵌入一个 PatternLayout。
3、layout,格式化数据将event事件转化为字符串,解析的过程
4、filter 过滤器
1 | LevelFilter levelFilter = new LevelFilter(); |
1 | 1. %-5level |
能看到logback的格式化信息
1 |
|
OutputStreamAppender
1 | protected void subAppend(E event) { |
1 | public byte[] encode(E event) { |
1 | private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) { |
logback配置
Let us begin by discussing the initialization steps that logback follows to try to configure itself:
- Logback tries to find a file called logback-test.xml in the classpath.
- If no such file is found, logback tries to find a file called logback.groovy in the classpath.
- If no such file is found, it checks for the file logback.xml in the classpath.
- If no such file is found, service-provider loading facility (introduced in JDK 1.6) is used to resolve the implementation of com.qos.logback.classic.spi.Configurator interface by looking up the file
META-INF\services\ch.qos.logback.classic.spi.Configurator
in the class path. Its contents should specify the fully qualified class name of the desiredConfigurator
implementation. - If none of the above succeeds, logback configures itself automatically using the BasicConfigurator which will cause logging output to be directed to the console.
2、基本配置信息
1 |
|
3、FileAppender配置
1 |
|
4、RollingFileAppender配置
1 |
|
5、Filter和异步日志配置
1 |
|
logback-access的使用
在server.xml里的标签下加上
1 | <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="common" resolveHosts="false"/> |
就可以了,下面咱们逐一分析各个参数。
To use logback-access with Tomcat, after downloading the logback distribution, place the files logback-core-1.3.0-alpha10.jar and logback-access-1.3.0-alpha10.jar under $TOMCAT_HOME/lib/ directory, where $TOMCAT_HOME is the folder where you have installed Tomcat.
logback-access模块与Servlet容器(如Tomcat和Jetty)集成,以提供HTTP访问日志功能。我们可以使 用logback-access模块来替换tomcat的访问日志。
- 将logback-access.jar与logback-core.jar复制到$TOMCAT_HOME/lib/目录下
- 修改$TOMCAT_HOME/conf/server.xml中的Host元素中添加:
1
<Valve className="ch.qos.logback.access.tomcat.LogbackValve"/>
- logback默认会在$TOMCAT_HOME/conf下查找文件 logback-access.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<configuration>
<!-- always a good activate OnConsoleStatusListener -->
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener"/>
<property name="LOG_DIR" value="${catalina.base}/logs"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/access.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>access.%d{yyyy-MM- dd}.log.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<!-- 访问日志的格式 -->
<pattern>combined</pattern>
</encoder>
</appender>
<appender-ref ref="FILE"/>
</configuration>
1 | %h %l %u %user %date "%r" %s %b |
官方配置:https://logback.qos.ch/access.html#configuration
log4j2的使用
Apache Log4j2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:
1、异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异常处理机制。
2、性能提升, log4j2相较于log4j 和logback都具有很明显的性能提升,后面会有官方测试的数据。
3、自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产 上可以动态的修改日志的级别而不需要重启应用。
官网: https://logging.apache.org/log4j/2.x/
Log4j2入门
目前已经有三个门面了,其实不管是哪里都是江湖,都想写一个门面,一统江湖,所以log42出了提供日志实现以外,也拥有一套自己的独立的门面。
目前市面上最主流的日志门面就是SLF4J,虽然Log4j2也是日志门面,因为它的日志实现功能非常强大,性能优越。所以大家一般还是将Log4j2看作是日志的实现,Slf4j + Log4j2应该是未来的大势所趋。
使用log4j-api做门面
(1)添加依赖
1 | <!-- Log4j2 门面API--> |
(2)JAVA代码
1 | public class TestLog4j2 { |
结果:
使用slf4j做门面
使用slf4j作为日志的门面,使用log4j2作为日志的实现。
1 | <!-- Log4j2 门面API--> |
1 | private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(TestLog4j2.class); |
结果:
我们看到log4j2的默认日志级别好像是error。
Log4j2配置
DefaultConfiguration类中提供的默认配置将设置,通过debug可以在LoggerContext类中发现
1 | private volatile Configuration configuration = new DefaultConfiguration(); |
可以看到默认的root日志的layout
我们也能看到他的日志级别:
我们能从默认配置类中看到一些默认的配置:
1 | protected void setToDefault() { |
自定义配置文件位置
log4j2默认在classpath下查找配置文件,可以修改配置文件的位置。在非web项目中:
1 | public static void main(String[] args) throws IOException { |
如果是web项目,在web.xml中添加
1 | <context-param> |
log4j2默认加载classpath下的 log4j2.xml 文件中的配置。事实上log4j2可以通过 XML、JSON、YAML 或properties格式进行配置:https://logging.apache.org/log4j/2.x/manual/configuration.html
如果找不到配置文件,Log4j 将提供默认配置。
1 | DefaultConfiguration 类中提供的默认配置将设置: |
1 | private void reconfigure(final URI configURI) { |
ConfigurationFactory
1 | for (final ConfigurationFactory factory : getFactories()) { |
1 |
|
注意根节点增加了一个monitorInterval属性,含义是每隔300秒重新读取配置文件,可以不重启应用的情况下修改配置,还是很好用的功能。
RollingRandomAccessFile的属性:
- fileName 指定当前日志文件的位置和文件名称
- filePattern 指定当发生Rolling时,文件的转移和重命名规则
- SizeBasedTriggeringPolicy 指定当文件体积大于size指定的值时,触发Rolling
- DefaultRolloverStrategy 指定最多保存的文件个数
- TimeBasedTriggeringPolicy 这个配置需要和filePattern结合使用,
- 注意filePattern中配置的文件重命名规则是${FILE_NAME}-%d{yyyy-MM-dd HH-mm}-%i,最小的时间粒度是mm,即分钟。
- TimeBasedTriggeringPolicy指定的size是1,结合起来就是每1分钟生成一个新文件。如果改成%d{yyyy-MM-dd HH},最小粒度为小时,则每一个小时生成一个文件。
Log4j2异步日志
异步日志
log4j2最大的特点就是异步日志,其性能的提升主要也是从异步日志中受益,我们来看看如何使用 log4j2的异步日志。
同步日志
异步日志
Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger,分别对应 前面我们说的Appender组件和Logger组件。
注意:配置异步日志需要添加依赖
1 | <!--异步日志依赖--> |
1、AsyncAppender方式
1 |
|
2、AsyncLogger方式
AsyncLogger才是log4j2 的重头戏,也是官方推荐的异步方式。它可以使得调用Logger.log返回的 更快。你可以有两种选择:全局异步和混合异步。
全局异步就是,所有的日志都异步的记录,在配置文件上不用做任何改动,只需要添加一个 log4j2.component.properties 配置;
1 | Log4jContextSelector=org.apache.logging.log4j.core.asyn c.AsyncLoggerContextSelector |
混合异步就是,你可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活。
1 |
|
如上配置: com.taopanfeng 日志是异步的,root日志是同步的。
使用异步日志需要注意的问题:
- 如果使用异步日志,AsyncAppender、AsyncLogger和全局日志,不要同时出现。性能会和 AsyncAppender一致,降至最低。
- 设置includeLocation=false ,打印位置信息会急剧降低异步日志的性能,比同步日志还要慢。
1
2
3
4
5for (int i = 0; i < 100_0000; i++) {
LOGGER.fatal("fatal");
}
long end = System.currentTimeMillis();
System.out.println(end - start); //2970
Log4j2的性能
log4j官网对其性能进行大肆宣扬,但是网上也有专业认识进行测试,log4j在大量日志的情况下有一定的优势,他确实是日后的选择。但是也不必纠结。
Log4j – Performance (apache.org)
怎么打日志
基本格式
必须使用参数化信息的方式:
1 | logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol); |
不要进行字符串拼接,那样会产生很多String对象,占用空间,影响性能。
反例(不要这么做):
1 | logger.debug("Processing trade with id: " + id + " symbol: " + symbol); |
使用[]
进行参数变量隔离,如有参数变量,应该写成如下写法:
1 | logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol); |
这样的格式写法,可读性更好,对于排查问题更有帮助。不同级别的使用
ERROR,影响到程序正常运行、当前请求正常运行的异常情况:
- 打开配置文件失败
- 所有第三方对接的异常(包括第三方返回错误码)
- 所有影响功能使用的异常,包括:SQLException和除了业务异常之外的所有异常(RuntimeException和Exception)
- 不应该出现的情况,比如要使用阿里云传图片,但是未响应
- 如果有Throwable信息,需要记录完成的堆栈信息:
1
log.error("获取用户[{}]的用户信息时出错",userName,e);
说明,如果进行了抛出异常操作,请不要记录error日志,由最终处理方进行处理:
反例(不要这么做):
1 | try { |
WARN,不应该出现但是不影响程序、当前请求正常运行的异常情况:
- 有容错机制的时候出现的错误情况
- 找不到配置文件,但是系统能自动创建配置文件
- 即将接近临界值的时候,例如:缓存池占用达到警告线,业务异常的记录,比如:用户锁定异常
INFO,系统运行信息
- Service方法中对于系统/业务状态的变更
- 主要逻辑中的分步骤:1,初始化什么 2、加载什么
- 外部接口部分
- 客户端请求参数(REST/WS)
- 调用第三方时的调用参数和调用结果
- 对于复杂的业务逻辑,需要进行日志打点,以及埋点记录,比如电商系统中的下订单逻辑,以及OrderAction操作(业务状态变更)。
- 调用其他第三方服务时,所有的出参和入参是必须要记录的(因为你很难追溯第三方模块发生的问题)
说明并不是所有的service都进行出入口打点记录,单一、简单service是没有意义的(job除外,job需要记录开始和结束,)。
反例(不要这么做):
1 | public List listByBaseType(Integer baseTypeId) { |
DEBUG,可以填写所有的想知道的相关信息(但不代表可以随便写,debug信息要有意义,最好有相关参数)
生产环境需要关闭DEBUG信息(如果在生产情况下需要开启DEBUG,需要使用开关进行管理,不能一直开启)。
说明
如果代码中出现以下代码,可以进行优化:
//1. 获取用户基本薪资
//2. 获取用户休假情况
//3. 计算用户应得薪资
1 | logger.debug("开始获取员工[{}] [{}]年基本薪 资", employee, year); |
TRACE,特别详细的系统运行完成信息,业务代码中,不要使用.(除非有特殊用意,否则请使用DEBUG级别替代)
规范示例说明
1 |
|