文章转自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一个目录就行了),然后配置如下:

 <!--?xml version="1.0" encoding="UTF-8"?-->

 <!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration debug="false" scan="true" scanperiod="1800 seconds"> <!-- 定义日志的根目录,日志文件将会在运行服务端的机器上的这个路径上被创建 -->
<property name="log.shop" value="/data/logs/tomcat/shop"> <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 --> <!-- 这里关闭谨慎模式,如果开启谨慎模式,可以使多个JVM中运行的
多个FileAppender实例,安全的写入到同一个日志文件。 -->
<prudent>false</prudent> <!-- 基于时间的文件滚动 -->
<rollingpolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 文件命名规则,这么写就是按天滚动了,每天一个日志文件 -->
<filenamepattern>
${log.shop}.%d{yyyy-MM-dd}.log
</filenamepattern> <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
且maxHistory是720,则只保存最近720天的文件,删除之前的旧文件。注意,删除旧文件是,
那些为了归档而创建的目录也会被删除。 -->
<maxhistory>720</maxhistory>
</rollingpolicy> <!-- 配置日志的输出格式,里面这些占位符具体什么意思,参见文末的参考资料 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
[%p][%d{yyyy-MM-dd HH:mm:ssS}][%c#%M]:%L-%m%n
</pattern>
</layout>
</appender> <!-- 任何一个类都只会对应一个logger,要不是某个具体的logger,要不就是root,root其实
类似于是logger的父类。并且一个类获取logger时,默认就是获取root,这里把前面配好的名为
logfile的Appender配置给root -->
<root level="info"> </appender-ref></root> <!-- 这么配置的话,com.cry.test下的logger会走这个,并且输出级别为info,而不在这个
包下的会走上面的root -->
<logger level="info" name="com.cry.test"> <!-- 这个logger就能看出logback在企业应用中巨大的灵活性,我们可能不关注spring框架产生的
各种各样的info日志,但还是非常关心warn级别日志的 -->
<logger level="warn" service="org.springframework"> </logger></logger></property></configuration>

使用

现在可以开心的打印日志了。

比如说你想在某个类里打印日志到文件,只需要声明为一个成员变量如下:

 //Logger和LoggerFactory都是slf4j提供的
private static final Logger log =
LoggerFactory.getLogger(AccountController.class);

然后在任意地方调用:

log.info("Deep dark fantasy!");

好了,日志就被成功记录了,你将在运行服务端的机器上,你指定的位置,看到日志文件:

打开文件,你将看到这样一行:

如果服务器布置在其它机器上,比如常见的情况是部署在某台linux服务器上,那我们使用SecureCRT之类的工具连接到命令窗口上,也可以很方便的找到日志文件(或者通过grep找到),并且使用vim之类的工具查看。

现在,恭喜你成功地配置好了日志组件,并打出了日志!

新需求!按线程编号的日志!

对于企业级的应用,这样的需求是相当常见的:

对每个请求中的日志进行编号,比如以springMVC为例,在Controller的某一个URL处理方法上,接收参数直到返回视图之间的所有操作,我们希望都具有同样的标识,比如打印出来的日志有同样的LogId。

因为比如你这个url接口上调用的某个方法出了问题,那我们就很想知道发生问题的前因后果是什么样的,并且springMVC的设计就是一个请求一个线程,同一个LogId就意味着同一个客户端的同一次请求,可以借此追踪该线程的行为,这对于debug或者维护显然是非常有用的,非常实用!

那么思考一下,怎么实现呢?每个线程都有自己的一个属性:LogId,我们还需要在这个线程方法栈中涉及到的各个方法都获取到这个属性并打印,比如像下面这种情况:

 @RequestMapping(value = "/get")
@ResponseBody
public ResultBean<t> get(Param model) { VO result = manager.findVO(model); //LogUtil用于按规则拼接出要打印的日志字符串
log.info(LogUtil.logResult(model, result)); return ResultBean.success(result);
}

中间还会跳到manager.findVO方法里,但它们是同一个线程调用的,所以想让它们有同样的LogId,该怎么办呢?生成一个LogId然后传参数到findVO里?未免太丑太糟糕了。那么怎么做能更优雅一些,更易读易维护呢?

解决方案:ThreadLocal+拦截器!

前两天刚写过一篇分析ThreadLocal的文章(戳此链接查看),觉得ThreadLocal用在这里不是正合适吗?我们希望每个线程都有一个属于该线程的数据:LogId,这个数据应该每个线程各有一个副本,不能共享,而且最好管理方便,不要方法之间传来传去的。那么ThreadLocal简直完美解决了这个问题!如果把LogId存在各个线程自己的“线程本地存储”里,那就可以轻松实现这个需求了。

然而,怎么在一个线程开始的时候分配给它LogId呢?使用springMVC提供的拦截器岂不可以开心的实现!前两天也是刚好写过分析拦截器的文章(戳此链接查看)。

那么我们创建一个拦截器,比如可以叫作LogInceptor:

 /**
* 日志处理拦截器
*
*/
public class LogInterceptor implements HandlerInterceptor { @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
LogUtil.logRequest(); return true;
} @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {} @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {}
}

