文档章节

log4j2之简化封装,告别静态成员变量

倚楼听风雨_
 倚楼听风雨_
发布于 2016/10/15 21:43
字数 1661
阅读 4781
收藏 162

log4j2

本文是使用 slf4j + log4j2 示例,由于 slf4j 只是一个统一接口包,log4j / log4j2 / logback 等都是有其实现类,所以本文中是以 slf4j 为例。若有朋友坚持不使用 slf4j ,则将代码中 slf4j 相关的都做对应更改即可,并不麻烦。

一般情况下,每当我们使用 slf4j 等log组件时,都是在需要记载日志的类中,创建一个静态的 Logger 成员变量,然后调用 debug,info,error 等方法。这就意味着我们每一个要记载日志的类,都需要先定义一个静态的成员变量,这样才打印出正确的前缀(带有类名称)

一、现状

我们以码云中几个热门的java项目为例(下面的代码片段都是 star 超过 500+ 的项目)

public class DepartController {
    private static final Logger logger = Logger.getLogger(DepartController.class);
    ... method() {
        logger.info("some things!");
    }
}
@Controller
public class LoginController extends BaseController {
    private static final Logger LOGGER = LogManager.getLogger(LoginController.class);
    ... method() {
        LOGGER.info("some things!");
    }
}
public class AccountAction extends BaseAction<Account> {
    private static final Logger logger = LoggerFactory.getLogger(AccountAction.class);
    ... method() {
        logger.info("some things!");
    }
}

二、假设一个更优的 Logger

不知道大家是否有想过,要是有一个 Logger 直接提供静态的 debug/info/error 方法,就不用每个类都要去定义静态的成员变量了,比方说下面的代码(为了不被误解,我们将封装过的 Logger 命名为 GjpLogger ):

public class LoginController extends BaseController {
    ... method() {
        GjpLogger.info("some things!");
    }
}

注意,我们没有定义静态的 Logger 成员变量!

三、如何封装实现假设的 Logger

1、封装前科普:java.lang.StackTraceElement

该类元素代表一个堆栈帧。除了一个在堆栈的顶部所有的栈帧代表一个方法调用。在堆栈顶部的帧表示在将其生成的堆栈跟踪的执行点。

该类包含4个可用的get方法:getClassName()、getMethodName()、getLineNumber()、getFileName()

(细心的同学估计已经猜到了,没错!我们就从它下手,获取出被调用的那个方法所属的 类名、方法名以及当前行号)

2、封装前科普:Thread.currentThread().getStackTrace()

该方法返回一个代表该线程的堆栈转储堆栈跟踪元素的数组。这将返回一个零长度数组,如果该线程尚未启动或已经终止。

举个例子,我们调用一下项目中的 MainController.login() 方法,看可以打印出什么

StackTraceElement[] callStack = Thread.currentThread().getStackTrace();

for (StackTraceElement s : callStack) {
    System.out.println("s.getClassName() -> " + s.getClassName());
    System.out.println("s.getMethodName() -> " + s.getMethodName());
    System.out.println("s.getLineNumber() -> " + s.getLineNumber());
}
s.getClassName() -> java.lang.Thread
s.getMethodName() -> getStackTrace
s.getLineNumber() -> 1568
s.getLineNumber() -> 69
s.getClassName() -> com.guijianpan.framework.log.GjpLogger
s.getMethodName() -> info
s.getLineNumber() -> 94
s.getClassName() -> com.guijianpan.system.controller.MainController
s.getMethodName() -> login
s.getLineNumber() -> 34
s.getClassName() -> com.guijianpan.system.controller.MainController$$FastClassBySpringCGLIB$$d0dda813
s.getMethodName() -> invoke
s.getLineNumber() -> -1
s.getClassName() -> org.springframework.cglib.proxy.MethodProxy
s.getMethodName() -> invoke
s.getLineNumber() -> 204
s.getClassName() -> org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation
s.getMethodName() -> invokeJoinpoint
s.getLineNumber() -> 717
s.getClassName() -> org.springframework.aop.framework.ReflectiveMethodInvocation
s.getMethodName() -> proceed
s.getLineNumber() -> 157

我们可以看到,打印出了很多信息,实际上,我们要的只是 上面日志的 第8-10行内容

