读
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第一篇。
序
写这篇文章的初衷,是想在团队内做一次Java日志的分享,因为日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式。但在准备分享、补充细节的过程中,我又进一步发现目前日志相关的文章,都只是专注于某一个方面,或者讲历史和原理,或者解决包冲突,却都没有把整个Java日志知识串联起来。最终这篇文章超越了之前的定位,越写越丰富,为了让大家看得不累,我的文章将以系列的形式展示。
一、前言
日志发展到今天,被抽象成了三层:接口层、实现层、适配层:
适配层又可以分为绑定(Binding)和桥接(Bridging)两种能力:
如果你觉得上面的描述比较抽象生硬,可以先跳过,等把本篇看完自然就明白了。
接下来我们就以时间顺序,回顾一下Java日志的发展史,这有助于指导我们后续的实践,真正做到知其所以然。
二、历史演进
2.1 标准输出 (<1999)
Java最开始并没有专门记录日志的工具,大家都是用System.out和System.err输出日志。但它们只是简单的信息输出,无法区分错误级别、无法控制输出粒度,也没有什么管理、过滤能力。随着Java工程化的深入,它们的能力就有些捉襟见肘了。
虽然System.out和System.err默认输出到控制台,但它们是有能力将输出保存到文件的:
System.setOut(new PrintStream(new FileOutputStream("log.txt", true)));
System.out.println("这句将输出到 log.txt 文件中");
System.setErr(new PrintStream(new FileOutputStream("error.txt", true)));
System.err.println("这句将输出到 error.txt 文件中");
2.2 Log4j (1999)
在1996年,一家名为SEMPER的欧洲公司决定开发一款用于记录日志的工具。经过多次迭代,最终发展成为Log4j。这款工具的主要作者是一位名叫Ceki Gülcü[2]的俄罗斯程序员,请记住他的名字:Ceki,后面还会多次提到他。
到了1999年,Log4j已经被广泛使用,随着用户规模的增长,用户诉求也开始多样化。于是Ceki在2001年选择将Log4j开源,希望借助社区的力量将Log4j发展壮大。不久之后Apache基金会向Log4j抛出了橄榄枝,自然Ceki也加入Apache继续从事 Log4j的开发,从此Log4j改名Apache Log4j[3]并进入发展的快车道。
Log4j相比于System.out提供了更强大的能力,甚至很多思想到现在仍被广泛接受,比如:
随着Log4j的成功,Apache又孵化了Log4Net[4]、Log4cxx[5]、Log4php[6]产品,开源社区也模仿推出了如Log4c[7]、Log4cpp[8]、Log4perl[9]等众多项目。从中也可以印证Log4j在日志处理领域的江湖影响力。
不过Log4j有比较明显的性能短板,在Logback和Log4j 2推出后逐渐式微,最终Apache在2015年宣布终止开发Log4j并全面迁移至Log4j 2[10](可参考【2.7 Log4j 2 (2012)】)。
2.3 JUL (2002.2)
随着Java工程的发展,Sun也意识到日志记录非常重要,认为这个能力应该由JRE原生支持。所以在1999年Sun提交了JSR 047[11]提案,标题就叫「Logging API Specification」。不过直到2年后的2002年,Java官方的日志系统才随Java 1.4发布。这套系统称做Java Logging API,包路径是java.util.logging,简称JUL。
在某些追溯历史的文章中提到,「Apache曾希望将 Log4j加入到JRE中作为默认日志实现,但傲慢的Sun没有答应,反而很快推出了自己的日志系统」。对于这个说法我并没有找到出处,无法确认其真实性。
不过从实际推出的产品来看,更晚面世的JUL无论是功能还是性能都落后于Log4j,颇有因被寄予厚望而仓促发布的味道,也许那个八卦并非空穴来风,哈哈。虽然在2004年推出的Java 5.0 (1.5) [12]上JUL进步不小,但它在Log4j面前仍无太多亮点,广大开发者并没有迁移的动力,导致JUL始终未成气候。
我们在后文没有推荐JUL的计划,所以这里也不多介绍了(主要是我也不会)。
2.4 JCL (2002.8)
在Log4j和JUL之外,当时市面上还有像Apache Avalon[13](一套服务端开发框架)、 Lumberjack[14](一套跑在JDK 1.2/1.3上的开源日志工具)等日志工具。
对于独立且轻量的项目来说,开发者可以根据喜好使用某个日志方案即可。但更多情况是一套业务系统依赖了大量的三方工具,而众多三方工具会各自使用不同的日志实现,当它们被集成在一起时,必然导致日志记录混乱。
为此Apache在2002年推出了一套接口Jakarta Commons Logging[15],简称 JCL,它的主要作者仍然是Ceki。这套接口主动支持了Log4j、JUL、Apache Avalon、Lumberjack等众多日志工具。开发者如果想打印日志,只需调用JCL的接口即可,至于最终使用的日志实现则由最上层的业务系统决定。我们可以看到,这其实就是典型的接口与实现分离设计。
但因为是先有的实现(Log4j、JUL)后有的接口(JCL),所以JCL配套提供了接口与实现的适配层(没有使用它的最新版,原因会在【1.2.7 Log4j2 (2012)】提到):
简单介绍一下JCL自带的几个适配层/实现层:
当时项目前缀取名Jakarta,是因为它属于Apache与Sun共同推出的Jakarta Project[16]项目(邮件[17])。现在JCL作为Apache Commons[18]的子项目,叫 Apache Commons Logging,与我们常用的Commons Lang[19]、Commons Collections [20]等是师兄弟。但JCL的简写命名被保留了下来,并没有改为ACL。
2.5 Slf4j (2005)
Log4j的作者Ceki看到了很多Log4j和JCL的不足,但又无力推动项目快速迭代,加上对Apache的管理不满,认为自己失去了对Log4j项目的控制权(博客[21]、邮件[22]),于是在2005年选择自立门户,并很快推出了一款新作品Simple Logging Facade for Java[23],简称Slf4j。
Slf4j也是一个接口层,接口设计与JCL非常接近(毕竟有师承关系)。相比JCL有一个重要的区别是日志实现层的绑定方式:JCL是动态绑定,即在运行时执行日志记录时判定合适的日志实现;而Slf4j选择的是静态绑定,应用编译时已经确定日志实现,性能自然更好。这就是常被提到的classloader问题,更详细地讨论可以参考What is the issue with the runtime discovery algorithm of Apache Commons Logging[24]以及Ceki自己写的文章Taxonomy of class loader problems encountered when using Jakarta Commons Logging[25]。
在推出Slf4j的时候,市面上已经有了另一套接口层JCL,为了将选择权交给用户(我猜也为了挖JCL的墙角),Slf4j推出了两个桥接层:
Slf4j通过推出各种适配层,基本满足了用户的所有场景,我们来看一下它的全家桶:
网上介绍Slf4j的文章,经常会引用它官网上的两张图:
感兴趣的同学也可以参考。
这里解释一下slf4j-log4j12这个名字,它表示Slf4j + Log4j 1.2(Log4j的最后一个版本) 的适配层。类似的,slf4j-jdk14表示Slf4j + JDK 1.4(就是 JUL)的适配层。
2.6 Logback (2006)
然而Ceki的目标并不止于Slf4j,面对自己一手创造的Log4j,作为原作者自然是知道它存在哪些问题的。于是在2006年Ceki又推出了一款日志记录实现方案:Logback[26]。无论是易用度、功能、还是性能,Logback 都要优于Log4j,再加上天然支持Slf4j而不需要额外的适配层,自然拥趸者众。目前Logback已经成为Java社区最被广泛接受的日志实现层(Logback自己在2021年的统计是48%的市占率[27])。
相比于Log4j,Logback提供了很多我们现在看起来理所当然的新特性:
Logback主要由三部分组成(网上各种文章在介绍classic和access时都描述的语焉不详,我不得不直接翻官网文档找更明确的解释):
2.7 Log4j 2 (2012)
看着Slf4j + Logback搞的风生水起,Apache自然不会坐视不理,终于在2012年憋出一记大招:Apache Log4j 2[29],它自然也有不少亮点:
Log4j 2主要由两部分组成:
你会发现Log4j 2的设计别具一格,提供JCL和Slf4j之外的第三个接口层(log4j-api,虽然只是自己的接口),它在官网API Separation[33]一节中解释说,这样设计可以允许用户在一个项目中同时使用不同的接口层与实现层。
不过目前大家一般把Log4j 2作为实现层看待,并引入JCL或Slf4j作为接口层。特别是JCL,在时隔近十年后,于2023年底推出了1.3.0 版[34],增加了针对Log4j 2的适配。还记得我们在【1.2.4 JCL (2002.8)】中没有用最新版的JCL做介绍吗,就是因为这个十年之后的版本把那些已经「作古」的日志适配层@Deprecated掉了。
多说一句,其实Logback和Slf4j就像log4j-core和log4j-api的关系一下,目前如果你想用Logback也只能借助Slf4j。但谁让它们生逢其时呢,大家就会分别讨论认为是两个产品。
虽然Log4j 2发布至今已有十年(本文写于2024年),但它仍然无法撼动Logback的江湖地位,我个人总结下来主要有两点:
比如,曾有人建议Spring Boot将日志系统从Logback切换到Log4j2[35],但被Phil Webb[36](Spring Boot核心贡献者)否决。他在回复中给出的原因包括:Spring Boot需要保证向前兼容以方便用户升级,而切换Log4j 2是破坏性的;目前绝大部分用户并未面临日志性能问题,Log4j 2所推崇的性能优势并非框架与用户的核心关切;以及如果用户想在Spring Boot中切换到Log4j 2也很方便(如需切换可参考 官方文档[37])。
2.8 spring-jcl (2017)
因为目前大部分应用都基于Spring/Spring Boot搭建,所以我额外介绍一下spring-jcl [38]这个包,目前Spring Boot用的就是spring-jcl + Logback这套方案。
Spring曾在它的官方Blog《Logging Dependencies in Spring》[39]中提到,如果可以重来,Spring会选择李白Slf4j而不是JCL作为默认日志接口。
现在Spring又想支持Slf4j,又要保证向前兼容以支持JCL,于是从5.0(Spring Boot 2.0)开始提供了spring-jcl这个包。它顶着Spring的名号,代码中包名却与JCL 一致(org.apache.commons.logging),作用自然也与JCL一致,但它额外适配了Slf4j,并将Slf4j放在查找的第一顺位,从而做到了「既要又要」(你可以回到【1.2.4 JCL (2002.8)】节做一下对比)。
如果你是基于Spring Initialize [40]新创建的应用,可以不必管这个包,它已经在背后默默工作了;如果你在项目开发过程中遇到包冲突,或者需要自己选择日志接口和实现,则可以把spring-jcl当作JCL对待,大胆排除即可。
2.9 其他
除了我们上边提到的日志解决方案,还有一些不那么常见的,比如:
因为这些日志框架我们在实际开发中用的很少,此文也不再赘述了(主要是我也不会)。
三、总结
历史介绍完了,但故事并没有结束。两个接口(JCL、Slf4j)四个实现(Log4j、JUL、Logback、Log4j2),再加上无数的适配层,它们之间串联成了一个网,我专门画了一张图:
解释/补充一下这张图:
如果你之前在看「1.1 前言」时觉得过于抽象,那么此时建议你再回头看一下,相信会有更多体会。
从这段历史,我也发现了几个有趣的细节:
参考链接:
[1]https://codedocs.org/what-is/david-wheeler-computer-scientist
[2]https://github.com/ceki
[3]https://logging.apache.org/log4j/1.2/
[4]https://logging.apache.org/log4net/
[5]https://logging.apache.org/log4cxx/
[6]https://logging.apache.org/log4php/
[7]https://log4c.sourceforge.net/
[8]https://log4cpp.sourceforge.net/
[9]https://mschilli.github.io/log4perl/
[10]https://news.apache.org/foundation/entry/apache_logging_services_project_announces
[11]https://jcp.org/en/jsr/detail
[12]https://www.java.com/releases/
[13]https://avalon.apache.org/
[14]https://javalogging.sourceforge.net/
[15]https://commons.apache.org/proper/commons-logging/
[16]https://jakarta.apache.org/
[17]https://lists.apache.org/thread/53otcqljjfnvjs3hv8m4ldzlgz59yk6k
[18]https://commons.apache.org/
[19]https://commons.apache.org/proper/commons-lang/
[20]https://commons.apache.org/proper/commons-collections/
[21]http://ceki.blogspot.com/2010/05/forces-and-vulnerabilites-of-apache.html
[22]https://lists.apache.org/thread/dyzmtholjdlf3h32vvl85so8sbj3v0qz
[23]https://www.slf4j.org/
[24]https://stackoverflow.com/questions/3222895/what-is-the-issue-with-the-runtime-discovery-algorithm-of-apache-commons-logging
[25]https://articles.qos.ch/classloader.html
[26]https://logback.qos.ch/
[27]https://qos.ch/
[28]https://logback.qos.ch/access.html
[29]https://logging.apache.org/log4j/2.x/
[30]https://logging.apache.org/log4j/2.x/manual/extending.html
[31]https://logging.apache.org/log4j/2.x/manual/async.html
[32]https://logging.apache.org/log4j/2.x/performance.html
[33]https://logging.apache.org/log4j/2.x/manual/api-separation.html
[34]https://commons.apache.org/proper/commons-logging/changes-report.html
[35]https://github.com/spring-projects/spring-boot/issues/16864
[36]https://spring.io/team/philwebb
[37]https://docs.spring.io/spring-boot/docs/3.2.x/reference/html/howto.html
[38]https://docs.spring.io/spring-framework/reference/core/spring-jcl.html
[39]https://spring.io/blog/2009/12/04/logging-dependencies-in-spring
[40]https://start.spring.io/
[41]https://google.github.io/flogger/
[42]https://github.com/jboss-logging
[43]https://reload4j.qos.ch/
[44]https://github.com/torvalds
[45]https://moolenaar.net/
作者:尚左
来源-微信公众号:阿里云开发者
出处:https://mp.weixin.qq.com/s/eIiu08fVk194E0BgGL5gow
Postfix有数百种配置选项。 我概述了/etc/postfix/main.cf文件中的一些常见问题。
用于出站邮件的域名
将此选项设置为出站邮件所需的域名。 默认情况下,此选项使用出站电子邮件的主机名。 该选项还指定附加到非限定收件人地址的域。
#default value is: #myorigin=$myhostname #myorigin=$mydomain #we are going to send outbound mail as originating from example.com mydomain=example.com myorigin=$mydomain
此选项指定Postfix接收电子邮件的域名。 默认情况下,Postfix仅接收主机名的电子邮件。 对于域邮件服务器,请更改该值以包含域名。
#default value mydestination=$myhostname, localhost.$mydomain, localhost #we are going to change it so that we can receive email for example.com mydestination=$myhostname, localhost.$mydomain, localhost, $mydomain
默认情况下,Postfix允许Postfix服务器的本地子网上的客户端将其用作中继 - 换句话说,即$ mynetworks配置参数中定义的那些网络。 更改此项以包括组织内应允许使用此Postfix服务器发送电子邮件的所有网络。
#default value #mynetworks_style=class #mynetworks_style=subnet #mynetworks_style=host #mynetworks=168.100.189.0/28, 127.0.0.0/8 #mynetworks=$config_directory/mynetworks #mynetworks=hash:/etc/postfix/network_table #change the default values to be your network, assuming your network is 10.0.0.0/8 mynetworks=10.0.0.0/8 #another way of accomplishing the above is: mynetworks_style=class #if you want to forward e-mail only from the Postfix host: mynetworks_style=host #if you want only the Postfix server subnet to forward e-mail via the Postfix server: mynetworks_style=subnet
当邮件来自授权网络之外的客户端时,Postfix仅将电子邮件转发给授权域。 您可以使用relay_domains参数指定哪些域可以是未经身份验证的发件人的收件人域。
#default value is: #relay_domains=$mydestination #if you do not want to forward e-mail from strangers, then change it as follows (recommended for outgoing mail servers, not for incoming): relay_domains=#if you want to forward e-mail from strangers to your domain: relay_domains=$mydomain
Postfix使用收件人的邮件交换器(MX)记录直接发送邮件。 您可能不需要此功能,因为转发到过滤出站邮件的外部邮件托管提供商可能更好。
#default value is: #relayhost=$mydomain #relayhost=[gateway.my.domain] #relayhost=[mailserver.isp.tld] #relayhost=uucphost #relayhost=[an.ip.add.ress] #change the value to be relayhost=external.mail.provider.ip.address
您可能想要定义的另一个值是在出现任何问题时向谁发送电子邮件。 postmaster电子邮件地址在/ etc / aliases中指定,而不是在/etc/postfix/main.cf中指定。
#default value is: $ grep -i postmaster /etc/aliases mailer-daemon: postmaster postmaster: root #change to an e-mail address in your org that is an alias for the team responsible for Postfix postmaster: email-admins@example.com
如果Postfix服务器使用NAT(换句话说,它在私有IP空间中),并且它正在接收公共IP地址的电子邮件地址,则还需要指定它。
#default value is: #proxy_interfaces=#proxy_interfaces=1.2.3.4 #change it to your external IP address to which e-mail is sent proxy_interfaces=your.public.email.server.ip.address
Postfix日志记录通过/etc/rsyslog.conf中的Syslog完成。 默认情况下,通常会将所有邮件日志发送到/ var / log / maillog,这是存储邮件日志的好地方。
#default value is: mail.* -/var/log/maillog
设置Postfix后,下一个要解决的问题是如何让客户端访问电子邮件。 在客户端访问方面,一些流行的协议包括
通常,避免直接传递到组织中的目标服务器,以保持简单; 相反,应用程序服务器从邮箱服务器中提取电子邮件。 应用程序服务器可以使用一个或多个协议,例如IMAP或POP。 您也可以让最终用户使用这些协议来访问其邮箱,而不是应用程序服务器。 IMAP在RFC 3501(http://tools.ietf.org/html/rfc3501)中定义,并且具有比POP更多的功能。 POP在RFC 1939(https://www.ietf.org/rfc/rfc1939.txt)中定义,并且已经存在了很长时间。
如果您有需要提取电子邮件的应用程序服务器,可以提供帮助的潜在设计如图4-12所示。 邮件传递代理(MDA)和邮件提交代理(MSA)都可以是Postfix服务器。 MDA是将邮件发送到邮箱的代理; MSA接受来自邮件用户代理的电子邮件。
For the MSA, there are numerous options, including
这些MSA选项中的每一个都是开源和免费的。 它们都支持数千个邮箱,可以帮助您轻松管理电子邮件环境。
Postfix支持有很多选项。 在线文档非常好,与任何开源免费软件一样,用户社区是您阅读文档后获得帮助的最佳选择。 一些在线帮助选项包括:
版本控制在企业基础结构中有许多用途。 传统方法是使用修订控制来进行源代码管理。 但是,修订控制也可以在基础设施管理中发挥重要作用。 例如,如果在环境中使用BIND,则BIND配置可以存储在Git中。 Postfix和其他软件(如OpenVPN,iptables和Hadoop)等软件的配置也可以存储在版本控制中。 Puppet和Salt等配置管理工具也可以轻松地与Git集成。
大量的开源软件源代码存储在Git中。 在企业中使用Git的一个优点是与Internet社区的协作变得更加容易。 一些使用Git的主要开源项目包括:
A more complete list is found at https://git.wiki.kernel.org/index.php/GitProjects.
ython的list是一种非常灵活的数据结构,它允许存储任意类型的有序集合,包括数字、字符串、甚至是其他的list,其强大的特性为Python编程提供了很大的便利。在这个文档中,我们将介绍Python list的基础知识,涵盖了list的定义、创建、访问、操作等方面的内容。
在Python中,可以使用方括号[]来创建一个空的list,例如:
Copy codea=[]
也可以使用方括号[]初始化一个包含元素的list,例如:
Copy codeb=[1, 2, 3, 4, 5]
c=['apple', 'banana', 'orange']
d=[1, 'hello', 3.14]
list的元素可以是任何类型,而且list中的元素可以是不同的类型,这是它非常灵活的地方。
Python的list可以使用索引来访问其中的元素,索引从0开始,例如:
Copy codea=[1, 2, 3, 4, 5]
print(a[0]) # 1
print(a[2]) # 3
list还可以使用负数索引来访问其中的元素,例如:
Copy codeprint(a[-1]) # 5
print(a[-3]) # 3
这样,我们可以更加方便地访问list中的元素。
向list中添加元素有两种方式:使用append()方法添加,或使用+运算符连接两个list。例如:
Copy codea=['apple', 'banana', 'orange']
a.append('watermelon')
print(a) # ['apple', 'banana', 'orange', 'watermelon']
b=['pear', 'grape']
c=a + b
print(c) # ['apple', 'banana', 'orange', 'watermelon', 'pear', 'grape']
可以使用del语句或remove()方法从list中删除元素:
Copy codea=['apple', 'banana', 'orange']
del a[0]
print(a) # ['banana', 'orange']
a.remove('banana')
print(a) # ['orange']
我们可以使用切片来访问list的一个子集合。切片使用[start:stop:step]的格式,例如:
Copy codea=[1, 2, 3, 4, 5]
print(a[1:3]) # [2, 3]
print(a[:3]) # [1, 2, 3]
print(a[2:]) # [3, 4, 5]
print(a[::2]) # [1, 3, 5]
Python提供了两种方法对list进行排序:sort()方法和sorted()函数。sort()方法会改变原list,而sorted()函数返回一个新的已排序的list。例如:
Copy codea=[3, 1, 4, 2, 5]
a.sort()
print(a) # [1, 2, 3, 4, 5]
b=[3, 1, 4, 2, 5]
c=sorted(b)
print(b) # [3, 1, 4, 2, 5]
print(c) # [1, 2, 3, 4, 5]
以下是一些有用的学习资源,帮助你学习Python list:
总结
Python的list是一种非常灵活的数据结构,它可以存储任何类型的元素,包括数字、字符串、甚至是其他的list。Python list提供了丰富的操作方法,包括访问元素、添加元素、删除元素、切片、排序等。我们希望通过这篇文档提供了Python list的基础知识和一些有用的学习资源,帮助你更好地掌握Python编程中的list。
*请认真填写需求信息,我们会在24小时内与您取得联系。