一、背景
程序读取外部log4j2.xml配置文件方式为启动命令添加了--logging.config=/path/log4j2.xml,因系统安全整改,将/var/log/目录改为了700,程序使用非root启动时log4j2报错无法在/var/log目录下创建日志文件。经排查发现jar包的classpath目录下指定的日志文件位置在/var/log下,但是外部配置文件指定的日志文件在该系统用户下具有创建文件权限。按之前的理解通过logging.config指定了外部配置文件后jar包内的配置文件会失效,那为什么程序还是去读取了jar包内的配置文件导致在没有权限的目录下尝试创建日志文件呢。
二、问题排查
修改jar包内的配置文件,将日志输出路径改为该系统用户可以创建文件的/opt/log目录,修改外部配置文件,将日志输出路径改为该系统用户可以创建文件的/opt/log2目录。启动程序后发现,在/opt/log以及/opt/log2目录下都生成了日志文件,但是/opt/log目录下的日志文件并不会产生日志,只有/opt/log2目录下的日志文件(也就是外部配置文件指定的目录)会产生日志,且外部配置文件可以控制程序的日志级别。带着这个问题查看springboot加载log4j2并读取log4j2.xml过程的源码。
三、SpringBoot下Log4j2加载配置文件优先级。
1.初次加载
springboot程序启动的类初始化阶段会触发log4j2初次加载配置文件,即不受springboot配置的影响,一定会加载的,其加载顺序如下。(这里只说明在不同目录下加载log4j2.xml配置文件的顺序,不说明其他后缀的配置文件加载顺序)
-Dlog4j.configurationFile=/path/log4j2.xml > jar包内classpath下log4j2.xml
参数说明:
-D:添加在启动命令的虚拟机参数位置,代表设置虚拟机环境变量。必须使用该方式。
特别说明一下在背景中提到的--logging.config参数,它是配置在java启动命令的程序参数位置,也就是会通过main方法传递过来的,一般springboot的一些配置是通过这种形式传递的,例如:--spring.config.location配置指定springboot的配置文件位置,那么为什么不能通过--log4j.configuration的方式配置log4j2的配置文件位置呢,因为他的加载是在类的初始化阶段进行的,此时spring容器还尚未加载完成,且log4j源码中也体现了是从jvm环境变量中读取的该配置项。
详细的java启动参数的说明可以参考一下文章:
https://www.cnblogs.com/haycheng/p/12781261.html
2.配置重加载
log4j2的配置文件重加载发生在springboot的监听器中,它触发log4j2重加载配置文件,是加载logging.config配置的。
3.结论
通过以上两点就解释了问题排查中讲到的程序即加载了jar包内的log4j2.xml也加载了--logging.config指定的log4j2.xml,那么我们在springboot中使用log4j2且想指定外部配置文件时最好使用-Dlog4j.configurationFile的这种方式,让程序在初次加载时就指定外部配置文件,这样程序就不会加载jar包内的配置文件并创建对应的日志文件了(虽然也不往其中输出日志,但目录和文件毕竟会创建,当程序权限不足时就会报错)。
四、SpringBoot启动加载log4j2.xml过程源码解读
1.首次加载
SpringApplication类中定义了静态的Log对象,因此在加载SpringApplication类时会触发Log类的加载及初始化操作,而不是等到执行run方法,这也就是为什么说log4j2首次加载配置文件时spring容器还未加载完成。
springboot使用jcl(Jakarta Commons Logging)日志门面,其中LogFactory为jcl的包下的类,其通过spring-jcl引入。
进入LogAdapter#createLog,createLog为Function类型,查看何时给其赋值。
在LogAdapter的静态代码块中通过几个Present判断为createLog复制,可以看出这里为选择一种具体的日志实现的适配器。
查看这几个Present如何来的。通过查看不同日志类型的类是否可以被加载来判断程序使用哪种日志框架,该案例中选择的是log4j 2.x + slf4j的实现。
进入createLog定义的Function,即Log4jAdapter::createLog,Log4jAdapter为LogAdapter的内部类。
调用Log4jLog类的构造方法,Log4jLog为LogAdapter的内部类。其中LogManager为log4j-api包内的类,其通过spi机制找到org.apache.logging.log4j.core.impl.Log4jProvider