01、JUL日志(JDK自带日志框架,包含源码分析)(二)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 01、JUL日志(JDK自带日志框架,包含源码分析)(二)

五、日志的配置文件


5.1、初探源码(读取配置文件部分,前)*


当我们使用Logger.getLogger("")来获取一个Logger实例时,会默认在根Logger中添加一个Conslehandler(带有SimpleFormatter转换器)。


对于rootlogger默认添加的handler以及formatter实际上是通过一个配置文件来进行配置的,其配置文件是JDK自带的,名称为logging.properteis。

接下来我们来看源码中的内容,根据数字的顺序:只针对于读取配置文件


private static final Logger logger1 = Logger.getLogger("");//调用该方法开始读源码


public class Logger {
    //1、getLogger()获取实例的方法
  @CallerSensitive
    public static Logger getLogger(String name) {、
        return demandLogger(name, null, Reflection.getCallerClass());//去到2
    }
    //2、demandLogger():日志记录器
    private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
        LogManager manager = LogManager.getLogManager();//去到3
        ....
        //该方法会创建logger实例的操作,包含其中的handlers(根据配置文件是否有反射创建实例)以及level等...,其level若没有会去拿到父类的level
        return manager.demandLogger(name, resourceBundleName, caller);//返回logger实例对象
    }
}
public class LogManager {
    private static final LogManager manager;//该类是一个单例实例
    //3、获取到LogManager的单例属性
    public static LogManager getLogManager() {
        if (manager != null) {
            manager.ensureLogManagerInitialized();//去往4
        }
        return manager;
    }
    //4、进行LogManager读取配置文件与初始化操作
    final void ensureLogManagerInitialized() {
        final LogManager owner = this;
        if (initializationDone || owner != manager) {
            return;
        }
        synchronized(this) {
            final boolean isRecursiveInitialization = (initializedCalled == true);
            assert initializedCalled || !initializationDone
            if (isRecursiveInitialization || initializationDone) {
                return;
            }
            initializedCalled = true;
            try {
                AccessController.doPrivileged(new PrivilegedAction<Object>() {
                    @Override
                    public Object run() {
                        assert rootLogger == null;
                        assert initializedCalled && !initializationDone;
                        //注意这里
                        owner.readPrimordialConfiguration();//去往5
                        //own就是logManager实例,对rootLogger进行初始化
                        owner.rootLogger = owner.new RootLogger();
                        //add方法中会实例化LoggerContext,其中包含了一个hashtable实例,键为logger名称,LoggerWeakRef引用(属性有name、LogNode、parentRef,分别为名称、节点以及父节点引用,logNode存储一个完整的logger信息)
                        owner.addLogger(owner.rootLogger);
                        if (!owner.rootLogger.isLevelInitialized()) {
                            owner.rootLogger.setLevel(defaultLevel);
                        }
                        //这里是获取一个logger实例(为了向下兼容1.7的)
                        final Logger global = Logger.global;
                        owner.addLogger(global);
                        return null;
                    }
                });
            } finally {
                initializationDone = true;
            }
        }
    }
    //5、读取原始配置文件
    private void readPrimordialConfiguration() {
        if (!readPrimordialConfiguration) {
            synchronized (this) {
                if (!readPrimordialConfiguration) {
                    if (System.out == null) {
                        return;
                    }
                    readPrimordialConfiguration = true;
                    try {
                        AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                                @Override
                                public Void run() throws Exception {
                                    //继续调用读取配置文件方法(很重要的地方)
                                    readConfiguration();//去往6
                                    // Platform loggers begin to delegate to java.util.logging.Logger
                                    sun.util.logging.PlatformLogger.redirectPlatformLoggers();
                                    return null;
                                }
                            });
                    } catch (Exception ex) {
                        assert false : "Exception raised while reading logging configuration: " + ex;
                    }
                }
            }
        }
    }
    //6、读取配置文件(本部分最关键地方)
    public void readConfiguration() throws IOException, SecurityException {
        checkPermission();
        //自定义配置类,负责完成配置信息的初始化。
        //这里cname就是对应的配置类名称,方便下面通过类加载来加载对应类进行配置信息。
        String cname = System.getProperty("java.util.logging.config.class");
        if (cname != null) {
            try {
                try {
                    Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(cname);
                    clz.newInstance();
                    return;
                } catch (ClassNotFoundException ex) {
                    Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass(cname);
                    clz.newInstance();
                    return;
                }
            } catch (Exception ex) {
                System.err.println("Logging configuration class \"" + cname + "\" failed");
                System.err.println("" + ex);
                // keep going and useful config file.
            }
        }
        //自定义配置文件
  //获取System里Properties实例中的键
        String fname = System.getProperty("java.util.logging.config.file");
        //若是没有获取到,fname为null,则会读取jdk目录中官方的logging.properties
        if (fname == null) {
            //获取jdk中的配置文件路径
            //获取系统参数,Java 安装目录,我的是C:\Program Files\Java\jdk1.8.0_201\jre
            fname = System.getProperty("java.home");
            //若没有对应参数,则会报错
            if (fname == null) {
                throw new Error("Can't find java.home ??");
            }
            //此时f路径为:C:\Program Files\Java\jdk1.8.0_201\jre\lib
            File f = new File(fname, "lib");
            //在添加一个子目录:C:\Program Files\Java\jdk1.8.0_201\jre\lib\logging.properties
            f = new File(f, "logging.properties");
            fname = f.getCanonicalPath();
        }
        //使用文件输入流来读取该配置文件:logging.properties
        try (final InputStream in = new FileInputStream(fname)) {
            //添加缓冲流,加快读取速度
            final BufferedInputStream bin = new BufferedInputStream(in);
            //读取之前读取的配置文件输入流
            readConfiguration(bin);
        }
    }
    //7、读取文件输入流操作
    public void readConfiguration(InputStream ins) throws IOException, SecurityException {
        checkPermission();
        reset();
        //该props是一个properties实例,这里将参数中的输入流放置到其中,为之后键值对读取做准备。
        props.load(ins);
        ...
    }
}



上面的源码分析暂时只到配置文件为止,不再往下深入更细节的内容,我们只要知道,在不进行自定义配置文件时,会自动读取jre/lib目录中的logging.properties到一个输入流中,再调用LoggerManager实例(单例)的readConfiguration(bin);,bin则为对应的配置文件输入流。

第122行:fname指的就是对应配置文件的路径,这里为:C:\Program Files\Java\jdk1.8.0_201\jre\lib\logging.properties。

若是我们不进行自定义配置类或自定义配置文件,那么就会自动读取jre/lib目录下官方提供的配置文件!


源码看完了,我们就去看一下这个logging.properties里面的内容吧:



确实有logging.properties配置文件。

清理了注释后内容如下:


# RootLogger 顶级父元素指定的默认处理器 ConsoleHandler,可添加多个用,隔开
handlers= java.util.logging.ConsoleHandler
# RootLogger 顶级父元素默认的日志级别为INFO
.level= INFO
# 若是上面handlers中多加了一个FileHandler就生效8-11行配置
java.util.logging.FileHandler.pattern = %h/java%u.log  # ①%h表示当前用户目录路径,我的为C:\Users\93997;②%u对  # 应下面的count,范围为[0,count)
java.util.logging.FileHandler.limit = 50000  # 输出日志文件限制大小(50000字节)
java.util.logging.FileHandler.count = 1      # 定义上面%h的范围
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter  # 设置FileHandler中的转换器为    # XMLFormatter (XML格式)
# 对应上面handlers中的ConsoleHandler进行配置
java.util.logging.ConsoleHandler.level = INFO  # handler的日志等级为INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # 该handler转换器为        # SimpleForamtter
# 特殊logger日志的默认等级,当设置name为com.xyz.foo的logger实例,其日志等级为SEVERE
com.xyz.foo.level = SEVERE  #


