100 行代码打造日志组件

日志记录功能在开发中很常用,不仅可以记录程序运行的细节,方便调试,也可以记录用户的行为,是框架中不可或缺的组件。

配套视频

使用方法

为最大程度复用现有的组件,当前我们没有依赖如 Log4j、LogBack、SLF4j 的日志方案,而是就地取材使用了 JDK 自带的 JUL(java.util.logging)作为日志组件。通过它可以满足一般的日志业务需求。该组件结构内容如下图所示。

在这里插入图片描述
笔者一开始还是依赖 slf4j 的,以后把这个依赖也去掉了,直接上 JUL,常见过去的博文《使用 slf4j + Java.util.logger》

和其他日志组件一样,JUL 提供了若干的级别用于描述日志信息的重要程度,有严重 SEVERE(最高级)、警告 WARNING、信息 INFO、配置 CONFIG、跟踪调试 FINE/FINER/FINEST(最低级)。默认的级别是 INFO,比 INFO 更低级别的日志将不予显示。开发者应根据源码上下文的不同采用相应级别的日志输出。JUL 简单用法如下所示(测试例子在 aj-demo 项目中, https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-demo/src/main/java/com/demo/base/SimpleLogger.java)。

import java.util.logging.Level;
import java.util.logging.Logger;

public class SimpleLogger {
	// 通过静态方法 Logger.getLogger 创建 LOGGER 实例
	private static final Logger LOGGER = Logger.getLogger(SimpleLogger.class.getName());

	public static void main(String[] args) {
		LOGGER.info("Logger 名称" + LOGGER.getName());
		LOGGER.warning("数组越界!");
		int[] a = { 1, 2, 3 };// 数组长度为 3
		int index = 4;
		LOGGER.config("index is set to " + index);

		try {
			System.out.println(a[index]);
		} catch (ArrayIndexOutOfBoundsException ex) {
			LOGGER.log(Level.SEVERE, "发送异常", ex);
		}
	}
}

运行 main() 函数,控制台显示如下图所示,打印格式一般为“[运行时间] [当前类名] [方法名]”。

在这里插入图片描述

直接使用 java.util.logging 的话比较麻烦,不妨对其进行封装一下,而且还打算提供其他额外的实用功能,比如下面这些。

  • 封装了主要三种级别的日志输出方法,分别是 config()info()warning() 方法,均可定位日志所发生的行数及类 *.java 源文件的超链接,直接出现在 IDE 控制台里面,将大大便于调试,点击即可进入发生异常源码的那一行。这是 Logger 的基本功能。
  • 日志消息支持带有多个字符串参数,类似 String.format() 机制,可以减轻开发者手动拼接字符串的负担。
  • warning() 支持传入 Throwable 类型的异常参数,并自动打印。
  • 支持不同颜色输出日志,便于区分。
  • 通过 FileHandle r实现 WARNING 级别的或以上的日记磁盘记录保存,按照当前日期命名。

新建的日志组件位于 com.ajaxjs.util.logger(源码在这里),只有两个类,分别是核心类 LogHelper 和保存到磁盘的日志处理器 FileHandler(FileHandler 源码并非原创,而是在 Tomcat 的代码基础上稍微修改而来,参见文档)。如下图所示为控制台打印日志显示时情形。

这里写图片描述

一般情况下通过工厂模式(Factory Pattern)创建 LogHelper,执行 LogHelper.getLog() 并传入目标类的 class 引用。如下代码(在 com.ajaxjs.util.logger.LogHelper 约第 239 行)粗体中就是常见的创建方法,实际使用把 TestLogHelper 替换为当前类即可。

import com.ajaxjs.util.logger.LogHelper;

public class TestLogHelper {
	// 创建类成员为日志服务
	private static final LogHelper LOGGER = LogHelper.getLog(TestLogHelper.class);

	public void testGetLog() {
		// …… 其他代码
		LOGGER.warning("发生异常!……");
		LOGGER.info("bar");
		LOGGER.warning("foo");
		// 带有多个日志消息的对象参数,用 {0},{1},{2} 预留消息位置
		LOGGER.warning("脚本引擎 {0} 没有 {1}() 这个方法", "js", "foo");
		LOGGER.warning(new Exception("致命错误!"), "脚本引擎 {0} 没有 {1}() 这个方法", "js", "foo");
	}
}

创建的日志组件成为静态成员而不是实例成员,这样的好处是节省资源,无须每个实例都创建一次日志实例,实际上也没有那个必要。