3、获取想要的 StackTraceElement

public static StackTraceElement findCaller() {
    // 获取堆栈信息
    StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
    if(null == callStack) return null;

    // 最原始被调用的堆栈信息
    StackTraceElement caller = null;
    // 日志类名称
    String logClassName = GjpLogger.class.getName();
    // 循环遍历到日志类标识
    boolean isEachLogClass = false;
    
    // 遍历堆栈信息,获取出最原始被调用的方法信息
    for (StackTraceElement s : callStack) {
        // 遍历到日志类
        if(logClassName.equals(s.getClassName())) {
            isEachLogClass = true;
        }
        // 下一个非日志类的堆栈,就是最原始被调用的方法
        if(isEachLogClass) {
            if(!logClassName.equals(s.getClassName())) {
                isEachLogClass = false;
                caller = s;
                break;
            }
        }
    }
    
    return caller;
}

到此,我们就取到了 MainController.login() 的 StackTraceElement 对象

4、封装 logger -> debug / info / error

private static Logger logger() {
    StackTraceElement caller = findCaller();
    if(null == caller) return LoggerFactory.getLogger(GjpLogger.class);
    
    // 实例化一个原始被调用的类 Logger 对象,并且带上 方法名称、行号,更方便的通过日志定位代码
    Logger log = LoggerFactory.getLogger(caller.getClassName() + "." + caller.getMethodName() + "() Line: " + caller.getLineNumber());
    
    return log;
}

/**
 * 静态的 debug 方法
 */
public static void debug(String msg) {
    debug(msg, null);
}

5、调用 logger

public class MainController {
    public ModelAndView login() {
        GjpLogger.info("test log info!");
        return render("login/login");
    }
}

6、logger 结果

2016-10-14 22:56:12 INFO  com.guijianpan.system.controller.MainController.login() Line: 34 - test log info!

附:GjpLogger.java

package com.guijianpan.framework.log;

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

/** 
 * Gjp日志工具类 {logger name 值为 [className].[methodName]() Line: [fileLine]} <br/>
 * 若要自定义可配置打印出执行的方法名和执行行号位置等信息,请参考RequestLoggerLogger.java<br/>
 * @see com.guijianpan.framework.log.log4j.RequestLogger
 * @author yzChen
 * @date 2016年10月13日 下午11:50:59 
 */
public class GjpLogger {
    
    /**
     * 获取最原始被调用的堆栈信息
     * @return    
     * @author yzChen
     * @date 2016年10月13日 下午11:50:59 
     */
    public static StackTraceElement findCaller() {
        // 获取堆栈信息
        StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
        if(null == callStack) return null;

        // 最原始被调用的堆栈信息
        StackTraceElement caller = null;
        // 日志类名称
        String logClassName = GjpLogger.class.getName();
        // 循环遍历到日志类标识
        boolean isEachLogClass = false;
        
        // 遍历堆栈信息,获取出最原始被调用的方法信息
        for (StackTraceElement s : callStack) {
            // 遍历到日志类
            if(logClassName.equals(s.getClassName())) {
                isEachLogClass = true;
            }
            // 下一个非日志类的堆栈,就是最原始被调用的方法
            if(isEachLogClass) {
                if(!logClassName.equals(s.getClassName())) {
                    isEachLogClass = false;
                    caller = s;
                    break;
                }
            }
        }
        
        return caller;
    }

    /**
     * 自动匹配请求类名,生成logger对象,此处 logger name 值为 [className].[methodName]() Line: [fileLine]
     * @return    
     * @author yzChen
     * @date 2016年10月13日 下午11:50:59 
     */
    private static Logger logger() {
        // 最原始被调用的堆栈对象
        StackTraceElement caller = findCaller();
        if(null == caller) return LoggerFactory.getLogger(GjpLogger.class);
        
        Logger log = LoggerFactory.getLogger(caller.getClassName() + "." + caller.getMethodName() + "() Line: " + caller.getLineNumber());
        
        return log;
    }