所以我们刚开始获取一个logger实例会自带一个handler及formatter,与这个配置文件关系很大。



5.2、自定义配置文件


首先要定义一个自定义配置文件,接着通过调用方法或在java命令中添加参数(目的就是让LogManager的单例加载到该配置文件),对于配置文件我们可以借鉴jre/lib下的logging.properties,在其基础上进行自定义配置。


名字可以随意如:xxx.properteis,后缀应当使用properteis,因为源码中读取配置文件的输入流使用的Properties的load()方法。


我们来自定义一个吧,还是logging.properties,放置的位置若是Maven项目的话放置在resource目录下;若是普通java工程放置在src目录下即可:


# ①额外增加了一个FileHandler,用于输出到文件中
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
# ②默认日志等级为ALL(那么)
.level= ALL
# ④在.log文件上层多添加了一个logs目录
java.util.logging.FileHandler.pattern = %h/logs/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
# ⑤转换器更改为简单转换
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# ③设置handler的日志等级为ALL
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
com.xyz.foo.level = SEVERE


增添内容如上数字标记部分。

因为创建的是Maven工程,所以放在resources目录中:




方式如下:


方式一:使用System.setProperties()来配置文件


System.setProperty("java.util.logging.config.file",
        "C:\\Users\\93997\\Desktop\\工程文件\\logdemo\\src\\main\\resources\\logging.properties");


解析:我们之前看源码时,针对于自定义配置文件是通过System.getProperty()来获取对应的文件路径的,所以我们在main()方法前添加对应的配置文件路径之后调用Logger.getLogger()方法就会加载我们的自定义配置文件。



方式二:在执行字节码时添加命令参数



-D java.util.logging.config.file="C:\Users\93997\Desktop\projectexers\logdemo\src\main\resources\logging.properties",值应当为详细路径。


添加该命令之后,运行程序则会自动添加到系统参数中。



方式三:使用LogManager的单例实例调用readConfiguration()方法读取配置文件输入流


@Test
public void test01() throws IOException {
    //1、通过系统类加载器的getResourceAsStream()来加载配置文件,返回输入流
    InputStream is = LogTest.class.getClassLoader().getResourceAsStream("logging.properties");
    //2、使用LogManager的单例来读取该配置文件(与源码读取官方logging.properties方法类似)
    LogManager.getLogManager().readConfiguration(is);
    //测试
    Logger logger = Logger.getLogger("xyz.changlu");
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}



这种方式与源码中的使用文件流读取配置文件类似,源码使用的是FileInputStream来读取配置文件,最后都是使用的LogManager实例(该实例是单例)的对应方法加载输入流。





配置文件中%h表示的是当前用户目录路径。

OK,三种方式都可以进行自定义配置文件,其实也可以使用配置类进行来进行自定义配置,这里就不作描述。


5.3、深入日志配置文件


配置文件详解(含源码)*

之前介绍了JDK默认的配置文件logging.properties,其中内容其实并不完整,本部分来完善相关的日志配置文件,并再去窥探源码看看在程序中是如何读取到配置文件中的配置信息的:


# 1、handlers集合介绍
# 1.1、handlers(无层级关系默认为RootLogger的handlers):是一个集合可设置多个handler
handlers = java.util.logging.ConsoleHandler,java.util.logging.FileHandler
# 默认为RootLogger的日志等级
.level= ALL
# 1.2、该handlers(包含层级关系,若程序中logger的name为xyz.changlu则默认为其添加handlers):
#                          相当于对获取的logger实例添加自定义handlers集合
xyz.changlu.handlers = java.util.logging.FileHandler
# name为xyz.changlu的logger的日志等级
xyz.changlu.level = ALL
# name为xyz.changlu的logger忽略父日志设置
xyz.changlu.useParentHandlers = false
# 2、处理器设置
# 2.1、ConsoleHandler(控制台处理器):设置等级为ALL
java.util.logging.ConsoleHandler.level = ALL
# 设置输出日志格式使用SimpleFormatter
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 设置指定的编码格式
java.util.logging.ConsoleHandler.encoding = UTF-8
# 2.2、FileHandler(文件处理器):设置日志等级为ALL
java.util.logging.FileHandler.level = ALL
# 设置输出日志文件路径:%h表示家目录  %u与下面count有关(范围为[0,count))
java.util.logging.FileHandler.pattern = %h/logs/java%u.log
# 输出日志文件限制大小(50000字节)
java.util.logging.FileHandler.limit = 50000
# 输出日志文件限制个数
java.util.logging.FileHandler.count = 1
# 设置输出日志格式使用SimpleFormatter
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 输出的日志内容是否追加到文件
java.util.logging.FileHandler.append = true
# 3、指定转换器的日志格式
# 3.1、指定SimpleFormatter的转换形式 该format输入内容为 如WARNING: warning message [Tue Mar 22 13:11:31 PDT 2011]  对比以前少了指定调用方法
java.util.logging.SimpleFormatter.format = "%4$s: %5$s [%1$tc]%n"
# 设置name=com.xyz.foo的logger实例的日志等级(可省略不看)
com.xyz.foo.level = SEVERE


对于RootLogger的日志等级、指定handlers集合以及为自定义logger设置handlers以及日志等级;

针对于不同处理器进行设置日志等级,其中FileHandler设置较多,包含日志等级、路径、文件大小、日志文件限制个数、转换各时期、是否追加。

对于SimpleFormatter也可进行转换格式设置,设置formatter参数。

该配置文件又补充了不少内容,那么补充的依据是在哪里呢?我们去源码中一探究竟:


首先看下ConsoleHandler类:其在初始化构造器时就进行了一系列的操作


public class ConsoleHandler extends StreamHandler {
  //1、无参构造
  public ConsoleHandler() {
        sealed = false;
        configure();//进行配置 去到2
        setOutputStream(System.err);
        sealed = true;
    }
    //2、进行配置操作
    private void configure() {
        LogManager manager = LogManager.getLogManager();//在此之间该LogManager的单例已经加载了自定义配置文件或官方提供的配置文件
        //cname就是获取本身的类名,为java.util.logging.ConsoleHandler
        String cname = getClass().getName();
  //重要!!!!
        //setLevel()中的方法意思是,首先会去该LogManager实例的getProperty()方法获取对应的值(实际上就是使用properties实例获取键值对中的值),若是没有获取到就使用第二个参数默认设置为INFO
        setLevel(manager.getLevelProperty(cname +".level", Level.INFO));
        //针对于Filter若是从properteis中找不到对应的键值对,则默认设置filter为null
        setFilter(manager.getFilterProperty(cname +".filter", null));
        //若是找不到键值对,则默认设置转换器为SimpleFormatter
        setFormatter(manager.getFormatterProperty(cname +".formatter", new SimpleFormatter()));
        try {
            //同理,之类若是没有设置编码格式,则会设置为null
            setEncoding(manager.getStringProperty(cname +".encoding", null));
        } catch (Exception ex) {
            try {
                setEncoding(null);
            } catch (Exception ex2) {
                // doing a setEncoding with null should always work.
                // assert false;
            }
        }
    }
}


实际上就是配置文件中对应的java.util.logging.ConsoleHandler.xxx的值,所以我们要记住了当我们进行自定义配置文件之后若是对对应handler进行了设置,那么在之后初始化handler实例也会受到影响!!!

相对于FileHandler也同样如此,我直接放相应代码即可:


//同样无参构造中调用了configure()方法
private void configure()() {
    LogManager manager = LogManager.getLogManager();
    //cname = java.util.logging.FileHandler
    String cname = getClass().getName();
    //若是LogManager实例中的properties实例若是读取不到指定键值对java.util.logging.FileHandler.pattern,默认路径为%h/java%u.log
    pattern = manager.getStringProperty(cname + ".pattern", "%h/java%u.log");
    //若是读取不到java.util.logging.FileHandler.limit,默认为0
    limit = manager.getIntProperty(cname + ".limit", 0);
    if (limit < 0) {
        limit = 0;
    }
    //读取不到默认为1 (对应%u)
    count = manager.getIntProperty(cname + ".count", 1);
    if (count <= 0) {
        count = 1;
    }
    //append是是否追加的意思,默认若是没有配置文件为false,每次向文件写日志时就会进行覆盖,所以一般会设置为true
    append = manager.getBooleanProperty(cname + ".append", false);
    //对于FileHandler,若是没有自定义配置,会设置日志等级为ALL
    setLevel(manager.getLevelProperty(cname + ".level", Level.ALL));
    //对于过滤器,无自定义配置时默认为null
    setFilter(manager.getFilterProperty(cname + ".filter", null));
    //对于转换器,无自定义配置时默认为XMLFormatter(就是以XML形式保存,一般不这样)
    setFormatter(manager.getFormatterProperty(cname + ".formatter", new XMLFormatter()));
    try {
        //对于编码,无自定义配置时默认为null
        setEncoding(manager.getStringProperty(cname +".encoding", null));
    } catch (Exception ex) {
        try {
            setEncoding(null);
        } catch (Exception ex2) {
        }
    }
}



ok,可以看到FileHandler与ConsoleHandler的默认配置还有有差别的。对于日志等级FileHandler为ALL,而console为INFO;对于转换器formatter,FileHandler默认设置为XMLFormatter,console默认设置为SimpleFormatter。

最后看下转换器SimpleFormatter类:


public class SimpleFormatter extends Formatter {
  //这里是调用一个方法获取的,和之前的有些不一样,我们深入看一下  见1
  private static final String format = LoggingSupport.getSimpleFormat()
}
//看一下LoggingSupport,粗略看下即可
public class LoggingSupport {
    //1、LoggingSupport的静态方法
    public static String getSimpleFormat() {
        return getSimpleFormat(true);//见2
    }
    //2、注意这个方法中的操作与之前的就大致相同了!!!
    static String getSimpleFormat(boolean var0) {
        String var1 = (String)AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                //这里是尝试获取System类中props是否有该键值对,默认是没有的,返回null
                return System.getProperty("java.util.logging.SimpleFormatter.format");
            }
        });
        //注意这里:若是参数var0为true&系统变量中没有读取到var1&代理类不为null(该代理类是static实例)
        //实际上这里调用代理类的getProperty()方法就是调用的LogManager.getLogManager().getProperty(key);
        //其实也就是之前LogManager读取的配置文件流!!!若是我们自定义了就会取到对应的值,没有的话返回null
        if (var0 && proxy != null && var1 == null) {
            var1 = proxy.getProperty("java.util.logging.SimpleFormatter.format");
        }
        //若var1不为null,表示从自定义配置文件中读取到了
        if (var1 != null) {
            try {
                //格式化时会加上调用该方法的日期
                String.format(var1, new Date(), "", "", "", "", "");
            } catch (IllegalArgumentException var3) {
                var1 = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%6$s%n";
            }
        } else {
            //自定义配置文件没有配置时采用该格式
            var1 = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%6$s%n";
        }
        return var1;
    }
}


我们这边再看下proxy实例是怎么获取到的,以及调用的getProperty()到底是什么:



最后对于该format的设置可以见源码中的注释,有是包含详细例子:



三个方框里的是使用对应format打印出的例子。

三个示例贴在这里:%4$s: %5$s [%1$tc]%n、%1$tc %2$s%n%4$s: %5$s%6$s%n、%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%n。其中符号含义可见源码或者api文档。