下面演示了 LogHelper 更多的用法,其中字符串参数 msg_tpl 表示为信息模版,用占位符 {0},{1},{2} 预消息位置。

public void config(String msg);
public void config(String msg_tpl, Object... params);
public void info(String msg);
public void info(String msg_tpl, Object... params);
public void warning(String msg);
public void warning(String msg_tpl, Object... params);
public void warning(Throwable ex, String msg);
public void warning(Throwable ex, String msg_tpl, Object... params);

调用例子如下。

log.info("脚本引擎 {0} 没有 {1}() 这个方法", "js", "foo");
log.warning(new Exception("致命错误!"), "脚本引擎 {0} 没有 {1}() 这个方法", "js", "foo");

要保持日志内容就要通过 FileHandler 将其保存为 *.log 文件,以备程序的后续维护。默认保存的位置固定在 /WEB-INF/LogHelper 目录下。不是所有的日志都被记录,LOGGER.info() 仅仅提示用的所以不会被记录,LOGGER.warning() 处理异常信息就会被纪录下来。这些异常信息对于程序调试非常有帮助,WARNING 级别以上的才会被 FileHandler 所记录。

源码原理分析

LogHelper 打印日志的同时,如果可以显示出该日志的执行源头,那么将为开发者调试提供莫大的便利:知道异常从哪个类来、哪个方法来,也知道是在哪一行代码上发生的。于是不禁要问这是从哪里来的?

首先观察内部 logger.logp(Level.WARNING, className, getMethodName(), msg),它调用了原生的 JDK API 方法,其中参数 className 是源头发出日志记录请求的类名,对此 LogHelper 已经把 className 作为类的属性保存起来,需要的时候直接传入即可;而 getMethodName() 是发出日志记录请求的方法名,这正是个中的关键。如下是 getMethodName() 的源码(在 com.ajaxjs.util.logger.LogHelper 约第 239 行)。

/**
 * 获取所在的方法,调用时候
 * 
 * @return 方法名称
 */
private String getMethodName() {
	StackTraceElement frame = null;

	// Thread.getCurrentThread().getStackTrace() 暴露了当前线程的运行栈信息
	for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
		String clzName = ste.getClassName();

		if (ste.isNativeMethod() || clzName.equals(Thread.class.getName()) || clzName.equals(getClass().getName()))
			continue;// 过滤不要的类

		if (clzName.equals(className)) {// className是字符串,表示所在的类名
			frame = ste;
			break;
		}
	}

	if (frame != null) // 超链接,跳到源码所在行数
		return String.format(".%s(%s:%s)", frame.getMethodName(), frame.getFileName(), frame.getLineNumber());
	else
		return null;
}

在方法执行时线程会将该方法压入栈,因此可以通过线程获取相应的栈帧元素来取得方法的调用信息。执行Thread.getCurrentThread().getStackTrace() 返回当前线程的运行栈信息,类型是 java.lang.StackTraceElement[] 栈帧元素数组。StackTraceElement 专门用于跟踪堆栈元素的信息,观察 JDK 源码如下可见。

public final class StackTraceElement implements java.io.Serializable {
	// Normally initialized by VM (public constructor added in 1.5)
	private String declaringClass; 	// 类名
	private String methodName; 		// 方法名
	private String fileName; 		// 文件名
	private int lineNumber; 		// 行号
	……
}

相关属性正好是为显示当前类名、方法名、文件名、行号等信息而准备的,可方便读取所需的信息。最后 getMethodName() 生成如 String.format(".%s(%s:%s)", …) 格式的字符串。只要符合这种格式控制台就可以输出可供点击的超链接。

如何打印彩色的日志到控制台?

通过修改控制台的字符颜色,在大量输出日志的情况下通过颜色来分类日志。该功能需要 IDE 和代码控制两个方面支持。其效果如下图所示。

在这里插入图片描述

IDEA配置方法:点击右上角的【Edit Configurations】,找到【VM options】,填入-Dspring.output.ansi.enabled=ALWAYS,如下图所示。重新启动应用,就会发现控制台日志变成彩色了。
在这里插入图片描述

Eclipse 配置方法:如图所示,搜索插件市场,找到 ANSI Escape 插件并安装。
在这里插入图片描述

输出日志时,须加上特定的前缀和后缀方能令颜色生效。例如最开始的前缀统一为 \u001b,它是 Unicode 编码中被定义为转义字符。具体生成规则参见 LogHelper 源码,这里就是不赘述了。

最后补充一下,写入本地文件使用了 Tomcat 的组件 FileHandler。另外,关于实时读取日志文件,可参考这里

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值