preHandle里永远返回true,那也就意味着这只是想做做手脚,不是要把request拦下来。我们只是想在请求到来之时,生成一个LogId。

来看看LogUtil.logRequest()怎么写:

 /**
* 记录请求日志
*
* @param request
*/
public static void logRequest() { //生成一个logId,怎么生成随你便,这里就是搞个大点的随机数
String logId = CommonUtil.getRandomNum(12); //把这个logId存进线程本地数据副本里
LocalMap.set(LOG_ID, logId);
}

注意LogUtil是单例模式,不然每次搞出来一个就搞笑了。上面的LocalMap是创建的另一个类,用于保管每个线程的信息(毕竟logId应该只是线程里的多个数据之一)。我的意思是,每个线程都有一个本地数据副本,这个例子里我就让每个线程的本地数据是一个Map,键值对“(LOG_ID, logId)”就保存了这个线程的logId信息。
LocalMap可以这么实现(代码非原创,来自JiaYY):

 public class LocalMap {

     private static final ThreadLocal<map<string, object="">> CONTEXT = new ThreadLocal<>();

     private LocalMap() {

     }

     /**
* 向当前线程暂存数据
*
* @param key
* @param value
*/
public static void set(String key, Object value) {
Map<string, object=""> map = contextMap();
if(map != null) {
map.put(key, value);
}
} /**
* 从当前线程获取暂存数据
*
* @param key
* @return
*/
public static String get(String key) {
String value = "";
Map<string, object=""> map = contextMap();
if(map != null && map.get(key) != null) {
value = String.valueOf(map.get(key));
}
return value;
} /**
* 资源释放
*/
public static void destroy() {
Map<string, object=""> map = contextMap();
if (map != null) {
if (!map.isEmpty()) {
map.clear();
}
}
CONTEXT.remove();
} /**
* 获取当前线程里暂存的数据
* @return
*/
public static Map<string, object=""> contextMap() {
Map<string, object=""> map = CONTEXT.get();
if (map == null) {
map = new HashMap<>();
CONTEXT.set(map);
}
return map;
} }

get/set方法差不多就是这么实现,我们可爱的ThreadLocal对象也是在这被实例化的,注意这个类也是单例的,并且虽然这个类是单例的,但每个访问它的线程将会get/set到自己的数据副本(原理见我分析ThreadLocal那篇文章)。然后ThreadLocal的泛型在这里就是一个Map,也就是前面我说的“每个线程有一个自己的Map”。

然后在LogUtil里写一个获取logId的方法:

 /**
* 获取请求日志id
* @return
*/
private static String getLogId() {
return LocalMap.get(LOG_ID);
}

现在你只要调用这个方法就可以开心的在任何地方获取到当前线程的logId,我们的目的达到了。当然如果你在LogUtil里实现更多方便的方法,那就会用得更开心

最后一点小问题

如果你仔细看了上面的代码,可能会担心每次线程进来都产生一个数据副本,多了会OOM,那这个担心是没必要的。如果你仔细研究过ThreadLocal源码就会发现,线程数据副本是存在于Thread类中的,而非存在于ThreadLocal,所以当线程结束,它的数据副本(作为该线程的内部变量)自然也被释放了,其实并不会多到OOM。

但是实际上,tomcat维护了线程池,一些线程是被重复使用的, 并不会被销毁,这样的话当你取出线程的时候,说不定会发现线程里保存着之前的ThreadLocalMap,这就意味着你要么覆盖掉之前的内容,要么在每次请求结束的时候,清空该线程的数据副本。前者效率略高一点,也不用多写什么;后者效率略低一点,还需要你在拦截器afterCompletion里自己实现一个清理方法。

前者实现出来的话,其tomcat线程池占用的空间会稍大一些;后者则会小一点。但是后者还有其它的好处,比如对于一个很大的项目来说能预防未知的问题,毕竟谁也不知道别人会对这代码干什么,会不会出现你暂时没想到的BUG,会不会某一天每个线程都需要比较大的数据副本,保险一点总没坏处。

我觉得这就像ACM题里,拿一个数组循环处理每次测试,你可以每次用之前清空一下,也可以直接拿来用,反正能覆盖,但谁知道万一某道题不能覆盖了呢?万一需求又变了呢?这个问题就仁者见仁,智者见智了。

slf4j+logback搭建超实用的日志管理模块的更多相关文章

  1. python+pytest接口自动化(15)-日志管理模块loguru简介

    python自带日志管理模块logging,使用时可进行模块化配置,详细可参考博文Python日志采集(详细). 但logging配置起来比较繁琐,且在多进行多线程等场景下使用时,如果不经过特殊处理, ...

  2. 用slf4j+logback实现多功能日志解决方案 --- 转

    大家都知道,slf4j是原来log4j的作者写的一个新的日志组件,意思是简单日志门面接口,可以跟其他日志组件配合使用,常用的配合是slf4j+logback,无论从功能上还是从性能上都较之log4j有 ...

  3. logback:用slf4j+logback实现多功能日志解决方案

    slf4j是原来log4j的作者写的一个新的日志组件,意思是简单日志门面接口,可以跟其他日志组件配合使用,常用的配合是slf4j+logback,无论从功能上还是从性能上都较之log4j有了很大的提升 ...

  4. 项目日志的管理和应用 log4js-Node.js中的日志管理模块使用与封装

    开发过程中,日志记录是必不可少的事情,尤其是生产系统中经常无法调试,因此日志就成了重要的调试信息来源. Node.js,已经有现成的开源日志模块,就是log4js,源码地址:点击打开链接 项目引用方法 ...

  5. log4js-Node.js中的日志管理模块使用与封装

    开发过程中,日志记录是不可缺少的事情.尤其是生产系统中常常无法调试,因此日志就成了重要的调试信息来源. Node.js,已经有现成的开源日志模块,就是log4js,源代码地址:点击打开链接 项目引用方 ...

  6. 函数式编程(logging日志管理模块)

    本节内容 日志相关概念 logging模块简介 使用logging提供的模块级别的函数记录日志 logging模块日志流处理流程 使用logging四大组件记录日志 配置logging的几种方式 向日 ...

  7. log4js_Node.js中的日志管理模块使用

    { "appenders": [ // 下面一行应该是用于跟express配合输出web请求url日志的 {"type": "console" ...

  8. koa2学习笔记02 - 给koa2添加系统日志 —— node日志管理模块log4js

    前言 没有日志系统的后台应用是没有灵魂的, 平时工作中每次我们遇到接口报错的时候, 都会叫后台的童鞋看下怎么回事, 这时后台的童鞋都会不慌不忙的打开一个骚骚的黑窗口. 一串噼里啪啦的命令输进去, 哐哐 ...

  9. nodejs 搭建自己的简易缓存cache管理模块

    http://www.infoq.com/cn/articles/built-cache-management-module-in-nodejs/ 为什么要搭建自己的缓存管理模块? 这个问题其实也是在 ...

随机推荐

  1. SqlServer--用代码创建和删除数据库和表

    创建数据库,创建表,设置主键数据库的分离和附加MS SQLServer的每个数据库包含:1个主数据文件(.mdf)必须.1个事务日志文件(.ldf)必须.可以包含:任意多个次要数据文件(.ndf)多个 ...

  2. 细说mysql replace into

    replace语句在一般的情况下和insert差不多,但是如果表中存在primary 或者unique索引的时候,如果插入的数据和原来的primary key或者unique相同的时候,会删除原来的数 ...

  3. Java学习笔记之——静态方法

    1.方法的定义 定义在类中,方法是独立的 2.语法: public static 返回值类型 方法名(形参列表){ 方法中的具体代码: } 1)方法名:在同一个类中方法名不能重复    命名规则:驼峰 ...

  4. 本地存储之sessionStorage

    源码可以到GitHub上下载! sessionStorage: 关闭浏览器再打开将不保存数据   复制标签页会连同sessionStorage数据一同复制 复制链接地址打开网页不会复制seession ...

  5. Python之历史

    一.python简单介绍 python的创始人:吉多·范罗苏姆(Guido van Rossum),于1989年开始编写,到1991年完成了第一个python编译器.它是用C语言实现的,并能够调用C语 ...

  6. JavaScript与正则表达式

    正则表达式的定义 正则表达式与字符串对象相关的方法  相关示例 一.正则表达式(regular expression简称res) 1.定义: 一个正则表达式就是由普通字符以及特殊字符(称为元字符)组成 ...

  7. 对JS作用域和作用域链的理解

    理解好javascript的变量作用域和链式调用机制对用好变量起着关键的作用,下面我来谈谈这两个概念的理解. (1)链式调用机制 作用域链的定义:函数在调用参数时会从函数内部到函数外部逐个”搜索“参数 ...

  8. 进程管理-PV操作

    1.临界资源:诸进程需要互斥方式对其进行共享的资源. 2.临界区:每个进程中访问临界资源的那段代码. 3.信号量:一种特殊的变量.

  9. Excel的快速录入

    数据有效性: 1.选择要限制数据有效性的区域: 2.点开[数据]选项卡选择”数据验证“: 3.[设置]中选择”序列": 4.若手动输入则需要将内容使用英文符号分割开来(比如:A级,B级): ...

  10. Python面试题(一)【转】

    注:本面试题来源于网络,转载自http://www.cnblogs.com/goodhacker/p/3366618.html. 1. (1)python下多线程的限制以及多进程中传递参数的方式 py ...