您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
打日志的优雅身姿
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
打日志的优雅身姿
自猿其说Tech
2021-10-19
IP归属:未知
52480浏览
计算机编程
### 1 前言 我是JK,跟你说哦,千万不要小看打日志! 那一天阳光很好,咖啡还加了糖,幸福的JK在听着慢歌,刚拉取了一下项目,突然就遭劫了。本地测试竟然打不了日志,排查出来是日志的文件配置有迷之修改,更多的情况是引用第三方库出现了问题。 卑微的JK无奈咯,排除依赖,好多带着log关键字的依赖,我该排啷个咧?修改配置文件,咋么又没有文件记录了?在不知道第几次修改Bug后,终于下定决心要把日志总结一下,之后慢慢整理了本文,接下来就跟你好好聊聊日志这个小东西。 ### 2 历史 现如今市面上的日志很是纷杂,但其实是有各自出现的顺序的,不知道为神马,我了解之后就是一场不见血的硝烟战争: - 首先是1996年出现的Log4j(Log for java)版本1.xx,其中一个主要贡献者是Ceki Gülcü,最后该项目归属于Apache基金会(阿帕奇一统日志界!) - 2002年二月,Sun推出了模仿Log4j的JUL(Java Util Logging),作为JDK自带的日志库跟前者同台竞技。(我大太阳必须拥有姓名!) - 同年八月份,阿帕奇又将之前出现的两个实现进行统一,推出了门面Jakarta Commons Logging,把JCL作为一个日志接口,算是第一个较有影响力的日志门面,默认实现的Simple Log无需多说,存在感极低,上面的两个实现有谁就用谁(阿帕奇再度一统日志界!) - 2006年,日志界的传奇Ceki Gülcü不在阿帕奇打工了,创建了新公司。新公司要有拳头产品啊,于是推出了日志门面Slf4j(Simple Logging Facade for Java)直接就跟阿帕奇的JCL争市场了。(日志界归我一个人支配),但是很遗憾,作为新一代的日志门面,要兼容旧项目的门面(此时只有JCL一个)。没有实现,开发者没有办法使用啊。于是传奇人物又说,没事我给你们写桥接包,用那俩公司的实现就行!(我才是站在金字塔最顶端的王者!) - 同年,仍然是日志界的传奇Ceki Gülcü,或许是写烦了桥接包,直接推出一个Logback作为Slf4j的直接实现,只需要在项目中引用这两个包就可以优雅地打日志了!(我一个人就能扛的起来整个日志界!) - 2012年,阿帕奇在和传奇人物争夺市场五六年之后,实在是看不下去了,就推出了一个全新项目即Log4j版本为2.xx,也就是本文的主角出场了!为了兼容之前的日志实现,也出了很多的桥接包。(来啊,看看谁的抛瓦更多!) 于是,日志界的三国战记拉开帷幕。哈哈,纯属戏言。 所以,截止到现在,共有三个日志门面,按照出现的顺序依次为JCL(Jakarta Commons Logging),Slf4j(Simple Logging Facade for Java)和Log4j2(Log4j版本为2.xx)。四个日志实现,按照出现的顺序依次为Log4j1(Log for java版本1.xx),JUL(Java Util Logging),LogBack和Log4j2(Log for java版本2.xx)。 为了让在旧项目中使用新的门面,出现了从一个日志门面桥接到另一个日志门面的桥接包。类似log4j-slf4j-impl这种,可以在一个项目中同时使用Log4j2的规范和Slf4j的规范去打印日志。为了让日志门面直接使用其他日志实现,又出现了日志适配包,目的是通过改变接口来达到重复使用的目的。类似slf4j-log4j12这种,可以在一个项目中用Slf4j的规范来打印日志。 ### 3 现实 简单了解了历史之后,就到了重点,我们要如何在项目中优雅地打印日志呢?简单来说就两点,第一就是选择并且引入一套可以共存且有效打印日志的套组,第二就是在项目中配置和使用了。 #### 3.1 选择依赖 首先,选择一个日志门面,它是对实际项目如何去使用日志框架制定的一种规范、标准、接口,但并不能独立使用。现存的三大日志门面中,我仍然选择推荐的是中兴之主Slf4j,原因很简单,用顺手了。 ```xml <!--对外Api包,是一种日志框架,单独的话缺少实现无法应用--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> ``` 其次,选择一个日志实现,它是一种记录日志的具体实现。在这个时候就要关心性能了,后浪把前浪拍死在沙滩上是历史的必然!Log4j2作为本文的主角,是对JUL的替代,是对Log4j的改进,吸取了Logback改进经验的同时,还解决了Logback体系结构中的一些固有问题,无论怎么说在现存的四大日志实现中必然是选择他了啊! 但是这还没有结束,真正性能的优势是体现在了异步打印日志的时候,所以还需要加上一个异步依赖disruptor!强强联合,不留余地!(猛得握拳) ```xml <!--实现包log4j-core是对log4j2的具体实现--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14.0</version> <!--依赖可选,默认false,是会将该依赖传递到引用包的,设为true就不会传递依赖了--> <optional>true</optional> <!--在编写的过程中不会用到实现类的api--> <scope>runtime</scope> </dependency> <!--异步依赖,需要使用log4j2的AsyncLogger需要包含disruptor--> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.4.2</version> </dependency> ``` - optional设置为true是为了依赖不会被传递,防止其他人引用了本项目,就会被迫使用不想用的日志依赖。 - scope设置为runtime,是为了在实际研发的过程中不会引用到他的api。 最后,用桥接器,将日志框架的API桥接到日志实现的API上面,使得项目可以直接打印日志。既然选择了门面和实现,那么桥接器的选择就呼之而出了!没错,就是你了,log4j-slf4j-impl出来吧!(丢出精灵球) ```xml <!--桥接包log4j-slf4j-impl起到适配的作用,版本须对应log4j-core的版本--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.14.0</version> </dependency> ``` 有调皮的小伙伴就想提问了,这个转接包里面就包含了日志实现和依赖啊!你再重复多引了!这样子不好,不清爽!你说的没错,这个转接包本身就已经包含了对应的日志门面和日志实现,再搭配上一个异步依赖就可以用这两个依赖替换掉这四个依赖,程序可以跑起来,没问题! 在这个时候就有必要说一下,我们选择依赖的基础目标是为了在项目中引用需要的API内容,实际需求只要整个项目中存在所需要的依赖就可以了。故而,其实引用包的数量并没有优劣之分,只要不发生版本冲突都是没问题的。 如果一个项目中,有直接引用和间接引用同一个包的不同版本,那么是哪个生效呢?首先遵循短路优先的原则。如直接写明在pom.xml文件中的,优先级高于间接引用的。其次遵循声明顺序的原则,在pom.xml文件中同时写明了两个版本的依赖,那么以最先声明的优先级更高。 #### 3.2 配置Log4j2 因为我们最终选择的是以Log4j2为最终的实现类,所以我们必须要针对这个实现类进行配置。 Log4j2日志有八个日志级别,从小到大依次为:all、trace、debug、info、warn、error、fatal和off,级别越大,输出的日志信息越少,all就是全部打印,off就是完全不打印日志。 配置文件的扫描顺序,就是在项目的resources文件夹下查看是否存在: - log4j2.configurationFile - log4j2-test.properties - log4j2-test.yaml or log4j2-test.yml - log4j2-test.json or log4j2-test.jsn - log4j2-test.xml - log4j2.properties - log4j2.yaml or log4j2.yml - log4j2.json or log4j2.jsn - log4j2.xml 都没找到,就会用默认配置只输出到控制台。 ##### 3.2.1 异步配置 Log4j2的一个大特点就在于他的异步日志,性能提升也是主要从异步日志中得到收益。而异步日志是在主线程中完成了部分的过滤和内容生成,剩下的过滤,格式化和输出都有专属的线程去实现。Logger是负责具体的生产日志数据组装报文,Appender则是负责将数据运到终点,如控制台,文件等。 以下为网上搜寻了部分例子再加上JK自己准备了一点儿小配置,属于拿来就可以使用的: ```xml <?xml version="1.0" encoding="UTF-8"?> <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数--> <configuration monitorInterval="5"> <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--变量配置--> <Properties> <!-- 格式化输出:%date表示日期;%thread表示线程名;%-5level:级别从左显示5个字符宽度 ;%logger{36} 表示 Logger 名字最长36个字符;%msg:日志消息,%n是换行符--> <!--控制台打印彩色日志,文件打印无色日志--> <property name="CONSOLE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level} [%t] %highlight{%c{1.}.%M(%L)}: %msg%n" /> <property name="FILE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %c{1.}.%M(%L): %msg%n" /> <!-- 定义日志存储的路径 --> <property name="PROJECT_NAME" value="keenTest-logs"/> <property name="MOUDLE_NAME" value="java-log-async"/> <property name="FILE_PATH" value="/Users/JK/export/Logs/${PROJECT_NAME}/${MOUDLE_NAME}"/> </Properties> <appenders> <!--打印日志到控制台--> <console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="${CONSOLE_PATTERN}"/> <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/> </console> <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用--> <File name="Filelog" fileName="${FILE_PATH}/tempAll.log" append="false"> <PatternLayout pattern="${FILE_PATTERN}"/> </File> <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${PROJECT_NAME}-INFO-%d{yyyy-MM-dd}-%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${FILE_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="4"/> <SizeBasedTriggeringPolicy size="500MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="20"/> </RollingFile> <!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${PROJECT_NAME}-WARN-%d{yyyy-MM-dd}-%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${FILE_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="500MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="20"/> </RollingFile> <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${PROJECT_NAME}-ERROR-%d{yyyy-MM-dd}-%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${FILE_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="4"/> <SizeBasedTriggeringPolicy size="500MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="20"/> </RollingFile> </appenders> <loggers> <!--过滤掉spring的一些无用的信息,一般不会保存在文件中--> <AsyncLogger name="org.springframework" level="info" additivity="false" includeLocation="true"> <AppenderRef ref="Console"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> </AsyncLogger > <AsyncRoot level="info" includeLocation="true"> <appender-ref ref="Console"/> <appender-ref ref="Filelog"/> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> </AsyncRoot> </loggers> </configuration> ``` 到了这里,你就成功地配置了一个异步日志,如果对每个标签的实际意义兴趣不大,可以直接跳到下文3.3的应用部分,查看如何优雅地使用日志。 但是如果是更细心的小伙伴,这时候就会忍不住想要举手说:诶?不对啊,我看过不一样的异步配置啊?其实Log4j2是有两种实现异步日志方式的: - AsyncAppender的效率很低,极其特殊的情况下才会使用 - AsyncLogger相较起来则效率更好一些,也更通用。 在上面已经告诉你如何去配置更优的方式了,为什么还要介绍这种不推荐的配置方式呢?孔子曾说见贤思齐,见不贤而内自省也。真正的经验是知道错了而且能改正,而非是说你所讲的,与我知道的不一致,我认为你就是错的。 不推荐配置AsyncAppender的方式: ```xml <?xml version="1.0" encoding="UTF-8"?> <configuration monitorInterval="5"> <Properties> 此处请参考上文配置 </Properties> <appenders> <!--打印日志到控制台--> <console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="${CONSOLE_PATTERN}"/> <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/> </console> <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用--> <File name="Filelog" fileName="${FILE_PATH}/tempAll.log" append="false"> <PatternLayout pattern="${FILE_PATTERN}"/> </File> <!--异步输出到文件中--> <Async name="Async"> <AppenderRef ref="Filelog"/> </Async> </appenders> <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效--> <loggers> <root level="debug"> <appender-ref ref="Console"/> <!-- 使用异步 appender --> <AppenderRef ref="Async"/> </root> </loggers> </configuration> ``` 这种配置方式没有错误!还是可以正常打印日志的!不是说他不好,只是,不够好。如果AsyncAppender和AsyncLogger 两者混用会如何呢?不建议这样做,因为它只是添加了另一个使用 CPU/内存的中间步骤,而没有做出任何贡献。 ##### 3.2.2 同步配置 同步日志是在主线程中完成日志过滤,日志内容生成,格式化和输出的全部过程。 下图取自网络,传送门在文末: ![](//img1.jcloudcs.com/developer.jdcloud.com/e7035815-644d-44bf-b355-76876d836e3820211019153009.png) 其实我是不建议使用同步打印日志的方式,因为对项目的影响要更大。 为了保证知识体系的完整性,将同步配置的内容附了上来: ```xml <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。--> <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效--> <loggers> <!--过滤掉spring和mybatis的一些无用信息,只是在控制台打印--> <logger name="org.mybatis" level="info" additivity="false"> <AppenderRef ref="Console"/> </logger> <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。--> <!--Logger节点用来单独指定日志的形式,name为包路径,比如要为org.springframework包下所有日志指定为INFO级别等。 --> <Logger name="org.springframework" level="info" additivity="false"> <AppenderRef ref="Console"/> </Logger> <!-- Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出 --> <root level="debug"> <appender-ref ref="Console"/> <appender-ref ref="Filelog"/> <!--只要appender标签存在就会新建文件,但是如果这里注释掉就不会记录到文件了--> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> </root> </loggers> ``` 如果选择同步配置的话,就不需要多添加一个异步的依赖了,在选择依赖的步骤中可以省去disruptor了。 ##### 3.2.3 部分属性值意义 下文是从读到的文章那里直接引用过来的,文末有对应的传送门: 根节点Configuration有两个属性:status和monitorinterval: - status:用来指定log4j本身的打印日志的级别,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出; - monitorinterval:用于指定log4j2自动重新配置的监测间隔时间,单位是s,最小是5s。 节点说明: - Properties:一般用来做属性定义; - - property是类似键值对的形式定义属性 - Appender:可以理解为一个管道,定义了日志内容的输出位置: - - Console:用来定义输出到控制台Appender; - - - name:指定Appender的名字; - - - target:SYSTEM_OUT或SYSTEM_ERR,一般只设置默认:SYSTEM_OUT; - - - PatternLayout:输出格式,不设置默认为:%m%n; - - - Filter:配置日志事件能否被输出。过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(中立); - - File:用来定义输出到指定位置的文件Appender; - - - name:指定Appender的名字; - - - fileName:指定输出日志的目的文件带全路径的文件名; - - - PatternLayout:输出格式,不设置默认为:%m%n; - - - Filter:配置日志事件能否被输出。过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(中立); - - RollingFile:用来定义超过指定大小自动删除旧的创建新文件Appender; - - - name:指定Appender的名字; - - - fileName:指定输出日志的目的文件带全路径的文件名; - - - PatternLayout:输出格式,不设置默认为:%m%n; - - - Filter:配置日志事件能否被输出。过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(中立);一般常用的有以下三种: - - - - LevelRangeFilter:输出指定日志级别范围之内的日志; - - - - TimeFilter:在指定时间范围内才会输出日志; - - - - ThresholdFilter:输出符合特定日志级别及其以上级别的日志。 - - - PatternLayout:输出格式,不设置默认为:%m%n; - - - Policies:指定滚动日志的策略,就是什么时候进行新建日志文件输出日志;一般常用的有以下三种: - - - - SizeBasedTriggeringPolicy:根据日志文件的大小进行滚动;单位有:KB,MB,GB; - - - - CronTriggeringPolicy:使用Cron表达式进行日志滚动,很灵活; - - - - TimeBasedTriggeringPolicy:这个配置需要和filePattern结合使用,注意filePattern中配置的文件重命名规则。滚动策略依赖于filePattern中配置的最具体的时间单位,根据最具体的时间单位进行滚动。这种方式比较简洁。 - - - Strategy:配置Strategy以控制日志如何(How)进行滚动。 - Loggers:简单说Logger就是一个路由器,指定类、包中的日志信息流向哪个Appender,以及控制他们的日志级别; - - Root:必须要配置;用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出; - - - level:属性;用来指定日志输出级别; - - - AppenderRef:Root的子节点,用来指定该日志输出到哪个Appender。 - - Logger:用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。 - - - level:属性;用来指定日志输出级别; - - - name:属性;用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点; - - - AppenderRef:Logger的子节点,用来指定该日志输出到哪个Appender;如果没有指定,就会默认继承自Root;如果指定了,那么会在指定的这个Appender和Root的Appender中都会输出,此时我们可以设置Logger的additivity=”false”只在自定义的Appender中进行输出。 原文中没有说跟异步日志相关的内容,于是我就又单独补充了下面这些: - Loggers:路由器标签,内部保存了多个标签 - - AsyncLogger:作用同上面讲到的Logger标签 - - - name属性:用来指定该标签所适用的包路径,类似过滤。 - - - level属性:用来指定该标签所使用的日志输出级别; - - - includeLocation属性:是否记录日志的所在方法和行号,开启的话会严重影响异步输出的性能。 - - - additivity属性:用来指定该标签是否即成根标签,如果为false则日志信息只会打印到下属的appender-ref标签中 - - - appender-ref标签: - - - - ref属性:指定日志信息流输出到哪个Appender中 - - AsyncRoot:必须配置的内容,除去被AsyncLogger过滤且配置additivity=”false”的日志信息流都要通过这里。 - - - level属性:用来指定该标签所使用的日志输出级别; - - - includeLocation属性:是否记录日志的所在方法和行号,开启的话会严重影响异步输出的性能。 - - - appender-ref标签: - - - - ref属性:指定日志信息流输出到哪个Appender中 #### 3.3 应用 平常的话就是这样子使用的,为了方便对比这里加上了引入包: ```java package xyz.clzly.keen; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class KeenMain { private static Logger logger= LoggerFactory.getLogger(KeenMain.class); public static void main(String[] args) { logger.error("error"); logger.warn("warn"); logger.info("info"); logger.debug("debug"); logger.trace("trace"); } } ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/7804363c-4b9f-4e16-82c2-8c62654a8cb520211019153836.png) 这里提供一个包装类的思路而已,小伙伴要是有自己习惯的日志包装,直接看下一小节就好啦。但是在需要打印日志的地方之前是必须要有第二行语句的存在来构造一个静态常量的,一两个文件没问题,但是在整个项目中都这样子使用,未免有些冗余了,可以进一步简化和包装的。我们可以对日志再包装出来一个类做为工具类: xyz.clzly.annotations.LoggingUtils.java文件: ```java package xyz.clzly.keen.component.log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LoggingUtils { private static Logger logger = LoggerFactory.getLogger(LoggingUtils.class); public static boolean isDebugEnabled() { return logger.isDebugEnabled(); } public static boolean isInfoEnabled() { return logger.isInfoEnabled(); } public static boolean isErrorEnabled() { return logger.isErrorEnabled(); } public static void debug(Class clazz, String msg, Object... args) { if (clazz != null) { logger = LoggerFactory.getLogger(clazz); } logger.debug(msg, args); } public static void info(Class clazz, String msg, Object... args) { if (clazz != null) { logger = LoggerFactory.getLogger(clazz); } logger.info(msg, args); } public static void error(Class clazz, String msg, Object... args) { if (clazz != null) { logger = LoggerFactory.getLogger(clazz); } logger.error(msg, args); } } ``` 于是只需要直接使用就可以了: ```java package xyz.clzly.keen; import xyz.clzly.keen.component.log.LoggingUtils; public class KeenMain { public static void main(String[] args) { LoggingUtils.info(KeenMain.class, "打印日志"); LoggingUtils.info(KeenMain.class, "【方法】{}", "main"); } } ``` 在传参的时候将想要打印的类名和信息一起传入进去就可以了,还支持传入多个参数来组装复杂信息。 ![](//img1.jcloudcs.com/developer.jdcloud.com/154a2e9c-ef18-48df-a47e-5ee97db8118520211019154009.png) 不过也是有一个小缺点的,那就是在打开includeLocation属性后,打印日志的方法名和行数固定为了工具类的方法名和行数,不过如果发生异常了在打印堆栈内容时不受此影响。 ### 4 纵横捭阖的日志世界 看完上文的你,已经跟着我简单梳理了一遍日志,并且轻松地配置了一个新项目的日志,接下来可以方便地使用了。那么本文的初衷就已经完成了! 这样子JK就可以关掉屏幕,收拾回家了么?当然不是,好奇的JK怎么可能会不尝试一下其他的日志打印方式就直接关掉本文呢?所以接下来,跟着JK一起玩玩其他日志嘞。怎么知道哪个日志门面搭配其他日志实现的时候,是要用的哪个桥接包呢?下图取自网络,传送门在文末: ![](//img1.jcloudcs.com/developer.jdcloud.com/022e7f8f-d298-481b-8ed2-1be8c35f496a20211019154138.png) #### 4.1 JCL JCL作为出世的第一种日志门面,是很让人激动的,他在当时一统日志界,用良好易读的外部接口覆盖了日志实现的复杂接口。Spring项目本身是自带commons-logging这个日志门面的。 ##### 4.1.1 依赖和应用 ```xml <!--spring 自带commons-logging--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.3.4.RELEASE</version> </dependency> ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/8f5be731-6969-45b5-8181-6b88f51a8a6e20211019154211.png) 使用的时候就是这样子,为了节省篇幅,省去了启动类: ```java package xyz.clzly.keen.service; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Component; @Component public class KeenService { private static Log logger = LogFactory.getLog(KeenService.class); public void print(){ logger.error("error"); logger.warn("warn"); logger.info("info"); //如果不配置JUL的话,下面的内容是打印不出来的 logger.debug("debug"); logger.trace("trace"); } } ``` ##### 4.1.2 日志实现JUL 在运行的时候会自动去寻找日志实现,如果没有配置Log4j1的实现,那么就会用JDK自带的日志实现JUL: ![](//img1.jcloudcs.com/developer.jdcloud.com/f0962f0b-e327-422b-8a49-38e7ec15b36a20211019154244.png) 这个JDK自带的日志实现JUL(java.util.logging)很简单,如果只是使用打印消息还行,如果是要看其他功能,就完全不够看的了。怀着好奇的心情,我又简单地了解一下JUL日志等级划分,按照优先级递减的顺序整理后: - OFF(Integer.MAX_VALUE) 关闭日志功能 - SEVERE(1000)严重的失败或错误 - WARNING(900)潜在的问题 - INFO(800)一般信息 - CONFIG(700)配置级别 - FINE(500)详细信息 - FINER(400)更详细信息 - FINEST(300)最详细信息 - ALL(Integer.MIN_VALUE)所有信息 默认配置文件是在JDK安装目录下面的jre/lib/logging.properties文件。如果不自定义是可以直接打印信息,警告和严重级别的。如果自定义配置。。。是我前面介绍的Log4j2不香么,何苦费力用他呢? 如果不要日志门面,在应用JUL的时候,是这样子: ```java package xyz.clzly.keen; import java.util.logging.Level; import java.util.logging.Logger; public class KeenMain { //创建一个记录器名为xyz.clzly.keen的日志(有层级关系,倘若给记录器名为xyz.clzly的自定义日志设置级别,也会影响到这里 public static Logger log = Logger.getLogger("xyz.clzly.keen"); public static void main(String[] args) { log.info("info"); //信息日志 log.warning("warn"); //警告日志 log.log(Level.SEVERE,"error"); //严重日志 } } ``` 哦,打日志的方法和参数都不统一,难怪会被历史的车轮碾压过去。此时JK对于使用Log4j1的日志实现的兴趣也不大了,比JUL还要多引一个包,好处是可以配置的东西更多更灵活了,但是同样是引包的话,那么Log4j2才是我的第一选择。 ##### 4.1.3 桥接实现类 倘若现在有一个旧项目,很多文件都是使用的JCL方式打印的日志,现在我们要在不修改代码的前提下,将原先的api桥接到我刚说的Slf4j和Log4j2的更优异步配置上面。 项目中只需要如下配置即可: ```xml <!--自带commons-logging--> <!--旧项目中存在的日志依赖,注意要排除日志实现Log4j1--> <!--桥接包Commons Logging桥接至slf4j--> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.25</version> </dependency> <!--log4j2 开始--> <!--此处为文章3.1的四个依赖--> <!--log4j2 结束--> ``` 注意,需要排除掉旧项目中的日志实现,当然JUL不算在内! ![](//img1.jcloudcs.com/developer.jdcloud.com/6b6f3488-46bc-454e-86bb-64ba5b61bca420211019154413.png) #### 4.2 Log4j2 Log4j2是最新一款的日志门面,相较于之前的两个日志门面,有一定的改良,在现如今也占有一定的市场。整个项目也是分成两部分,log4j-api作为日志门面,而log4j-core作为日志直接实现。 ##### 4.2.1 依赖和应用 SpringBoot项目本身就是使用的Log4j2转接到Slf4j最后使用Logback实现的,也就是说他自带了两个日志门面,一个日志实现。 ```xml <!-- spring-boot-starter自带了spring-boot-starter-logging包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.2.6.RELEASE</version> </dependency> ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/a9fe1e52-461f-4996-bb14-76b3aafb717820211019154446.png) 使用Log4j2的时候就是这样子,跟Slf4j语法区别就只在于定义日志对象的时候而已,为了节省篇幅,省去了启动类: ```java package xyz.clzly.keen.service; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; @Component public class KeenService { // 定义Log4j2的日志记录器对象 public static final Logger logger = LogManager.getLogger(KeenService.class); public void print(){ logger.error("error"); logger.warn("warn"); logger.info("info"); //如果不配置的话,下面的内容是打印不出来的 logger.debug("debug"); logger.trace("trace"); } } ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/17048d8f-c272-40ad-9b45-8ef6d46131e420211019154515.png) ##### 4.2.2 日志实现Logback Spring Boot默认的日志实现是Logback,所以在使用的时候不能够在继续沿用前文的配置内容。配置文件的加载顺序是从网上资料直接引过来的,总传送门在文末。 - 在系统配置文件System Properties中寻找是否有logback.configurationFile对应的value - 在classpath下寻找是否有logback.groovy(即logback支持groovy与xml两种配置方式) - 在classpath下寻找是否有logback-test.xml - 在classpath下寻找是否有logback.xml 直接使用配置: ```xml <?xml version="1.0" encoding="UTF-8"?> <!-- 级别从高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL --> <!-- 日志输出规则 根据当前ROOT 级别,日志输出时,级别高于root默认的级别时 会输出 --> <!-- 以下 每个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,通过filter 过滤只记录本级别的日志 --> <!-- scan 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。 --> <!-- scanPeriod 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 --> <!-- debug 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 --> <configuration scan="true" scanPeriod="60 seconds" debug="false"> <!-- 动态日志级别 --> <jmxConfigurator /> <!-- 定义日志文件 输出位置 --> <property name="log_dir" value="/export/Logs/keen-logs/keen-logback" /> <!-- 日志最大的历史 30天 --> <property name="maxHistory" value="30" /> <!-- ConsoleAppender 控制台输出日志 --> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> <!-- 设置日志输出格式 --> %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n </pattern> </encoder> <target>System.error</target> </appender> <!-- ERROR级别日志 --> <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 RollingFileAppender --> <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 过滤器,只记录ERROR级别的日志 --> <!-- 如果日志级别等于配置级别,过滤器会根据onMath 和 onMismatch接收或拒绝日志。 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <!-- 设置过滤级别 --> <level>ERROR</level> <!-- 用于配置符合过滤条件的操作 --> <onMatch>ACCEPT</onMatch> <!-- 用于配置不符合过滤条件的操作 --> <onMismatch>DENY</onMismatch> </filter> <file>${log_dir}/error/error-log.log</file> <!-- 最常用的滚动策略,它根据时间来制定滚动策略.既负责滚动也负责出发滚动 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日志输出位置 可相对、和绝对路径 --> <fileNamePattern> ${log_dir}/error/error-log.%d.%i.log </fileNamePattern> <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件假设设置每个月滚动,且<maxHistory>是6, 则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除 --> <maxHistory>${maxHistory}</maxHistory> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <!-- maxFileSize:这是活动文件的大小,默认值是10MB,本篇设置为1KB,只是为了演示 --> <maxFileSize>1KB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern> <!-- 设置日志输出格式 --> %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n </pattern> </encoder> </appender> <!-- INFO级别日志 appender --> <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log_dir}/info/%d{yyyy-MM-dd}/info-log.log</fileNamePattern> <maxHistory>${maxHistory}</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern> </encoder> </appender> <!-- DEBUG级别日志 appender --> <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>DEBUG</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log_dir}/debug/%d{yyyy-MM-dd}/debug-log.log</fileNamePattern> <maxHistory>${maxHistory}</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern> </encoder> </appender> <!-- root级别 DEBUG --> <root> <!-- 打印debug级别日志及以上级别日志 --> <level value="debug" /> <!-- 控制台输出 --> <appender-ref ref="console" /> <!-- 文件输出 --> <appender-ref ref="ERROR" /> <appender-ref ref="INFO" /> <appender-ref ref="DEBUG" /> </root> </configuration> ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/dcfdb20f-b843-4472-9fc5-78ab74db1bfd20211019154619.png) ##### 4.2.3 桥接实现类 倘若现在又有一个旧的Spring Boot项目,很多文件都是使用的Log4j2日志门面,实现层就是Logback,现在我们要在不修改代码的前提下,将原先的api桥接到最一开始介绍的log4j2的更优异步配置上面。 经过我的多番尝试,最终拿出了两个配置,这是简单一点儿的: ```xml <!-- spring-boot-starter自带了spring-boot-starter-logging包--> <!--排除日志实现和日志转接口就只剩下一个日志门面slf4j了--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.2.6.RELEASE</version> <exclusions> <!--排除日志实现--> <exclusion> <artifactId>logback-classic</artifactId> <groupId>ch.qos.logback</groupId> </exclusion> <!--排除冲突的日志转接器--> <exclusion> <artifactId>log4j-to-slf4j</artifactId> <groupId>org.apache.logging.log4j</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> <version>2.2.6.RELEASE</version> </dependency> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.4.2</version> <scope>runtime</scope> </dependency> ``` 一定要排除日志实现和日志转接器! 这个有一点儿复杂,项目中要多配置一点儿,不过此时项目就不关心是否是SpringBoot的项目了: ```xml <properties> 。。。 <log4j.version>2.14.0</log4j.version> <slf4j.version>1.7.25</slf4j.version> <log4j.disruptor.version>3.4.2</log4j.disruptor.version> </properties> <dependencies> <!-- spring-boot-starter自带了spring-boot-starter-logging包--> <!--排除日志实现和日志转接口就只剩下一个日志门面slf4j了--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.2.6.RELEASE</version> <exclusions> <!--排除日志实现--> <exclusion> <artifactId>logback-classic</artifactId> <groupId>ch.qos.logback</groupId> </exclusion> <!--排除冲突的日志转接器--> <exclusion> <artifactId>log4j-to-slf4j</artifactId> <groupId>org.apache.logging.log4j</groupId> </exclusion> </exclusions> </dependency> <!--日志套组开始--> <!--对外的Api包,是一种日志框架,单独的话缺少实现无法应用--> <!--原项目已经有Slf4j的api包留下,所以就不引用了--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j.version}</version> </dependency> <!--桥接包log4j-slf4j-impl起到适配的作用,版本须对应log4j-core的版本--> <!--因为Log4j-api和Log4j-core本来就是配对的,所以不需要再加适配器--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>${log4j.version}</version> <!--在编写的过程中不会用到实现类的api--> <scope>runtime</scope> </dependency> <!--实现包log4j-core--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j.version}</version> <!--依赖可选,默认false,是会将该依赖传递到引用包的,设为true就不会传递依赖了--> <optional>true</optional> <scope>runtime</scope> </dependency> <!--异步依赖,需要使用log4j2的AsyncLogger需要包含disruptor--> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>${log4j.disruptor.version}</version> <optional>true</optional> <scope>runtime</scope> </dependency> <!--日志套组结束--> </dependencies> ``` 注意,需要排除掉旧项目中的日志实现和日志转接器!如果发生冲突了,就用这个方式去多加尝试。 #### 4.3 依赖冲突 为什么不同桥接器和适配器同时用会发生冲突呢?因为其目的是通过改变接口来达到重复使用的目的,当接口调用成为一个闭环,就会报内存溢出的问题,倘若被检测到则会直接抛出异常。 - jcl-over-slf4j 与 slf4j-jcl 冲突 - - jcl-over-slf4j: commons-logging切换到slf4j - - slf4j-jcl : slf4j切换到commons-logging - - 如果这两者共存的话,必然造成相互委托,造成内存溢出 - log4j-over-slf4j 与 slf4j-log4j12 冲突 - - log4j-over-slf4j : log4j1切换到slf4j - - slf4j-log4j12 : slf4j切换到log4j1 - - 如果这两者共存的话,必然造成相互委托,造成内存溢出。但是log4j-over-slf4内部做了一个判断,可以防止造成内存溢出,org.apache.log4j.Log4jLoggerFactory中会抛出异常提示用户要去掉对应的jar包 - jul-to-slf4j 与 slf4j-jdk14 冲突 - - jul-to-slf4j : jdk-logging切换到slf4j - - slf4j-jdk14 : slf4j切换到jdk-logging - - 如果这两者共存的话,必然造成相互委托,造成内存溢出 - log4j-slf4j-impl 与 log4j-to-slf4j 冲突 - - log4j-slf4j-impl:slf4j切换到log4j2 - - log4j-to-slf4j:log4j2切换到slf4j - - 如果这两者共存的话,必然造成相互委托,造成内存溢出。但是在log4j-slf4j-impl这个内部做了一次校验,org.apache.logging.slf4j.Log4jLoggerFactory会抛出一场,提示冲突了。 为了理解更清晰,画了一张表格: ![](//img1.jcloudcs.com/developer.jdcloud.com/15fb6830-6335-4583-8f48-f5c4bd0a3ab920211019154941.png) ### 5 小结和感谢 好啦!JK的好奇心已经被大大的满足了!到这里的你,已经了解了日志的历史啦,轻松配置和使用Log4j2也不在话下了。还能够用一杯咖啡的时间切换项目的日志实现,很不错哦!什么桥接器冲突的小case完全不在话下咯。 如果想要复现一下相关的内容,可以参看我的日志测试项目,希望可以帮到伙伴咧。 日志项目:https://coding.jd.com/weijikuo/keenTest-logs/ - 日志框架【历史+概述】:https://blog.51cto.com/u_14613614/2494204 - Java日志系统历史从入门到崩溃:https://segmentfault.com/a/1190000021121882 - java学习笔记-日志篇:https://www.jianshu.com/p/de4e1869e1fe - Log4j2 中文文档:https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-customconfig.html - Log4j2官网配置详解:https://logging.apache.org/log4j/2.x/manual/configuration.html - log4j2 的使用【超详细图文】:https://blog.csdn.net/weixin_32265569/article/details/110723441 - slf4j、jcl、jul、log4j1、log4j2、logback大总结:https://blog.csdn.net/jybzjf/article/details/84739475 - Log4j2中AsyncLogger和AsyncAppender的区别:https://stackoverflow.com/questions/24177601/difference-between-asynclogger-and-asyncappender-in-log4j2 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:销售发展技术部-实施研发一组 魏继扩
原创文章,需联系作者,授权转载
上一篇:经典书籍需要不断被重读——每一次重读都会有新的体会
下一篇:AspectJ浅析系列(三)自定义注解
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号