目录

log4j和slf4j分析及教程

为什么要log4j和slf4j代替System.out.println()输出日志

内容:

1.什么是log4j

2.log4j的特点

3.为什么要用Log4j来替代System.out.println

一、什么是log4j

Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。

二、log4j的特点

./1.png

./2.png

./3.png

./4.png

./5.png

三、为什么要用Log4j来替代System.out.println

关于log4j 的五个问题及答案:

(1)什么情况使用log4j、什么情况下使用System.out.println?

System.out.println在开发中可以使用,部署项目后用日志文件。最好不要用System.out.println。

(2)log4j是否会影响性能?

是的,log4j会拖慢进程响应时间

(3)为什么log4j日志的最高级是error,但error却是"虽然发生错误事件,但仍然不影响系统的继续运行"?

日志有7个等级:

A:off 最高等级,用于关闭所有日志记录。

B:fatal 指出每个严重的错误事件将会导致应用程序的退出。

C:error 指出虽然发生错误事件,但仍然不影响系统的继续运行。

D:warm 表明会出现潜在的错误情形。

E:info 一般和在粗粒度级别上,强调应用程序的运行全程。

F:debug 一般用于细粒度级别上,对调试应用程序非常有帮助。

G:all 最低等级,用于打开所有日志记录。

而log4j建议使用四种:debug、info、warn、error

(4)log4j 的文件保存位置?

存放到项目目录下:

${task5.root}/WEB-INF/logs/log.log

(5)log4j打印堆栈信息?

log.info(“xxx”, e)

看到log4j有好处也有缺点,我再来看看为什么要使用slf4j结合log4j来使用

为什么使用 SLF4J 而不是 Log4J 来做 Java 日志

每个Java开发人员都知道日志记录对Java应用的重要性,尤其是对服务端应用,而且其中许多人都已经熟悉了各种记录日志的库,比如java.util.logging,Apache的log4j,logback,然而如果你不知道 SLF4J,java的简单记录日志的设计的话 ,那么到了学习并在你的项目中使用它的时候了。在这篇Java文档里,我们将学习为什么使用SLF4J比使用log4j或者java.util.logging更好。从我写 Java开发人员的10个记录日志的技巧 算起已经过去了很长一段时间了。我不记得我所写的有关日志记录的任何事情了。无论如何,让我们回归到这个主题上来,与所有提到的这些日志记录库相比,SLF4J与它们之间有一个主要的区别。SLF4J或者说是Java的简单记录日志设计没有真正地实现日志记录,相反它只是一个允许你使用任何处于后端的日志记录库的 抽象层 。如果你正在编写内部或者外部使用的API或者应用库的话,那么你真的不需要让使用你所编写的库的客户端还去选择日志库。假设项目已经使用了log4j,而且你包含一个名为Apache Active MQ的库,这个库还依赖于另一个日志记录库logback的话,那么你还需要包含它们,然而,如果Apache Active MQ使用了SLF4J的话,你可以继续使用你的日志记录库,而不需要痛苦地添加和维护新的日志记录框架。简短的说,SLF4J让你的代码独立于任何特定的日志记录API,这个好的想法尤其适合于公共的API开发人员。虽然日志记录库的抽象理念不是新的,而且Apache的commons logging日志记录库也是用了这个理念,不过现在SLF4J很快就会成为Java世界里标准的日志记录库。让我们看一些使用 SLF4J而不使用log4j,logback或者java.util.logging的理由。

宁愿使用SLF4J也不愿使用Log4J,logback和java.util.Logging

