近期,考虑到Job系统需要打很多的日志(调用别人的API真的是坑呀,一不留神别人就把你限流了,还是别人内部服务器的问题,所以还是要多打印些日志)。笔者想优化下日志系统,然后成功又把自己拐坑里了。不过也是因为自己对于日志系统不够充分了解,借此机会更好的了解日志系统。
优化的手段比较简单:
- 生产环境去除打印控制台日志
- 将RollingFile改为RollingRandomAccessFile
- 添加Disruptor的Jar,开启Log4j2的异步化
ps:优化2是借用RollingRandomAccessFile中的buffer来提升IO效率,在开启Log4j2的异步化之后,笔者选择将RollingRandomAccessFile改回RollingFile。另外,异步化也不是十全十美的,在停止服务的时候会损失一部分日志的。如果能在停止服务的时候Sleep一段时间那是最好的,本项目对于一部分日志不在意所以也就没加Sleep。
步骤1:剔除Log4j1
既然需要用到Log4J2,那么就需要将所有的Log4j1的Jar给剔除。
mvn dependency:tree --> tree.txt
[INFO] | +- org.apache.hadoop:hadoop-hdfs:jar:2.5.1:compile
[INFO] | | +- log4j:log4j:jar:1.2.17:compile
把依赖树打印出来,发现hdfs的Jar下有Log41,找准Log4j1,使用:
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
结果发现没有生效……哔了狗了。这个Jar是包含在Biz层的pom中,索性我直接把Biz的Log4j1给剔除了。
世界应该安静了吧,结果报错了。
Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Level
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 66 more
不应该呀网上都说日志系统都是使用common-log或者slf4j,除非是直接使用Log41的代码~ 看代码hdfs也是用的common-log呀。查了两天发现hdfs使用了hdfs的INFO级别的常量…………所以,Log4j1不能删除,坑爹呀。
步骤2:隔离Log4j1
行吧,既然Hdfs你是大爷,你要用就用呗。但是打印日志如何嫁接到Log4j2上面,这个就是我要做的工作了。万能的google告诉我添加jcl-over-slf4j的Jar就可以解决问题,jcl-over-slf4j能够将commom-log嫁接到slf4j上,然后添加log4j-slf4j-impl:jar:2.7的Jar就能使用Log4j2。
这就陷入了我的知识盲区,为何添加Jar就能成功切换日志系统?
解决方案:万事不懂先google看别人原理讲解,然后在自己乖乖看源码。
Commom-log原理
commom-log:
- 从System.properties中查找LogFactory
- 从SPI来找
- 使用默认的LogFactoryImpl
为了方便大家理解,笔者把代码给抠了出来,还把常量给替换了方便看,可自行在common-log 1.2中搜索找到。
第一步,从System.properties中查找LogFactory
String factoryClass = getSystemProperty("org.apache.commons.logging.LogFactory", null);
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
第二步,从SPI来找
final InputStream is = getResourceAsStream(contextClassLoader, "META-INF/services/org.apache.commons.logging.LogFactory");
...
...
factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
ps:使用Java 1.6提供的java.util.ServiceLoader也是不错的选择,dubbo即其他很多框架都是SPI的最好实践者。
第三步,使用默认的LogFactoryImpl
factory = newFactory("org.apache.commons.logging.impl.LogFactoryImpl", thisClassLoader, contextClassLoader);
以下是每个会去寻找的Logger:
private static final String[] classesToDiscover = {
LOGGING_IMPL_LOG4J_LOGGER,
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};
Slf4j原理
从classPath中查找,将StaticLoggerBinder绑定到 Enumeration paths 上。
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources("org/slf4j/impl/StaticLoggerBinder.class");
} else {
paths = loggerFactoryClassLoader.getResources("org/slf4j/impl/StaticLoggerBinder.class");
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
可以看到log4j-slf4j-impl:jar:2.7中就有StaticLoggerBinder.class这个类。
总结
可以看到Commom-log和Slf4j为我们提供了两种做插件化服务很好的思路。Common-log使用接口(SPI的模式)或者是Slf4j使用门面模式(StaticLoggerBinder.class)来实现插件化,都是不错的选择。
最后,推荐一波Log4j2的异步化。
性能测试可看: