slf4j+logback搭建超实用的日志管理模块
文章转自http://www.2cto.com/kf/201702/536097.html
slf4j+logback搭建超实用的日志管理模块(对日志有编号管理):日志功能在服务器端再常见不过了,我们非常有必要记录下发生在服务器上的活动,这些日志将用于debug、统计等各种用途。
slf4j+logback这种实现方式是很常见的,好处自然是方便!。在这篇文章中,你将看到如何使用logback搭建你自己的日志组件并将日志输出到文件、如何查看这些文件、如何为每个线程上的访问分配独有的一个日志id。
基本概念和准备工作
首先,介绍几个相关的基本概念:
- slf4j:全拼为Simple Logging Facade for Java,即“为java提供的简单日志门面”,slf4j并不是一个具体的日志解决方案,实际上,它提供的核心api只是一个名为Logger的接口(里面封装了你可能需要的各种日志方法)和一个名为LoggerFactory工厂类。这个slf4j其实就是外观模式里的那个Facade,它使得你不用太过纠结于某个具体的日志框架,而是只要调用slf4j里的接口就行了。并且slf4j的性能几乎是零消耗的,毕竟它不是什么具体的东西。slf4j同时具有外观模式带来的各种好处,比如在logback和log4j这些子系统间方便切换。
- logback:logback是log4j创始人设计的另一个开源日志组件。logback被设计成了原生支持slf4j,也就是说调用slf4j接口时,似乎会自动调用logback,而不需要你做任何配置。我并不是很清楚是不是真的这样,也没研究logback和slf4j具体是如何做到这点的,但这绝对是外观模式的成功运用。以上内容我没有做过认真考究,时间关系不研究了,有见解的同学欢迎下面指正。
接下来,你当然需要导入相关的包,我项目里的几个包是这个:
- slf4j-api-1.7.21.jar
- logback-classic-1.1.7.jar
- logback-core-1.1.7.jar
应该是这三个包就够了,反正如果不全,你自己去maven上下吧,也不麻烦。
配置logback
下面就是配置logback了,请在classpath下创建一个logback.xml(如果是springMVC,跟dispatcher-servlet.xml一个目录就行了),然后配置如下:
1 <!--?xml version="1.0" encoding="UTF-8"?--> 2 3 <!-- 4 scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。 5 scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。 6 debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 7 --> 8 <configuration debug="false" scan="true" scanperiod="1800 seconds"> 9 10 <!-- 定义日志的根目录,日志文件将会在运行服务端的机器上的这个路径上被创建 --> 11 <property name="log.shop" value="/data/logs/tomcat/shop"> 12 13 <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 --> 14 15 16 <!-- 这里关闭谨慎模式,如果开启谨慎模式,可以使多个JVM中运行的 17 多个FileAppender实例,安全的写入到同一个日志文件。 --> 18 <prudent>false</prudent> 19 20 <!-- 基于时间的文件滚动 --> 21 <rollingpolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> 22 23 <!-- 文件命名规则,这么写就是按天滚动了,每天一个日志文件 --> 24 <filenamepattern> 25 ${log.shop}.%d{yyyy-MM-dd}.log 26 </filenamepattern> 27 28 <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动, 29 且maxHistory是720,则只保存最近720天的文件,删除之前的旧文件。注意,删除旧文件是, 30 那些为了归档而创建的目录也会被删除。 --> 31 <maxhistory>720</maxhistory> 32 </rollingpolicy> 33 34 <!-- 配置日志的输出格式,里面这些占位符具体什么意思,参见文末的参考资料 --> 35 <layout class="ch.qos.logback.classic.PatternLayout"> 36 <pattern> 37 [%p][%d{yyyy-MM-dd HH:mm:ssS}][%c#%M]:%L-%m%n 38 </pattern> 39 </layout> 40 </appender> 41 42 <!-- 任何一个类都只会对应一个logger,要不是某个具体的logger,要不就是root,root其实 43 类似于是logger的父类。并且一个类获取logger时,默认就是获取root,这里把前面配好的名为 44 logfile的Appender配置给root --> 45 <root level="info"> 46 47 </appender-ref></root> 48 49 <!-- 这么配置的话,com.cry.test下的logger会走这个,并且输出级别为info,而不在这个 50 包下的会走上面的root --> 51 <logger level="info" name="com.cry.test"> 52 53 <!-- 这个logger就能看出logback在企业应用中巨大的灵活性,我们可能不关注spring框架产生的 54 各种各样的info日志,但还是非常关心warn级别日志的 --> 55 <logger level="warn" service="org.springframework"> 56 57 </logger></logger></property></configuration>
使用
现在可以开心的打印日志了。
比如说你想在某个类里打印日志到文件,只需要声明为一个成员变量如下:
1 //Logger和LoggerFactory都是slf4j提供的 2 private static final Logger log = 3 LoggerFactory.getLogger(AccountController.class);
然后在任意地方调用:
log.info("Deep dark fantasy!");
好了,日志就被成功记录了,你将在运行服务端的机器上,你指定的位置,看到日志文件:
打开文件,你将看到这样一行:
如果服务器布置在其它机器上,比如常见的情况是部署在某台linux服务器上,那我们使用SecureCRT之类的工具连接到命令窗口上,也可以很方便的找到日志文件(或者通过grep找到),并且使用vim之类的工具查看。
现在,恭喜你成功地配置好了日志组件,并打出了日志!
新需求!按线程编号的日志!
对于企业级的应用,这样的需求是相当常见的:
对每个请求中的日志进行编号,比如以springMVC为例,在Controller的某一个URL处理方法上,接收参数直到返回视图之间的所有操作,我们希望都具有同样的标识,比如打印出来的日志有同样的LogId。
因为比如你这个url接口上调用的某个方法出了问题,那我们就很想知道发生问题的前因后果是什么样的,并且springMVC的设计就是一个请求一个线程,同一个LogId就意味着同一个客户端的同一次请求,可以借此追踪该线程的行为,这对于debug或者维护显然是非常有用的,非常实用!
那么思考一下,怎么实现呢?每个线程都有自己的一个属性:LogId,我们还需要在这个线程方法栈中涉及到的各个方法都获取到这个属性并打印,比如像下面这种情况:
1 @RequestMapping(value = "/get") 2 @ResponseBody 3 public ResultBean<t> get(Param model) { 4 5 VO result = manager.findVO(model); 6 7 //LogUtil用于按规则拼接出要打印的日志字符串 8 log.info(LogUtil.logResult(model, result)); 9 10 return ResultBean.success(result); 11 }
中间还会跳到manager.findVO方法里,但它们是同一个线程调用的,所以想让它们有同样的LogId,该怎么办呢?生成一个LogId然后传参数到findVO里?未免太丑太糟糕了。那么怎么做能更优雅一些,更易读易维护呢?
解决方案:ThreadLocal+拦截器!
前两天刚写过一篇分析ThreadLocal的文章(戳此链接查看),觉得ThreadLocal用在这里不是正合适吗?我们希望每个线程都有一个属于该线程的数据:LogId,这个数据应该每个线程各有一个副本,不能共享,而且最好管理方便,不要方法之间传来传去的。那么ThreadLocal简直完美解决了这个问题!如果把LogId存在各个线程自己的“线程本地存储”里,那就可以轻松实现这个需求了。
然而,怎么在一个线程开始的时候分配给它LogId呢?使用springMVC提供的拦截器岂不可以开心的实现!前两天也是刚好写过分析拦截器的文章(戳此链接查看)。
那么我们创建一个拦截器,比如可以叫作LogInceptor:
1 /** 2 * 日志处理拦截器 3 * 4 */ 5 public class LogInterceptor implements HandlerInterceptor { 6 7 @Override 8 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception { 9 LogUtil.logRequest(); 10 11 return true; 12 } 13 14 @Override 15 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {} 16 17 @Override 18 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {} 19 }
preHandle里永远返回true,那也就意味着这只是想做做手脚,不是要把request拦下来。我们只是想在请求到来之时,生成一个LogId。
来看看LogUtil.logRequest()怎么写:
1 /** 2 * 记录请求日志 3 * 4 * @param request 5 */ 6 public static void logRequest() { 7 8 //生成一个logId,怎么生成随你便,这里就是搞个大点的随机数 9 String logId = CommonUtil.getRandomNum(12); 10 11 //把这个logId存进线程本地数据副本里 12 LocalMap.set(LOG_ID, logId); 13 }
注意LogUtil是单例模式,不然每次搞出来一个就搞笑了。上面的LocalMap是创建的另一个类,用于保管每个线程的信息(毕竟logId应该只是线程里的多个数据之一)。我的意思是,每个线程都有一个本地数据副本,这个例子里我就让每个线程的本地数据是一个Map,键值对“(LOG_ID, logId)”就保存了这个线程的logId信息。
LocalMap可以这么实现(代码非原创,来自JiaYY):
1 public class LocalMap { 2 3 private static final ThreadLocal<map<string, object="">> CONTEXT = new ThreadLocal<>(); 4 5 private LocalMap() { 6 7 } 8 9 /** 10 * 向当前线程暂存数据 11 * 12 * @param key 13 * @param value 14 */ 15 public static void set(String key, Object value) { 16 Map<string, object=""> map = contextMap(); 17 if(map != null) { 18 map.put(key, value); 19 } 20 } 21 22 /** 23 * 从当前线程获取暂存数据 24 * 25 * @param key 26 * @return 27 */ 28 public static String get(String key) { 29 String value = ""; 30 Map<string, object=""> map = contextMap(); 31 if(map != null && map.get(key) != null) { 32 value = String.valueOf(map.get(key)); 33 } 34 return value; 35 } 36 37 /** 38 * 资源释放 39 */ 40 public static void destroy() { 41 Map<string, object=""> map = contextMap(); 42 if (map != null) { 43 if (!map.isEmpty()) { 44 map.clear(); 45 } 46 } 47 CONTEXT.remove(); 48 } 49 50 /** 51 * 获取当前线程里暂存的数据 52 * @return 53 */ 54 public static Map<string, object=""> contextMap() { 55 Map<string, object=""> map = CONTEXT.get(); 56 if (map == null) { 57 map = new HashMap<>(); 58 CONTEXT.set(map); 59 } 60 return map; 61 } 62 63 }
get/set方法差不多就是这么实现,我们可爱的ThreadLocal对象也是在这被实例化的,注意这个类也是单例的,并且虽然这个类是单例的,但每个访问它的线程将会get/set到自己的数据副本(原理见我分析ThreadLocal那篇文章)。然后ThreadLocal的泛型在这里就是一个Map,也就是前面我说的“每个线程有一个自己的Map”。
然后在LogUtil里写一个获取logId的方法:
1 /** 2 * 获取请求日志id 3 * @return 4 */ 5 private static String getLogId() { 6 return LocalMap.get(LOG_ID); 7 }
现在你只要调用这个方法就可以开心的在任何地方获取到当前线程的logId,我们的目的达到了。当然如果你在LogUtil里实现更多方便的方法,那就会用得更开心
最后一点小问题
如果你仔细看了上面的代码,可能会担心每次线程进来都产生一个数据副本,多了会OOM,那这个担心是没必要的。如果你仔细研究过ThreadLocal源码就会发现,线程数据副本是存在于Thread类中的,而非存在于ThreadLocal,所以当线程结束,它的数据副本(作为该线程的内部变量)自然也被释放了,其实并不会多到OOM。
但是实际上,tomcat维护了线程池,一些线程是被重复使用的, 并不会被销毁,这样的话当你取出线程的时候,说不定会发现线程里保存着之前的ThreadLocalMap,这就意味着你要么覆盖掉之前的内容,要么在每次请求结束的时候,清空该线程的数据副本。前者效率略高一点,也不用多写什么;后者效率略低一点,还需要你在拦截器afterCompletion里自己实现一个清理方法。
前者实现出来的话,其tomcat线程池占用的空间会稍大一些;后者则会小一点。但是后者还有其它的好处,比如对于一个很大的项目来说能预防未知的问题,毕竟谁也不知道别人会对这代码干什么,会不会出现你暂时没想到的BUG,会不会某一天每个线程都需要比较大的数据副本,保险一点总没坏处。
我觉得这就像ACM题里,拿一个数组循环处理每次测试,你可以每次用之前清空一下,也可以直接拿来用,反正能覆盖,但谁知道万一某道题不能覆盖了呢?万一需求又变了呢?这个问题就仁者见仁,智者见智了。