正如我前面所说,在你的代码中编写日志记录语句使用SLF4J的主要动机是让你的程序独立于任何特定的日志记录库,这些日志记录库可能需要与你现在配置不同的配置,而且还会引入更多令人头疼的维护问题。然而除了这个之外,SLF4J API还有一个让你使用SLF4J而不是用长期感兴趣的 Log4j 更让人信服的功能,也就是占位符功能,在代码中用{}来表示。占位符功能与 String的format()方法中 的%s非常相似,因为它在运行时刻才提取所提供的真正的字符串。这不仅缩减了代码中的许多字符串连接,而且减少了创建String对象所需要的资源。即便在你生产环境日志级别比如DEBUG和INFO级别的字符串连接可能不需要的时候,仍然可以起到同样的效果。由于 字符串是不可更改的 ,而且它们是在字符串池中创建的,这些字符串使用了 堆内存 ,当应用在生产环境中运行在ERROR级别的时候,字符串在大多数情况下就不是必须的,比如DEBUG语句里的字符串就不是必须的。通过使用SLF4J,你可以延迟字符串的创建到运行时刻,这意味着只有在需要字符串的时候才创建它。如果你已经使用了log4j,那么你已经熟悉把调试语句放入if()条件内的工作场景,而SLF4J占位符功能比log4j更适合这种场景。

下面是你用Log4j时的做法,当然这并不好玩而且它增加了不必要的公式化的代码,减少了代码的可读性。

1
2
3
if (logger.isDebugEnabled()) {
    logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
}

而如果你使用SLF4J,你可以使用更简洁的格式达到同样的效果,如下:

1
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);

在SLF4J中,我们不需要进行字符串拼接,不会导致使用临时字符串带来的消耗。相反,我们使用带占位符的模板消息来记录日志信息,并提供实际值作为参数。也许你会想,要是有多个参数该怎么办,你可以使用带参数版的日志方法,也可以通过Object数组传入。这确实是非常方便而且高效的记日志的方法。记住,在为日志信息产生最终的字符串之前,该方法会检查是否开启了特定的日志级别,这不仅降低了内存占用,而且预先减少了执行字符串拼接所消耗的CPU时间。下面的SLF4J日志方法的代码,来自于slf4j-log4j12-1.6.1.jar包里的Log4j的适配器类Log4jLoggerAdapter.

1
2
3
4
5
public void debug(String format, Object arg1, Object arg2) { if (logger.isDebugEnabled()) {
      FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
      logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
}

同样值得了解是,日志也会对应用程序的性能产生压力,大家通常宣扬的是只在生产环境中才强制记录日志。

slf4j的优势与使用原理

概述

slf4j的全称是Simple Loging Facade For Java,即它仅仅是一个为Java程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就比如JDBC一样,只是一种规则而已。所以单独的slf4j是不能工作的,必须搭配其他具体的日志实现方案,比如apache的 org.apache.log4j.Logger,jdk自带的 java.util.logging.Logger等等。

slf4j的优势

知道什么是slf4j之后我们应该明白为什么要使用slf4j,为什么不适用具体的日志实现方案。笔者理解,slf4j主要有以下几点优势:

与客户端解耦

想象一下下面的场景:

有一个别人写的很棒的类库,里面使用的是jdk自带的java.util.logging.Logger这个日志系统,现在你有一个程序需要用到这个类库,并且你自己的程序现在是使用apache的org.apache.log4j.Logger这个日志系统。那么问题来了,如果你的程序导入了这个类库,那么是不是必须两种日志系统都要支持,那么你是不是需要多配置一些东西,多维护一些东西?耗费了太多维护成本,你想死的心都有了吧?

有问题就要有解决方案,不错,解决方案就是: 使用slf4j

slf4j只是一种接口,它本身并不关心你底层使用的是什么日志实现方案,所以它支持各种日志实现方案。简单的说,只要我们在类库中使用slf4j打日志,那么底层使用什么日志实现方案是使用者决定的,怎么决定?依靠配置文件和jar库。

省内存

如果大家之前使用过log4j,那么一定基本都是这样用的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.winwill.test;
import org.apache.log4j.Logger;
 * @author qifuguang
 * @date 15/8/26 21:54
 */
public class TestLog4j {
    private static final Logger LOGGER = Logger.getLogger(TestLog4j.class);
    public static void main(String[] args) {
        String message = "Hello World.";
        LOGGER.info("This is a test message: " + message);
    }
}

注意到log4j的info函数有两种使用方式:

1
2
public void info(Object message)
public void info(Object message, Throwable t)

第一个参数是要输出的信息,假设要输出的是一个字符串,并且字符串中包含变量,则message参数就必须使用字符串相加操作,就比如上面测试代码的14行一样。姑且不说字符串相加是一个比较消耗性能的操作,字符串是一个不可变对象,一旦创建就不能被修改,创建的字符串会保存在String池中,占用内存。更糟糕的是如果配置文件中配置的日志级别是ERROR的话,这行info日志根本不会输出,则相加得到的字符串对象是一个非必须对象,白白浪费了内存空间。有人会说了,那我可以这样写啊:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.winwill.test;
import org.apache.log4j.Logger;
 * @author qifuguang
 * @date 15/8/26 21:54
 */
public class TestLog4j {
    private static final Logger LOGGER = Logger.getLogger(TestLog4j.class);
    public static void main(String[] args) {
        String message = "Hello World.";
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("This is a test message: " + message);
        }
    }
}

