开源日志库Logger的剖析

简介:

库的整体架构图

详细剖析

我们从使用的角度来对Logger库抽茧剥丝:

 
 
  1. String userName = "Jerry"
  2. Logger.i(userName);  

看看Logger.i()这个方法:

 
 
  1. public static void i(String message, Object... args) {       
  2.     printer.i(message, args); 
  3.  

还有个可变参数,来看看printer.i(message, args)是啥:

 
 
  1. public Interface Printer{ 
  2.     void i(String message, Object... args); 

是个接口,那我们就要找到这个接口的实现类,找到printer对象在Logger类中声明的地方:

 
 
  1. private static Printer printer = new LoggerPrinter(); 

实现类是LoggerPrinter,而且这还是个静态的成员变量,这个静态是有用处的,后面会讲到,那就继续跟踪LoggerPrinter类的i(String message, Object... args)方法的实现:

 
 
  1. @Override public void i(String message, Object... args) {   
  2.     log(INFO, null, message, args); 
  3. /**  
  4. * This method is synchronized in order to avoid messy of logs' order.  
  5. */ 
  6. private synchronized void log(int priority, Throwable throwable, String msg, Object... args) { 
  7.     // 判断当前设置的日志级别,为NONE则不打印日志   
  8.     if (settings.getLogLevel() == LogLevel.NONE) {     
  9.         return;   
  10.     } 
  11.     // 获取tag 
  12.     String tag = getTag();  
  13.     // 创建打印的消息 
  14.     String message = createMessage(msg, args);       
  15.     // 打印 
  16.     log(priority, tag, message, throwable); 
  17.  
  18. public enum LogLevel {   
  19.     /**    
  20.     * Prints all logs    
  21.     */   
  22.     FULL,   
  23.     /**    
  24.     * No log will be printed    
  25.     */   
  26.     NONE 
  27.  
  • 首先,log方法是一个线程安全的同步方法,为了防止日志打印时候顺序的错乱,在多线程环境下,这是非常有必要的。
  • 其次,判断日志配置的打印级别,FULL打印全部日志,NONE不打印日志。
  • 再来,getTag(): 
     
       
    1. private final ThreadLocal<String> localTag = new ThreadLocal<>(); 
    2. /**  
    3. * @return the appropriate tag based on local or global */ 
    4. private String getTag() {   
    5.     // 从ThreadLocal<String> localTag里获取本地一个缓存的tag 
    6.     String tag = localTag.get();   
    7.     if (tag != null) {     
    8.         localTag.remove();     
    9.         return tag;   
    10.     }   
    11.     return this.tag; 
    12.  

这个方法是获取本地或者全局的tag值,当localTag中有tag的时候就返回出去,并且清空localTag的值,关于ThreadLocal还不是很清楚的可以参考主席的文章:http://blog.csdn.net/singwhat...

接着,createMessage方法:

 
 
  1. private String createMessage(String message, Object... args) {  
  2.     return args == null || args.length == 0 ? message : String.format(message, args); 

这里就很清楚了,为什么我们用Logger.i(message, args)的时候没有写args,也就是null,也可以打印,而且是直接打印的message消息的原因。同样博主上一篇文章也提到了:

 
 
  1. Logger.i("博主今年才%d,英文名是%s", 16, "Jerry"); 

像这样的可以拼接不同格式的数据的打印日志,原来实现的方式是用String.format方法,这个想必小伙伴们在开发Android应用的时候String.xml里的动态字符占位符用的也不少,应该很容易理解这个format方法的用法。

重头戏,我们把tag,打印级别,打印的消息处理好了,接下来该打印出来了:

 
 
  1. @Override public synchronized void log(int priority, String tag, String message, Throwable throwable) { 
  2.     // 同样判断一次库配置的打印开关,为NONE则不打印日志 
  3.     if (settings.getLogLevel() == LogLevel.NONE) {     
  4.         return;   
  5.     } 
  6.     // 异常和消息不为空的时候,获取异常的原因转换成字符串后拼接到打印的消息中   
  7.     if (throwable != null && message != null) {     
  8.         message += " : " + Helper.getStackTraceString(throwable);   
  9.     }   
  10.     if (throwable != null && message == null) {     
  11.         message = Helper.getStackTraceString(throwable);   
  12.     }   
  13.     if (message == null) {     
  14.         message = "No message/exception is set";   
  15.     }   
  16.     // 获取方法数 
  17.     int methodCount = getMethodCount();  
  18.     // 判断消息是否为空  
  19.     if (Helper.isEmpty(message)) {     
  20.         message = "Empty/NULL log message";   
  21.     }   
  22.     // 打印日志体的上边界 
  23.     logTopBorder(priority, tag); 
  24.     // 打印日志体的头部内容   
  25.     logHeaderContent(priority, tag, methodCount);   
  26.     //get bytes of message with system's default charset (which is UTF-8 for Android)   
  27.     byte[] bytes = message.getBytes();   
  28.     int length = bytes.length;   
  29.     // 消息字节长度小于等于4000 
  30.     if (length <= CHUNK_SIZE) {     
  31.         if (methodCount > 0) {   
  32.             // 方法数大于0,打印出分割线     
  33.             logDivider(priority, tag);     
  34.         }     
  35.         // 打印消息内容 
  36.         logContent(priority, tag, message); 
  37.         // 打印日志体底部边界 
  38.         logBottomBorder(priority, tag);     
  39.         return;   
  40.     }   
  41.     if (methodCount > 0) {     
  42.         logDivider(priority, tag);   
  43.     }   
  44.     for (int i = 0; i < length; i += CHUNK_SIZE) {     
  45.         int count = Math.min(length - i, CHUNK_SIZE); 
  46.         //create a new String with system's default charset (which is UTF-8 for Android)     
  47.         logContent(priority, tag, new String(bytes, i, count));   
  48.     }   
  49.     logBottomBorder(priority, tag); 

我们重点来看看logHeaderContent方法和logContent方法:

 
 
  1. @SuppressWarnings("StringBufferReplaceableByString"
  2. private void logHeaderContent(int logType, String tag, int methodCount) {   
  3.   // 获取当前线程堆栈跟踪元素数组 
  4.   //(里面存储了虚拟机调用的方法的一些信息:方法名、类名、调用此方法在文件中的行数) 
  5.   // 这也是这个库的 “核心” 
  6.   StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 
  7.   // 判断库的配置是否显示线程信息   
  8.   if (settings.isShowThreadInfo()) { 
  9.       // 获取当前线程的名称,并且打印出来,然后打印分割线     
  10.       logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + "Thread: " + Thread.currentThread().getName());    logDivider(logType, tag);   
  11.   }   
  12.   String level = "";   
  13.   // 获取追踪栈的方法起始位置 
  14.   int stackOffset = getStackOffset(trace) + settings.getMethodOffset();   
  15.   //corresponding method count with the current stack may exceeds the stack trace. Trims the count   
  16.   // 打印追踪的方法数超过了当前线程能够追踪的方法数,总的追踪方法数扣除偏移量(从调用日志的起算扣除的方法数),就是需要打印的方法数量 
  17.   if (methodCount + stackOffset > trace.length) {     
  18.       methodCount = trace.length - stackOffset - 1;   
  19.   }   
  20.   for (int i = methodCount; i > 0; i--) {    
  21.       int stackIndex = i + stackOffset;     
  22.       if (stackIndex >= trace.length) {       
  23.           continue;     
  24.       }     
  25.       // 拼接方法堆栈调用路径追踪字符串 
  26.       StringBuilder builder = new StringBuilder();  
  27.       builder.append("║ ")         
  28.       .append(level)      
  29.       .append(getSimpleClassName(trace[stackIndex].getClassName()))  // 追踪到的类名 
  30.       .append(".")  
  31.       .append(trace[stackIndex].getMethodName())  // 追踪到的方法名       
  32.       .append(" ")         
  33.       .append(" (")        
  34.       .append(trace[stackIndex].getFileName()) // 方法所在的文件名 
  35.       .append(":")         
  36.       .append(trace[stackIndex].getLineNumber())  // 在文件中的行号       
  37.       .append(")");     
  38.       level += "   ";     
  39.       // 打印出头部信息 
  40.       logChunk(logType, tag, builder.toString());  
  41.   } 

接下来看logContent方法:

 
 
  1. private void logContent(int logType, String tag, String chunk) {   
  2.     // 这个作用就是获取换行符数组,getProperty方法获取的就是"\\n"的意思 
  3.     String[] lines = chunk.split(System.getProperty("line.separator"));   
  4.     for (String line : lines) {     
  5.         // 打印出包含换行符的内容 
  6.         logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " " + line);   
  7.     } 

如上图来说内容是字符串数组,本身里面是没用换行符的,所以不需要换行,打印出来的效果就是一行,但是json、xml这样的格式是有换行符的,所以打印呈现出来的效果就是:

上面说了大半天,都还没看到具体的打印是啥,现在来看看logChunk方法:

 
 
  1. private void logChunk(int logType, String tag, String chunk) { 
  2.     // 最后格式化下tag   
  3.     String finalTag = formatTag(tag);   
  4.     // 根据不同的日志打印类型,然后交给LogAdapter这个接口来打印 
  5.     switch (logType) {     
  6.         case ERROR:       
  7.             settings.getLogAdapter().e(finalTag, chunk);       
  8.         break;     
  9.         case INFO:       
  10.             settings.getLogAdapter().i(finalTag, chunk);       
  11.         break;     
  12.         case VERBOSE:       
  13.             settings.getLogAdapter().v(finalTag, chunk);       
  14.         break;     
  15.         case WARN:       
  16.             settings.getLogAdapter().w(finalTag, chunk);       
  17.         break;    
  18.         case ASSERT:       
  19.             settings.getLogAdapter().wtf(finalTag, chunk);       
  20.         break;     
  21.         case DEBUG:       
  22.             // Fall through, log debug by default     
  23.         default:             
  24.             settings.getLogAdapter().d(finalTag, chunk);       
  25.         break;   
  26.     } 
  27.  

这个方法很简单,就是最后格式化tag,然后根据不同的日志类型把打印的工作交给LogAdapter接口来处理,我们来看看settings.getLogAdapter()这个方法(Settings.java文件):

 
 
  1. public LogAdapter getLogAdapter() {   
  2.     if (logAdapter == null) { 
  3.         // 最终的实现类是AndroidLogAdapter 
  4.         logAdapter = new AndroidLogAdapter();   
  5.     }   
  6.     return logAdapter; 
  7.  

找到AndroidLogAdapter类:

原来绕了一大圈,最终打印还是使用了:系统的Log。

好了Logger日志框架的源码解析完了,有没有更清晰呢,也许小伙伴会说这个最终的日志打印,我不想用系统的Log,是不是可以换呢。这是自然的,看开篇的那种整体架构图,这个LogAdapter是个接口,只要实现这个接口,里面做你自己想要打印的方式,然后通过Settings 的logAdapter(LogAdapter logAdapter)方法设置进去就可以。

以上就是博主分析一个开源库的思路,从使用的角度出发抽茧剥丝,基本上一个库的核心部分都能搞懂。画画整个框架的大概类图,对分析库非常有帮助,每一个轮子都有值得学习的地方,吸收了就是进步的开始,耐心的分析完一个库,还是非常有成就感的。

感谢你耐心看完,以后博主还会继续努力分析其它轮子的。






作者:jerryloveemily
来源:51CTO

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
11天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
87 1
|
1月前
|
C++
glog --- C++日志库
glog --- C++日志库
|
1月前
|
API 开发工具 C语言
【嵌入式开源库】EasyLogger的使用, 一款轻量级且高性能的日志库
【嵌入式开源库】EasyLogger的使用, 一款轻量级且高性能的日志库
|
1月前
|
芯片
【嵌入式开源库】使用J-Link打印日志,让你节省一个打印串口
【嵌入式开源库】使用J-Link打印日志,让你节省一个打印串口
|
2月前
|
自然语言处理 Java API
常见C++ 开源日志库的比较
常见C++ 开源日志库的比较
27 0
|
2月前
|
算法 Java 测试技术
【深入探究 C++ 日志库性能比较】glog、log4cplus 和 spdlog 的日志输出性能分析
【深入探究 C++ 日志库性能比较】glog、log4cplus 和 spdlog 的日志输出性能分析
208 0
|
1月前
|
安全 Linux 网络安全
/var/log/secure日志详解
Linux系统的 `/var/log/secure` 文件记录安全相关消息,包括身份验证和授权尝试。它涵盖用户登录(成功或失败)、`sudo` 使用、账户锁定解锁及其他安全事件和PAM错误。例如,SSH登录成功会显示&quot;Accepted password&quot;,失败则显示&quot;Failed password&quot;。查看此文件可使用 `tail -f /var/log/secure`,但通常只有root用户有权访问。
108 4
|
4天前
|
C++
JNI Log 日志输出
JNI Log 日志输出
12 1
|
4天前
|
存储 运维 大数据
聊聊日志硬扫描,阿里 Log Scan 的设计与实践
泛日志(Log/Trace/Metric)是大数据的重要组成,伴随着每一年业务峰值的新脉冲,日志数据量在快速增长。同时,业务数字化运营、软件可观测性等浪潮又在对日志的存储、计算提出更高的要求。
|
11天前
|
XML Java Maven
Springboot整合与使用log4j2日志框架【详解版】
该文介绍了如何在Spring Boot中切换默认的LogBack日志系统至Log4j2。首先,需要在Maven依赖中排除`spring-boot-starter-logging`并引入`spring-boot-starter-log4j2`。其次,创建`log4j2-spring.xml`配置文件放在`src/main/resources`下,配置包括控制台和文件的日志输出、日志格式和文件切分策略。此外,可通过在不同环境的`application.yml`中指定不同的log4j2配置文件。最后,文章提到通过示例代码解释了日志格式中的各种占位符含义。