小案例

要求:在自定义配置文件中指定name=xyz.changlu的logger实例(日志等级为ALL)包含一个FileHandler(限制大小为50000字节,文件数1个,路径指定D盘的los目录中,日志等级为ALL),该FileHandler对应一个SimpleFormatter(打印两行信息的格式),并且支持追加。


答:自定义配置文件如下:


# 指定name为xyz.changlu的handlers集合中添加FileHandler
xyz.changlu.handlers = java.util.logging.FileHandler
# 该logger日志等级为ALL
xyz.changlu.level = ALL
# 表示不使用父元素的handlers集合(这里可不加)
xyz.changlu.useParentHandlers = false
# FileHandler的日志等级为ALL、路径、限制大小、数量、指定转换器、以及是否追加
java.util.logging.FileHandler.level = ALL
java.util.logging.FileHandler.pattern = D:/logs/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.FileHandler.append = true
# SimpleFormatter中转换格式为堆栈方法
java.util.logging.SimpleFormatter.format = "%1$tc %2$s%n%4$s: %5$s%6$s%n"



这里我们没有指定rootlogger的handler,那么初始时自动创建的对应rootlogger的handlers则不会存在。

注意点:logs目录我们应当提前创建好,否则会报错。(若logs目录没有,则会出现找不到文件的异常)


我们来验证一下结果:


@Test
public void test02() throws IOException {
    //读取配置文件输入流
    InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("logging.properties");
    //让LogManager来读取输入流
    LogManager.getLogManager().readConfiguration(is);
    //前两步骤做好之后再进行日志测试
    Logger logger = Logger.getLogger("xyz.changlu");
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}



屏幕前确实没有输出任何信息。



成功打印到指定文件目录下的对应文件中。


5.4、深入源码(日志执行流程,后)*


之前5.1看的源码是关于配置文件相关的,这里的话来看一看日志执行的流程:


//getlogger()方法读取了配置文件(前面5.1部分看的源码)
Logger logger = Logger.getLogger("xyz.changlu");
//本部分来看一下logger实例调用servere()方法中的执行过程
logger.severe("severe");


接下来跟着一起看下去吧:


public class Logger {
    //1、该方法实际就是调用的log()方法,其第一个参数就是指定日志等级,很明显这里就是封装了一下变得更简洁
    public void severe(String msg) {
        log(Level.SEVERE, msg);//见2
    }
    //2、传入日志等级,以及日志要打印的msg参数
    public void log(Level level, String msg) {
        //重要①(比较自身logger实例的level),若是返回false,则该日志直接结束不进行下去
        if (!isLoggable(level)) {//见3
            return;
        }
        //将日志等级以及msg日志描述参数封装到LogRecord实例中
        LogRecord lr = new LogRecord(level, msg);
        doLog(lr);//见4
    }
    //3、对日志等级进行比较
    public boolean isLoggable(Level level) {
        //实例本身日志等级比当前要打印的日志等级大或者实例本身日志等级为最大情况下返回false
      if (level.intValue() < levelValue || levelValue == offValue) {//offValue的value为Integer.MAX_VALUE
            return false;
      }
      return true;
    }
    //4、传入封装好日志等级以及msg的lr
    private void doLog(LogRecord lr) {
        lr.setLoggerName(name);
        final LoggerBundle lb = getEffectiveLoggerBundle();
        final ResourceBundle  bundle = lb.userBundle;
        final String ebname = lb.resourceBundleName;
        if (ebname != null && bundle != null) {
            lr.setResourceBundleName(ebname);
            lr.setResourceBundle(bundle);
        }
        //上面先省略,继续看该方法
        log(lr);//见5
    }
    //5、该方法是执行过程中很重要的一部分
    public void log(LogRecord record) {
        //这里再次比较logger实例的日志等级以及本次日志打印的日志等级(有疑惑之前不是已经比较过了嘛)
        if (!isLoggable(record.getLevel())) {
            return;
        }
        //重要②,这里filter是本logger实例的filter过滤器
        Filter theFilter = filter;
        //重要②,若过滤器为不为null同时该过滤器的方法执行返回为false时直接结束方法
        if (theFilter != null && !theFilter.isLoggable(record)) {
            return;
        }
        //获取本身logger实例
        Logger logger = this;
        //接着开始执行logger实例中的handlers了
        while (logger != null) {
            //在Logger(String name)中设置isSystemLogger为true了,将原本使用List存储的handlers转为一个数组形式
            final Handler[] loggerHandlers = isSystemLogger
                ? logger.accessCheckedHandlers()
                : logger.getHandlers();
    //重要③:遍历handlers数组
            for (Handler handler : loggerHandlers) {
                //重要④:这一步实际上就是使用handler进行输出日志msg信息
                handler.publish(record);//去6
            }
            //若是useParentHandlers为true将该布尔值给一个临时变量
            final boolean useParentHdls = isSystemLogger
                ? logger.useParentHandlers
                : logger.getUseParentHandlers();
    //若是useParentHdls为false(表示不获取其父handler),那么就不会往下执行,整个log()方法结束
            if (!useParentHdls) {
                break;
            }
    //获取到上级logger(若是没有定义多个有层级关系的logger,则为rootlogger)
            //重新执行比对一遍logger的handlers集合中的日志等级及过滤器
            logger = isSystemLogger ? logger.parent : logger.getParent();
        }
    }
}
//假定当前的handler为ConsoleHandler(几个handler执行过程大致相同)
public class ConsoleHandler extends StreamHandler {
    //6、输出方法
    @Override
    public void publish(LogRecord record) {
        //调用的是父类StreamHandler的publish()方法
        super.publish(record);//去7
        //刷新字符流
        flush();
    }
}
//Handler的父类StreamHandler
public class StreamHandler extends Handler {
    //7、StreamHandler的publish()方法
    @Override
    public synchronized void publish(LogRecord record) {
        //重要⑤:比对指定handler的日志等级与logger实例的日志等级
        if (!isLoggable(record)) {//去8
            return;
        }
        //若是前面都顺利通过来到这里
        String msg;
        try {
            //使用logger实例指定的转换器进行格式转换
            msg = getFormatter().format(record);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.FORMAT_FAILURE);
            return;
        }
        try {
            if (!doneHeader) {
                writer.write(getFormatter().getHead(this));
                doneHeader = true;
            }
            //最终打印出日志信息(根据handler决定去处)
            writer.write(msg);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.WRITE_FAILURE);
        }
    }
    //8、这里有判断record与输出流是否为null
    @Override
    public boolean isLoggable(LogRecord record) {
        if (writer == null || record == null) {
            return false;
        }
        return super.isLoggable(record);//去9 (Handler类中的方法)
    }
}
//StreamHandler的父类Handler
public abstract class Handler {
    private static final int offValue = Level.OFF.intValue();
    //9、这里才是真正比较handler的日志等级与logger实例的日志等级
    public boolean isLoggable(LogRecord record) {
        //levelValue默认为ALL,若是读取官方配置文件按配置文件的日志等级为INFO(这里默认读取官方配置为文件)
        final int levelValue = getLevel().intValue();
        //若是logger实例的日志等级为小于INFO或当前levelValue为Integer.MAX_VALUE,则结束方法回退到7
        if (record.getLevel().intValue() < levelValue || levelValue == offValue) {
            return false;
        }
        //若是日志等级通过,在调用执行handler的filter过滤器进行比对
        final Filter filter = getFilter();
        if (filter == null) {
            //若是无过滤器直接通过
            return true;
        }
        //指定filter方法返回filter的结果
        return filter.isLoggable(record);
    }


其实主要关键的看第3、5、7、9方法,最主要的是第5方法(核心调用)

这部分源码的执行过程以及之前读取配置文件放在下个部分统一总结,好好理清一下思路。



六、总结


五章节前后源码总结*

读取配置文件部分总结


当调用Logger.getLogger()获取logger实例时,该方法中间进行了读取配置文件的操作:


读取自定义配置类。首先会去看System类中是否包含java.util.logging.config.class对应键值对的值,若是有会对该配置类进行实例化,直接结束配置读取。

读取自定义配置文件。会去看有没有java.util.logging.config.file对应的配置文件路径,有的话赋值到String fname中。

读取官方默认的配置文件(/jre/lib/logging.properties)。若是fame没有说明没有对应的配置文件路径,就会默认去找到java配置的路径,获取到完整路径同样赋值到String fname中。

无论执行2或3步骤都是得到的一个fname全路径地址,使用FileInputStream将对应配置文件转为输入流,再统一使用LogManager中的properties实例的load()方法加载该输入流。

若不自定义配置文件以及配置类时,会默认读取官方的配置文件:


该方法生成实例logger包含内容如下:

level=INFO(默认是使用的rootlogger的日志等级)

parent=rootlogger(根logger),其属性如下:

handlers只含ConsoleHandler。

level=INFO。

ConsoleHandler属性设置:level=INFO,formatter=SimpleFormatter

FileHandler属性设置:pattern=%h/java%u.log,limit=50000,count = 1,formatter=XMLFormatter。

注意:当对应的handler、formatter初始化时会根据配置文件中的属性进行设置的。



执行流程总结


当logger实例调用severe(msg)或其他等级方法或log(level,msg)时,就会执行该流程:


首先比对的是logger实例的level,参数传入的level一定要比其>=才可继续执行。

执行logger实例的中filter过滤器,若是无过滤器或该过滤器方法返回false继续执行。

开始遍历logger实例中的handlers数组(list转数组)。

每一个handler都有重要的两步,①传入参数level比对该handler的level,只有>=才可继续执行;②执行该handler中的fiter,若为null或执行fliter返回false继续执行。两步都通过就会进行输出日志(根据handler输出指向)。

接着判断logger实例的useParentHandlers这个布尔参数,若是为true,则获取到父级logger,遍历父级logger的handlers,与第3步相同。

默认情况下logger实例的level为INFO,rootlogger的level为INFO(其中handler的等级为INFO)。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
294 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
2月前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
322 3
|
1月前
|
开发框架 前端开发 .NET
Abp源码分析之Serilog日志
本文介绍了如何在ASP.NET Core MVC项目和ABP框架中配置和使用Serilog日志库。通过修改`Program.cs`文件,配置日志级别、输出目标,并在控制器和页面模型中记录日志。具体步骤包括新建MVC项目、配置日志、修改控制器和首页代码。最终,日志将被记录到控制台和`Logs/logs.txt`文件中。
42 1
Abp源码分析之Serilog日志
|
2月前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
136 1
|
2月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
36 1
|
3月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
55 2
|
2月前
|
SQL XML 监控
SpringBoot框架日志详解
本文详细介绍了日志系统的重要性及其在不同环境下的配置方法。日志用于记录系统运行时的问题,确保服务的可靠性。文章解释了各种日志级别(如 info、warn、error 等)的作用,并介绍了常用的日志框架如 SLF4J 和 Logback。此外,还说明了如何在 SpringBoot 中配置日志输出路径及日志级别,包括控制台输出与文件输出的具体设置方法。通过这些配置,开发者能够更好地管理和调试应用程序。
|
3月前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
3月前
|
人工智能 Java 测试技术
JDK11下Mock框架进化:从PowerMockito到Mockito Only
本文探讨了从使用PowerMock的测试环境迁移到仅使用Mockito(Mockito Only)策略的必要性和实践方法。
|
3月前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
289 1