很重要,但又可以忽略,但又不得不提的东西。
这里面有很多的思想,如果你有设计模式基础,你应该看着会很舒服。
这里不再是简单的使用,而是有源码的解读,会对比各种日志(门面、实现)的优缺点,不同之处吧算是了。
.
“”””””””””””””
“”” 文章待整理 “””
“”””””””””””””
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) { |