前段时间,公司里组织了一次代码检查,其中有一条检查项让我有些费解:
理由是大量的不输出的日志对性能会有影响(日志中存在字符串拼接)。如果说只是DEBUG的加上,我也就认了,可是在系统中写成INFO的日志如果不输出,那还写它干嘛,我就是想看到关键路径的日志。而且在大多数日志上加上这么一个判断真的很难看。。。
所谓上有政策,下有对策,于是有人开始写一些包装了判断的辅助类,当中用这样的代码(截取):
public class LogUtil { public static void info(Logger logger, String message) { if (logger.isInfoEnabled()) { logger.info(message); } } public static void debug(Logger logger, String message) { if (logger.isDebugEnabled()) { logger.debug(message); } } public static void debug(Logger logger, String message, Throwable throwable) { if (logger.isDebugEnabled()) { logger.debug(message, throwable); } } }
但这样的代码真的就能解决问题了吗?答案是“不能”!字符串拼接还存在。
那让我们分两个部分来看一下这个问题:
关于第一个问题,String的拼接是不修改原来的字符串的,而是创建一个新的String对象,道理上是这么说的,我们也该这么理解。但Java编译器并不傻,实际情况是怎么样的呢,它会做些优化。请看如下代码:
public class StringAddDemo { public static void main(String[] args) { String a = "abc"; String b = a + "def"; System.out.println(b + "ghi"); } }
在编译后,通过javap -c StringAddDemo看看结果:
实际这里的拼接用的是StringBuilder.append。情况没想的这么糟糕,而且1000次字符串拼接的开销都比不上一次远程调用的开销大,与其想着从这里挤性能,还不如去优化远程调用和数据库访问。
第二点,同样用实验来做说明,这里对比了使用辅助类,使用isInfoEnabled判断和不使用判断的情况,日志级别为INFO,另外再加了使用辅助类,使用isDebugEnabled判断和不使用判断的情况,都是循环输出20万次,每次的拼接在最后都加了些运算:
public class LogDemo { private static final Logger logger = LoggerFactory.getLogger(LogDemo.class); public static void main(String[] args) { Profiler.start(); Profiler.enter("Ignore"); for (int i = 0; i < 200000; i++) { LogUtil.info(logger, "test" + "test" + i + i*2); } Profiler.release(); Profiler.enter("InfoUtil"); for (int i = 0; i < 200000; i++) { LogUtil.info(logger, "test1" + "test2" + i + i*2); } Profiler.release(); Profiler.enter("isInfoEnabled"); for (int i = 0; i < 200000; i++) { if (logger.isInfoEnabled()) { logger.info("test3" + "test4" + i + i*2); } } Profiler.release(); Profiler.enter("info"); for (int i = 0; i < 200000; i++) { logger.info("test5" + "test6" + i + i*2); } Profiler.release(); Profiler.enter("DebugUtil"); for (int i = 0; i < 200000; i++) { LogUtil.debug(logger, "test7" + "test8" + i + i*2); } Profiler.release(); Profiler.enter("isDebugEnabled"); for (int i = 0; i < 200000; i++) { if (logger.isDebugEnabled()) { logger.debug("test9" + "test0" + i + i*2); } } Profiler.release(); Profiler.enter("debug"); for (int i = 0; i < 200000; i++) { logger.debug("tes1" + "tes2" + i + i*2); } Profiler.release(); Profiler.release(); System.out.println(Profiler.dump()); } }
( 说明:这里的Profiler类是个工具类,作用是记录调用的时间;代码有截取;第一个循环主要是预热一下,不在统计范围内)
经过了几次测试,结果如下:
(数据说明:第一列为开始计时的时间点,[]内为时间及统计,15,171ms为该阶段的具体耗时,后面的百分比是该阶段耗时在这个统计内所占的百分比)
对上述数据分析后,可以得到这样的结论:
针对第3条再做些补充说明,日志输出大的开销应该在IO上,计算应该不会很多,也不该很多,如果存在大量的运算请自己考虑下是不是有问题;既然是确认要输出的日志,那增加判断其实是种浪费,虽然这种判断的开销可以忽略。
综上所述,个人建议在日常系统中无需对日志增加isInfoEnabled判断,想通过这种处理来优化效果的作用不会很明显,还是把精力从日志移到数据库和远程调用上效果更好些。 (特殊情况下,如果在日志中有复杂的操作,可以酌情考虑,但个人不倾向于复杂的日志)