    public static void trace(String msg) {
        trace(msg, null);
    }
    public static void trace(String msg, Throwable e) {
        logger().trace(msg, e);
    }
    public static void debug(String msg) {
        debug(msg, null);
    }
    public static void debug(String msg, Throwable e) {
        logger().debug(msg, e);
    }
    public static void info(String msg) {
        info(msg, null);
    }
    public static void info(String msg, Throwable e) {
        logger().info(msg, e);
    }
    public static void warn(String msg) {
        warn(msg, null);
    }
    public static void warn(String msg, Throwable e) {
        logger().warn(msg, e);
    }
    public static void error(String msg) {
        error(msg, null);
    }
    public static void error(String msg, Throwable e) {
        logger().error(msg, e);
    }

}

四、附录

1、关于获取堆栈代码的质疑

由于评论中很多童鞋都在问关于直接读取堆栈信息的问题,比如说性能方面。性能的话,本人一般都不会深究,具体原因就不说了,当然,最大的原因是有点懒。

这里,本人另外提一下关于 Log4j2 最终打印控制台时的一部分源码,它也是读取了堆栈信息,循环遍历。以下是源代码(log4j-api-2.7.jar -> org.apache.logging.log4j.status.StatusLogger.java -> Line 229 再跳转到 Line 249)

private StackTraceElement getStackTraceElement(final String fqcn, final StackTraceElement[] stackTrace) {
	if (fqcn == null) {
		return null;
	}
	boolean next = false;
	for (final StackTraceElement element : stackTrace) {
		final String className = element.getClassName();
		if (next && !fqcn.equals(className)) {
			return element;
		}
		if (fqcn.equals(className)) {
			next = true;
		} else if (NOT_AVAIL.equals(className)) {
			break;
		}
	}
	return null;
}

2、Log4j2.xml 环境搭建及基本配置

Log4j2.xml 环境搭建及基本配置

My Blog

blog.guijianpan.com

© 著作权归作者所有

共有 人打赏支持
倚楼听风雨_

倚楼听风雨_

粉丝 52
博文 10
码字总数 8746
作品 1
长沙
私信 提问
加载中

评论(22)

y
yy541451843
肯定是会有性能问题的,因为每次log都需要调用findCaller这个方法。虽然楼主列出来Log4j2 打印控制台的代码,但是生产环境是没有输出控制台这一说的,都是tail的log文件。改天抽空写的测试,1千万次的日志输出,看看谁先跑完。
萍水相逢OSC
萍水相逢OSC
是吗
OSC_Wahson
OSC_Wahson
是吗
Z
ZhouKing
为什么不要lambok?
倚楼听风雨_
倚楼听风雨_

引用来自“倚楼听风雨_”的评论

引用来自“愚_者”的评论

一个很简单的问题,如果这样性能真的没影响,那为什么各种日志框架不这么玩呢,难道他们想不到这样封装?怎么可能

回复@愚_者 : play! framework 1.x

引用来自“愚_者”的评论

以前尝鲜过,这个开发流程有点反直觉,而且非主流框架哇。。。。
主流的都不这么玩
虽然不能说少数者一定有问题,但是大部队一定是最稳定,综合指标最优的
这个封装的话,只是一种思路,也是为了开阔下大家的知识,用不用它不重要。就像 atom 和 vscode 的一样,同是基于 electron ,atom打开几十kb的文件就卡爆,vscode打开几M都没问题,但是依然很多人用 atom 😆
倚楼听风雨_
倚楼听风雨_

引用来自“愚_者”的评论

引用来自“倚楼听风雨_”的评论

引用来自“愚_者”的评论

一个很简单的问题,如果这样性能真的没影响,那为什么各种日志框架不这么玩呢,难道他们想不到这样封装?怎么可能

回复@愚_者 : play! framework 1.x
以前尝鲜过,这个开发流程有点反直觉,而且非主流框架哇。。。。
主流的都不这么玩
虽然不能说少数者一定有问题,但是大部队一定是最稳定,综合指标最优的

回复@愚_者 : 我记得前不久oschina就有个新闻,github 最近 star 增长率的java开源框架,spring boot第一,play 第二还是第三,不过说的是 play 2.x ,没看过play 2.x
愚_者
愚_者

引用来自“倚楼听风雨_”的评论

引用来自“愚_者”的评论

一个很简单的问题,如果这样性能真的没影响,那为什么各种日志框架不这么玩呢,难道他们想不到这样封装?怎么可能