这样不就解决了白白浪费内存的问题了吗?没错,这是一个变通方案,但是这样的代码太繁琐,不直观!

再来看看slf4j的打日志的方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.winwill.test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

 * @author qifuguang
 * @date 15/8/26 21:54
 */
public class TestLog4j {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestLog4j.class);

    public static void main(String[] args) {
        String message = "Hello World.";
        LOGGER.info("This is a test message: {}", message);
    }
}

看到没有,打日志的时候使用了{}占位符,这样就不会有字符串拼接操作,减少了无用String对象的数量,节省了内存。并且,记住,在生产最终日志信息的字符串之前,这个方法会检查一个特定的日志级别是不是打开了,这不仅降低了内存消耗而且预先降低了CPU去处理字符串连接命令的时间。这里是使用SLF4J日志方法的代码,来自于slf4j-log4j12-1.6.1.jar中的Log4j的适配器类Log4jLoggerAdapter。

1
2
3
4
5
6
public void debug(String format, Object arg1, Object arg2) {
    if (logger.isDebugEnabled()) {
        FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
        logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
}

slf4j的使用与绑定原理

前面介绍了slf4j的优势,本节介绍怎么使用slf4j以及其中的原理,前文说到了,单独的slf4j是不能工作的,必须带上其他具体的日志实现方案。就以apache的log4j作为具体日志实现方案为例,如果在工程中要使用slf4j作为接口,并且要用log4j作为具体实现方案,那么我们需要做的事情如下:(下面的xxx表示具体版本号)

  • 将slf4j-api-xxx.jar加入工程classpath中;
  • 将slf4j-log4jxx-xxx.jar加入工程classpath中;
  • 将log4j-xxx.jar加入工程classpath中;
  • 将log4j.properties(log4j.xml)文件加入工程classpath中。

介绍一下工作原理:

首先,slf4j-api作为slf4j的接口类,使用在程序代码中,这个包提供了一个Logger类和LoggerFactory类,Logger类用来打日志,LoggerFactory类用来获取Logger;slf4j-log4j是连接slf4j和log4j的桥梁,怎么连接的呢?我们看看slf4j的LoggerFactory类的getLogger函数的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * Return a logger named corresponding to the class passed as parameter, using
 * the statically bound {@link ILoggerFactory} instance.
 *
 * @param clazz the returned logger will be named after clazz
 * @return logger
 */
public static Logger getLogger(Class clazz) {
  return getLogger(clazz.getName());
}
/**
 * Return a logger named according to the name parameter using the statically
 * bound {@link ILoggerFactory} instance.
 * @param name The name of the logger.
 * @return logger
 */
 public static Logger getLogger(String name) {
   ILoggerFactory iLoggerFactory = getILoggerFactory();
   return iLoggerFactory.getLogger(name);
 }

  public static ILoggerFactory getILoggerFactory() {
  if (INITIALIZATION_STATE == UNINITIALIZED) {
    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
    performInitialization();
  }
  switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
      return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
      return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
      throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
      // support re-entrant behavior.
      // See also http://bugzilla.slf4j.org/show_bug.cgi?id=106
      return TEMP_FACTORY;
  }
  throw new IllegalStateException("Unreachable code");
}

