线上的系统出现了bug,可能是请求的数据出现了问题,这个时候,日志就为我们提供了解决问题的办法。但是线上的产品系统,一般的优先级都在INFO之上,如果修日日志级别,获取丰富的信息,可能需要重启服务,对线上的影响比较大。如何能做到 动态的修改日志的级别,而且不用重启服务,对线上环境的影响减少到最小呢?Log4jConfigListener就上场了
之前就听说有这么个功能,一直没有用上,这次线上产品出现了bug了,就趁这个机会使用下。
Log4jConfigListener在spring-web中,需要添加maven的依赖,在pom中添加
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency>
在web.xml中配置
<context-param> <param-name>log4jConfigLocation</param-name> <param-value>classpath:log4j.xml</param-value> </context-param> <context-param> <param-name>log4jRefreshInterval</param-name> <param-value>60000</param-value> </context-param> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> </listener>
这样几配置好了,可以部署到服务器上去了。平时根据项目的需求配置日志的输出级别,如果想动态修改日志级别,只需要修改log4j.xml就可以了。
那么,Log4jConfigListener做了什么,可以知道文件变化了并加以应用,难道是起了个线程来做的?
让我们看看源码吧,首先看下Log4jConfigListener
public class Log4jConfigListener implements ServletContextListener { public void contextInitialized(ServletContextEvent event) { Log4jWebConfigurer.initLogging(event.getServletContext()); } public void contextDestroyed(ServletContextEvent event) { Log4jWebConfigurer.shutdownLogging(event.getServletContext()); } }
这里Log4jConfigListener使用了Log4jWebConfigure,让我们继续
public static void initLogging(ServletContext servletContext) { // Expose the web app root system property. if (exposeWebAppRoot(servletContext)) { WebUtils.setWebAppRootSystemProperty(servletContext); } // Only perform custom log4j initialization in case of a config file. String location = servletContext.getInitParameter(CONFIG_LOCATION_PARAM); if (location != null) { // Perform actual log4j initialization; else rely on log4j's default initialization. try { // Resolve system property placeholders before potentially // resolving a real path. location = SystemPropertyUtils.resolvePlaceholders(location); // Leave a URL (e.g. "classpath:" or "file:") as-is. if (!ResourceUtils.isUrl(location)) { // Consider a plain file path as relative to the web // application root directory. location = WebUtils.getRealPath(servletContext, location); } // Write log message to server log. servletContext.log("Initializing log4j from [" + location + "]"); // Check whether refresh interval was specified. String intervalString = servletContext.getInitParameter(REFRESH_INTERVAL_PARAM); if (intervalString != null) { // Initialize with refresh interval, i.e. with log4j's watchdog thread, // checking the file in the background. try { long refreshInterval = Long.parseLong(intervalString); Log4jConfigurer.initLogging(location, refreshInterval); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid 'log4jRefreshInterval' parameter: " + ex.getMessage()); } } else { // Initialize without refresh check, i.e. without log4j's watchdog thread. Log4jConfigurer.initLogging(location); } } catch (FileNotFoundException ex) { throw new IllegalArgumentException("Invalid 'log4jConfigLocation' parameter: " + ex.getMessage()); } } }
这里有几行代码需要是重点,
String intervalString = servletContext.getInitParameter(REFRESH_INTERVAL_PARAM); Log4jConfigurer.initLogging(location, refreshInterval);
那Log4jConfigure.initLogging有干了啥呢?
public static void initLogging(String location, long refreshInterval) throws FileNotFoundException { String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location); File file = ResourceUtils.getFile(resolvedLocation); if (!file.exists()) { throw new FileNotFoundException("Log4j config file [" + resolvedLocation + "] not found"); } if (resolvedLocation.toLowerCase().endsWith(XML_FILE_EXTENSION)) { DOMConfigurator.configureAndWatch(file.getAbsolutePath(), refreshInterval); } else { PropertyConfigurator.configureAndWatch(file.getAbsolutePath(), refreshInterval); } }
获取配置文件,根据log4配置文件的格式(xml,properties)方式进行加载xml,那么一定是在DOMConfigurator.configureAndWatch 或者PropertyConfigurator.configureAndWatch里面有个线程在做幕后工作,由于LZ
采用的是XML格式的配置文件,那就看下DOMConfigurator.configureAndWatch,看看它到底怎么实现的吧。
static public void configureAndWatch(String configFilename, long delay) { XMLWatchdog xdog = new XMLWatchdog(configFilename); xdog.setDelay(delay); xdog.start(); }
XMLWatchdog,这是个WatchDog,哈哈,有啥动静,自然躲不过watchDog的眼睛,还有start方法,看起来应该是Thread类,让我们看看WatchDog的真面目吧。
class XMLWatchdog extends FileWatchdog { XMLWatchdog(String filename) { super(filename); } /** Call {@link DOMConfigurator#configure(String)} with the <code>filename</code> to reconfigure log4j. */ public void doOnChange() { new DOMConfigurator().doConfigure(filename, LogManager.getLoggerRepository()); } }
FileWatchDog
public abstract class FileWatchdog extends Thread{ ....... abstract protected void doOnChange(); public void run() { while(!interrupted) { try { Thread.sleep(delay); } catch(InterruptedException e) { // no interruption expected } checkAndConfigure(); } } protected void checkAndConfigure() { ........ if(fileExists) { long l = file.lastModified(); // this can also throw a SecurityException if(l > lastModif) { // however, if we reached this point this lastModif = l; // is very unlikely. doOnChange(); warnedAlready = false; } } else { if(!warnedAlready) { LogLog.debug("["+filename+"] does not exist."); warnedAlready = true; } } } }
FileWatchDog有个抽象方法,doOnChange,就是对文件变化后的响应,抽象方法的定义,为子类的扩展提供了可能。
我们看到,Log4jConfirgureListener也就是通过线程的方式扫描log4j.xml,当发现log4j的配置文件发生变化后就作出响应,从而做到了不重启应用修改日志的输出级别。
通过阅读源码,我们更清楚的知道web.xml中的配置参数
log4jRefreshInterval的时间单位是MS
如果你有空,不妨阅读下源码,这样更有收获。
由于本人水平有限,如果不对的地方或需要补充的地方,请您指出。如果您是大牛,可以忽略本文。
谢谢