回复@愚_者 : play! framework 1.x
以前尝鲜过,这个开发流程有点反直觉,而且非主流框架哇。。。。
主流的都不这么玩
虽然不能说少数者一定有问题,但是大部队一定是最稳定,综合指标最优的
倚楼听风雨_
倚楼听风雨_

引用来自“愚_者”的评论

一个很简单的问题,如果这样性能真的没影响,那为什么各种日志框架不这么玩呢,难道他们想不到这样封装?怎么可能

回复@愚_者 : play! framework 1.x
愚_者
愚_者
一个很简单的问题,如果这样性能真的没影响,那为什么各种日志框架不这么玩呢,难道他们想不到这样封装?怎么可能
倚楼听风雨_
倚楼听风雨_

引用来自“紫电清霜”的评论

😆果断采用楼主的方法,我也很懒,总是妄想要节省代码。
😛
第二十五节:Java语言基础-面向对象基础

面向对象 面向过程的代表主要是语言,面向对象是相对面向过程而言,是面向对象的编程语言,面向过程是通过函数体现,面向过程主要是功能行为。 而对于面向对象而言,将功能封装到对象,所以面...

达叔小生
08/10
0
0
Java程序员从笨鸟到菜鸟之(二)面向对象之封装,继承,多态(上)

本文来自:曹胜欢博客专栏。转载请注明出处:http://blog.csdn.net/csh624366188 Java是一种面向对象的语言,这是大家都知道的,他与那些像c语言等面向过程语言不同的是它本身所具有的面向对...

长平狐
2012/11/12
136
0
-1-2 java 面向对象基本概念 封装继承多态 变量 this super static 静态变量 匿名对象 值传递 初始化过程 代码块 final关键字 抽象类 接口 区别 多态 包 访问权限 内部类 匿名内部类 == 与 equal

java是纯粹的面向对象的语言 也就是万事万物皆是对象 程序是对象的集合,他们通过发送消息来相互通信 每个对象都有自己的由其他的对象所构建的存储,也就是对象可以包含对象 每个对象都有它的类...

noteless
07/03
0
0
【JavaSE(三)】Java面向对象(上)

原文地址:https://www.cloudcrossing.xyz/post/35/ 面向对象是基于面向过程的编程思想。 面向过程:强调的是每一个功能的步骤; 面向对象:强调的是对象,然后由对象去调用功能。 Java程序的...

苍云横渡
05/10
0
0
JAVA基础:面向对象之封装,继承,多态

  Java是一种面向对象的语言,这是大家都知道的,他与那些像c语言等面向过程语言不同的是它本身所具有的面向对象的特性——封装,继承,多态,这也就是传说中的面向对象三大特性   一:从...

JAVA懵比的小学员
2017/01/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Ubuntu18.04 安装MySQL

1.安装MySQL sudo apt-get install mysql-server 2.配置MySQL sudo mysql_secure_installation 3.设置MySQL非root用户 设置原因:配置过程为系统root权限,在构建MySQL连接时出现错误:ERROR...

AI_SKI
今天
3
0
3.6 rc脚本(start方法) 3.7 rc脚本(stop和status方法) 3.8 rc脚本(以daemon方式启动)

3.6-3.7 rc脚本(start、stop和status方法) #!/usr/bin/env python# -*- coding: utf-8 -*-# [@Version](https://my.oschina.net/u/931210) : python 2.7# [@Time](https://my.oschina.......

隐匿的蚂蚁
今天
3
0
Cnn学习相关博客

CNN卷积神经网络原理讲解+图片识别应用(附源码) 笨方法学习CNN图像识别系列 深度学习图像识别项目(中):Keras和卷积神经网络(CNN) 卷积神经网络模型部署到移动设备 使用CNN神经网络进行...

-九天-
昨天
5
0
flutter 底部输入框 聊天输入框 Flexible

想在页面底部放个输入框,结果键盘一直遮住了,原来是布局问题 Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("评论"), ...

大灰狼wow
昨天
4
0
Kernel I2C子系统

备注:所有图片来源于网络 1,I2C协议: 物理拓扑: I2C总线由两根信号线组成,一条是时钟信号线SCL,一条是数据信号线SDA。一条I2C总线可以接多个设备,每个设备都接入I2C总线的SCL和SDA。I...

yepanl
昨天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部