追踪到最后,发现LoggerFactory.getLogger()首先获取一个ILoggerFactory接口,然后使用该接口获取具体的Logger。获取ILoggerFactory的时候用到了一个StaticLoggerBinder类,仔细研究我们会发现StaticLoggerBinder这个类并不是slf4j-api这个包中的类,而是slf4j-log4j包中的类,这个类就是一个中间类,它用来将抽象的slf4j变成具体的log4j,也就是说具体要使用什么样的日志实现方案,就得靠这个StaticLoggerBinder类。再看看slf4j-log4j包种的这个StaticLoggerBinder类创建ILoggerFactory长什么样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private final ILoggerFactory loggerFactory;
private StaticLoggerBinder() {
  loggerFactory = new Log4jLoggerFactory();
  try {
    Level level = Level.TRACE;
  } catch (NoSuchFieldError nsfe) {
    Util
        .report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
  }
}

public ILoggerFactory getLoggerFactory() {
  return loggerFactory;
}

可以看到slf4j-log4j中的StaticLoggerBinder类创建的ILoggerFactory其实是一个 org.slf4j.impl.Log4jLoggerFactory,这个类的getLogger函数是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public Logger getLogger(String name) {
  Logger slf4jLogger = loggerMap.get(name);
  if (slf4jLogger != null) {
    return slf4jLogger;
  } else {
    org.apache.log4j.Logger log4jLogger;
    if(name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME))
      log4jLogger = LogManager.getRootLogger();
    else
      log4jLogger = LogManager.getLogger(name);

    Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
    Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
    return oldInstance == null ? newInstance : oldInstance;
  }
}

就在其中创建了真正的 org.apache.log4j.Logger,也就是我们需要的具体的日志实现方案的Logger类。就这样,整个绑定过程就完成了。

如何使用SLF4J和Log4J来做日志

除了上面所说的好处,我认为还有个警告需要说一下,为了使用SLF4J你不仅需要进入SLF4J API Jar包,比如slf4j-api-1.6.1.jar,还需要引入协同工作的JAR包,具体是什么jar包则依赖于后端你使用了什么日志工具库。假如你想使用 _SLF4J,Simple Logging Facade for Java,还想使用Lo4J,_那么你需要把下列jar包引入到你的classpath中,具体版本要视你使用的SLF4J和log4J版本而定, 比如:

slf4j-api-1.6.1.jar - JAR for SLF4J API log4j-1.2.16.jar - JAR for Log4J API slf4j-log4j12-1.6.1.jar - Log4J Adapter for SLF4J

如果你正在使用Maven来管理你的项目依赖,你可以只引入SLF4J JAR,然后maven会引入它所依赖的其它JAR包。为了使用Log4J和SLF4J,你可以在你项目的pom.xml中添加下列依赖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.6.1</version>
</dependency>
<dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.6.1</version>
</dependency> 

顺便说一下,如果你对使用带参数版的日志方法感兴趣,那就需要引入SLF4J 1.7版本。

总结

总结这篇文章,我具有充分的理由的来选择SLF4J而不是直接选用Log4j, commons logging, logback 或者 java.util.logging。

1)在你的开源库或者私有库中使用SLF4J,可以使它独立于任何的日志实现,这就意味着不需要管理多个库和多个日志文件。你的客户端将会体会到这一点。

2)SLF4J提供了占位日志记录,通过移除对isDebugEnabled(), isInfoEnabled()等等的检查提高了代码的可读性。

3)通过使用日志记录方法,直到你使用到的时候,才会去构造日志信息(字符串),这就同时提高了内存和CPU的使用率。

4)做一个侧面的说明,越少的临时字符串,垃圾回收器就意味着越少的工作,这就意味着为你的应用程序提供更好的吞吐量和性能。

这些优势都只是冰山一角,当你开始使用SL4J并阅读它,你会学到更多的好处。我强烈建议,在java中任何新的代码开发,都应使用SLF4J而不是任何的日志API,包括log4J。