拼命加载中~

JUL日志实现

简介

  • 本篇将介绍如何使用JDK原生自带的日志框架JUL

JUL日志框架

  • JUL全称Java Util Logging,是Java原生的日志框架,使用时不需要另外引入第三方类库,相对于其他日志框架来说其特点是使用方便,能够在小型应用中灵活应用。
  • JUL日志框架使用的频率并不高,但一旦需要解除此类的代码,仍然要求开发人员能够迅速看懂代码,并理解。

框架结构

  • 结构图:

  • Loggers:被称为记录器,应用程序通过获取Logger对象,调用其API来发布日志信息,Logger通常是应用程序访问日志系统的入口程序;
  • Appenders:也被称为Handlers,每个Logger都会关联一组HandlersLogger会将日志交给关联的Handlers处理,由Handlers负责将日志记录;Handlers在此是一个抽象类,由其具体的实现决定日志记录的位置是控制台、文件、网络上的其他日志服务异或是操作系统日志;
  • Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化,Layouts决定了记录的数据在一条日志记录中的最终显示形式;
  • Level:每条日志消息都有一个关联的日志级别。该级别粗略指导了日志消息的重要性和紧迫,可以将LevelLoggersAppenders做关联以便于我们过滤消息;
  • Filters:过滤器,根据需要定制哪些信息会被记录,哪些信息会被放过。
  • 一个完整的日志记录过程如下:
    1. 用户使用Logger来进行日志记录的行为,Logger可以同时持有若干个Handler,日志输出操作是由Handler完成的;
    2. Handler输出日志前,会经过Filter的过滤,判断哪些日志级别放行、哪些拦截,Handler会将日志内容输出到指定位置(日志文件、控制台等);
    3. Handler在输出日志时会使用Layout对日志内容进行排版,之后再输出到指定的位置。

入门案例

  • 示例代码:
@Test
public void testQuick() {
    // 1.创建JUL Logger对象无法传入class对象,迂回战术可以获取该类之后获取名字
    Logger logger = Logger.getLogger(JulTest.class.getName());

    // 2.输出日志的两种方式,其中日志级别共有7种,还有2种特殊级别
    logger.info("Hello, here is Java Util Logging");
    logger.log(Level.INFO, "You can also use this way to output log.");

    // 3.尽量不采用拼接字符串的形式,采用占位符的形式,占位符中需要填写索引编号
    String name = "dylan";
    int age = 12;

    logger.log(Level.INFO, "[USER]:{0} [AGE]:{1}", new Object[]{name, age});
}

日志级别

  • JUL内置工7种日志级别,另外有2中特殊级别:
  • java.util.logging.Level中定义了7种日志级别:
    1. SEVERE(最高等级)
    2. WARNING
    3. INFO(默认等级)
    4. CONFIG
    5. FINE
    6. FINER
    7. FINEST(最低等级)
  • 还有2种特殊日志级别,用于开关日志:
    1. OFF:可用于关闭日志记录;
    2. ALL:启用所有消息的日志记录。
  • 示例代码:
    1. 默认的日志级别是由RootLogger决定的,所有的Logger对象默认都会继承并使用RootLogger所提供的控制台输出处理器对象ConsoleHandler
    2. 同时,RootLogger的默认日志输出等级为INFO,则所有未经配置的Logger默认也是使用该日志级别。
/**
 * JUL默认的日志级别是Info,所有级别高于等于Info的日志都会被输出控制台。
*/
@Test
public void testLogLevel() {
    // 1.获取日志记录器对象,其中getLogger的参数name是此日志对象Logger的名称,可以由logger.getName()取出
    final Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");

    // 2.使用默认的ConsoleHandler输出各级别的日志,默认情况下只有Server、Warning和Info会输出到控制台
    logger.severe("Level Severe.");
    logger.warning("Level Warning.");
    logger.info("Level Info.");
    logger.config("Level Config.");
    logger.fine("Level Fine.");
    logger.finer("Level Finer.");
    logger.finest("Level Finest");
}
  • 运行输出:

  • 尽管代码中定义了输出INFO等级以下的日志,但实际控制台中并没有相关的日志信息,这是因为此时创建的Logger对象继承并使用了RootLogger中的日志等级和处理器对象。
  • 考虑以下代码:
@Test
public void testFindDefault() {
    final Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");

    System.out.println("Default Logger's Level: " + logger.getLevel());
    System.out.println("Default Logger's Handlers' Quantity: "  + logger.getHandlers().length);
}
  • 其中采用更为直观的方式打印当前Logger对象的日志等级,和与其关联的处理器对象数量。运行输出,得到:

  • 这并不意外,默认情况下由于Logger继承并使用RootLogger中的日志等级与处理器对象,因此对于它自身来说,并不拥有任何的日志等级信息与处理器对象。
  • 考虑以下代码:
@Test
public void testParentLogger() {
    final Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");
    final Logger loggerParent = logger.getParent();

    System.out.println("Logger's Default Parent Logger is: " + loggerParent.getClass().getSimpleName());
    System.out.println("Parent Logger's Level: " + loggerParent.getLevel());
    System.out.println("Parent Logger's Handlers' Quantity: " + loggerParent.getHandlers().length);

    for (Handler handler : loggerParent.getHandlers()) {
        System.out.println("Default " + handler.getClass().getSimpleName() + "'s Level: " + handler.getLevel());
    }
}
  • 同样采用更直观的方式打印当前Logger的父日志相关信息,得到以下输出:

  • 至此,不难推断出Logger的日志输出等级取决于RootLogger
  • 还有一个定论,即LoggerHandler的日志等级是相互牵制的,其中等级较高一方的配置生效。
  • 通过更改RootLogger中的日志级别及其ConsoleHandler的日志级别,编写以下代码:
@Test
public void testLevel() {
    // 0.准备工作
    final Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");
    final Logger parent = logger.getParent();

    // 1.RootLogger日志等级高于ConsoleHandler日志等级
    parent.setLevel(Level.WARNING);
    parent.getHandlers()[0].setLevel(Level.CONFIG);

    logger.severe("[SEVERE ]Something.");
    logger.warning("[WARNING]Something.");
    logger.info("[INFO   ]Something.");
    logger.config("[CONFIG ]Something.");

    System.out.println("=============================================");

    // 2.RootLogger日志等级小于ConsoleHandler日志等级
    parent.setLevel(Level.FINER);

    logger.severe("[SEVERE ]Something.");
    logger.warning("[WARNING]Something.");
    logger.info("[INFO   ]Something.");
    logger.config("[CONFIG ]Something.");
    logger.fine("[FINE   ]Something.");
    logger.finer("[FINER  ]Something.");
}
  • 运行输出:

  • 程序的输出与此前的定论一致。

自定义配置

  • 通常会在单独的配置文件中去配置Logger的日志等级和处理器类型等,但作为入门,需要了解如何在Java代码中,通过更改Logger日志级别和配置自定义ConsoleHandler的方式,去影响日志输出。
  • 如果不希望Logger对象使用RootLogger中的日志级别进行输出,则需要对Logger进行以下配置:
    1. 重新设置Logger的日志输出等级;
    2. 重新配置Logger的处理器Handler类型,并不再使用RootLogger中提供的默认处理器。
  • 测试类testUserDefined源码如下:
@Test
public void testUserDefined() {
    // 1.获取日志记录器对象
    Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");

    // 2.配置Logger使其不再继承使用RootLogger中的所有Handler
    logger.setUseParentHandlers(false);

    // 3.自定义ConsoleHandler对象,并配置该处理器的日志等级
    ConsoleHandler consoleHandler = new ConsoleHandler();
    consoleHandler.setLevel(Level.ALL);

    // 4.为Logger添加自定义的ConsoleHandler
    logger.addHandler(consoleHandler);

    // 5.由于Logger默认会使用RootLogger的日志等级,如果希望输出Level.ALL的日志,同时需要设置Logger的日志等级也为Level.ALL
    // *.否则,Logger将会使用RootLogger的默认日志等级INFO,最终日志只会输出等级高于或等于INFO的内容
    logger.setLevel(Level.ALL);

    // 6.分别输出各等级的日志
    logger.severe("[SEVERE ]Something.");
    logger.warning("[WARNING]Something.");
    logger.info("[INFO   ]Something.");
    logger.config("[CONFIG ]Something.");
    logger.fine("[FINE   ]Something.");
    logger.finer("[FINER  ]Something.");
    logger.finest("[FINEST ]Something.");
}
  • 运行输出:

  • 而开发中较为经常的使用方式,是通过logging.properties文件的方式进行配置:
    1. 程序中需要编写代码去显示加载logging.properties文件以启用自定义的配置;
    2. 配置文件中,Handler是单独进行配置的,开发人员可以单独定义控制台输出日志的处理器对象ConsoleHandler或文件输出日志的处理器对象FileHandler等;
    3. 具备相关自定义Handler后,需要将LoggerHandler进行关联,配置文件支持RootLogger或指定名称的Logger与自定义Handler进行关联;
    4. 无论任何时候,都需要明确日志最终的输出等级,是同时由Logger与其相关联的Handler所决定的。
# RootLogger的日志级别(默认INFO),所有的Handler都受限于此日志级别,Handler的日志级别可以比RootLogger的日志级别高
.level=ALL
# RootLogger默认的处理器,可以配置多个,所有非手动解除父日志的子日志都将使用这些处理器
handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler

# ConsoleHandler控制台输出处理器配置
# 指定ConsoleHandler默认日志级别
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.encoding=UTF-8

# FileHandler文件输出处理器配置
# 指定FileHandler默认日志级别
java.util.logging.FileHandler.level=INFO
# 日志文件输出路径
java.util.logging.FileHandler.pattern=/dylan%u.log
# 单个日志文件大小,单位是bit,1024bit即为1kb
java.util.logging.FileHandler.limit=1024*1024*10
# 日志文件数量,如果数量为2,则会生成dylan.log.0文件和dylan.log.1文件,总容量为: (limit * count)bit
java.util.logging.FileHandler.count=1
# FileHandler持有的最大并发锁数
java.util.logging.FileHandler.maxLocks=100
# 指定要使用的Formatter类的名称,FileHandler默认使用的是XMLFormatter
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
# 涉及中文日志就最好加上编码集
java.util.logging.FileHandler.encoding=UTF-8
# 是否以追加方式添加日志内容
java.util.logging.FileHandler.append=true
# SimpleFormatter的输出格式配置
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

# 自定义日志级别,其中”cn.hanna“指的是Logger.getLogger(String name)中的入参name!!!
cn.hanna.handlers=java.util.logging.ConsoleHandler
cn.hanna.level=INFO
# 如果此时不关闭名为cn.hanna的Logger的父日志处理器,则在控制台会同时出现父日志处理器和自定义的处理器,消息将重复输出
cn.hanna.useParentHandlers=false
  • 测试类test()的加载过程如下:
    1. 项目是通过Maven构建的,默认使用getResourceAsStream(String fileName)将读取resource目录下的配置文件;
    2. 其中LogManager对象是用于全局配置日志的管理对象,它是单例的,使用它来加载应用配置文件;
    3. 配置文件使用readConfiguration(InputStream is)加载应用后,RootLogger的日志级别和与之关联的Handler及其日志级别就已经配置完毕,与logging.properties中一致;
    4. 同时特殊名称cn.hannaLogger会遵循配置文件中设置的日志等级INFO,其关闭依赖RootLogger,同时仅仅将日志输出到控制台中。
@Test
public void testUserDefined() throws IOException {
    // 1.读取配置文件
    final InputStream is =
        Jul2Test.class.getClassLoader().getResourceAsStream("logging.properties");

    // 2.获取LogManager,LogManager是单例对象,并加载应用配置文件logging.properties
    final LogManager logManager = LogManager.getLogManager();
    logManager.readConfiguration(is);

    // 4.正常输出日志
    final Logger loggerNormal = Logger.getLogger(this.getClass().getName());

    loggerNormal.severe("[SEVERE ]Something.");
    loggerNormal.warning("[WARNING]Something.");
    loggerNormal.info("[INFO   ]Something.");
    loggerNormal.config("[CONFIG ]Something.");
    loggerNormal.fine("[FINE   ]Something.");
    loggerNormal.finer("[FINER  ]Something.");
    loggerNormal.finest("[FINEST ]Something.");

    System.out.println("=============================================");

    // 5.指定日志对象的名称,配置文件中对cn.hanna名称的Logger进行了特殊配置
    final Logger loggerSpecial = Logger.getLogger("cn.hanna");

    loggerSpecial.severe("[SEVERE ]Something.");
    loggerSpecial.warning("[WARNING]Something.");
    loggerSpecial.info("[INFO   ]Something.");
    loggerSpecial.config("[CONFIG ]Something.");
    loggerSpecial.fine("[FINE   ]Something.");
    loggerSpecial.finer("[FINER  ]Something.");
    loggerSpecial.finest("[FINEST ]Something.");
}
  • 运行测试,控制台输出:

  • 日志文件dylan0.log输出:

占位符相关

  • 留意到配置内formatter中的包含了相关占位符,如果希望掌握这一部分的含义,需要首先了解String.format()的使用。
  • 考虑以下代码:
@Test
public void test() {
    String name = "Mike";
    double score = 89;
    Calendar calendar = Calendar.getInstance();
    calendar.set(1992, Calendar.JANUARY, 3);

    final String formatA = String.format("[%1$-5s] %2$tF score: (%3$-8.2f)", name, calendar, score);
    final String formatB = String.format("[%1$-5s] %2$tY-%2$tm-%2$td score: (%3$8.2f)", name, calendar, score);

    System.out.println(formatA);
    System.out.println(formatB);
}
  • 运行输出:

  • 可以看到,String.format可以将数据按照指定的格式转换为String类型的数据,其中需要遵循以下规则:
    • %[argument_index$][flags][width][.precision]conversion
  • 参数说明:
参数 描述
argument_index$ 格式化参数在String.format中的索引位置,从1开始计算索引
flags 关于此格式化数据的额外格式设定,如左对齐-、是否显示正负数符号+
width 此格式数据所占用的宽度。当宽度大于参数的实际宽度时,会自动将格式化数据右对齐
.precision 需要格式化的是一个浮点数时,可以自定义该浮点数格式化后所需保留的小数点位数
conversion 表示需要格式的数据类型是什么
  • 关于String.format的详细内容,可以参考以下网址,其中提供了大量的关于各种类型的各种格式化占位符信息:
  • 了解String.format后,需要知道其实日志记录的格式化,内部使用的也是String.format,形式如下:
    • String.format(format, date, source, logger, level, message, thrown);
  • 参数说明:
参数 描述
format SimpleFormatter.format中使用的格式,也是配置文件logging.properties中定义的格式
date 日志的输出日期
source 日志的调用者,如果不存在则会输出日志对象的名称
logger 日志对象的名称
level 日志等级
message 日志信息
thrown 当有异常时,会在日志信息中包含异常信息,如果不存在则不输出
  • 关于SimpleFormatter的详细内容,可以参考以下网址,其中包含了更详细的说明与示例:
  • 初步了解相关原理后,重新审视配置文件中的%4$s: %5$s [%1$tc]%n
    1. %4$s:索引位4level,表示日志等级,数据类型是String,使用的conversions
    2. %5$s:索引位5message,表示日志信息,数据类型是String,使用的conversions
    3. %1$tc:索引位1date,表示日志输出日期,数据类型是Date,使用的conversiontc
    4. %n:换行符。
  • 因此,当运行代码logger.severe("[SEVERE ]Something.");,将得到日志记录,对比自定义格式:

  • 格式与日志输出格式一致。

滤器配置

  • 关于Filter过滤器,了解即可。
  • 代码中需要使用Logger对象中的setFilter方法,配置一个Filter对象,其源码如下:
@FunctionalInterface
public interface Filter {

    /**
     * Check if a given log record should be published.
     * @param record  a LogRecord
     * @return true if the log record should be published.
     */
    public boolean isLoggable(LogRecord record);
}
  • 其中真正控制日志是否运行记录的对象是LogRecord,使用其中的getMessage()可以获取到日志信息。
  • 当方法isLoggable返回false时,不记录日志;返回true则记录日志。
  • 测试代码如下:
    • 其中使用lambda表达式,返回的结果为!record.getMessage().contains("What"),即如果日志记录包含了关键字What,则返回false,表示过滤该条日志信息,不进行记录操作。
@Test
public void test() throws IOException {
    final InputStream is =
        Jul3Test.class.getClassLoader().getResourceAsStream("logging.properties");
    final LogManager logManager = LogManager.getLogManager();
    logManager.readConfiguration(is);

    final Logger logger = Logger.getLogger(this.getClass().getName());

    logger.severe("[SEVERE ]Something What.");
    logger.warning("[WARNING]Something.");
    logger.info("[INFO   ]Something.");
    logger.config("[CONFIG ]Something.");
    logger.fine("[FINE   ]Something What.");
    logger.finer("[FINER  ]Something.");
    logger.finest("[FINEST ]Something What.");

    System.out.println("=============================================");

    // *.配置过滤器Filter
    logger.setFilter((x) -> !x.getMessage().contains("What"));

    logger.severe("[SEVERE ]Something What.");
    logger.warning("[WARNING]Something.");
    logger.info("[INFO   ]Something.");
    logger.config("[CONFIG ]Something.");
    logger.fine("[FINE   ]Something What.");
    logger.finer("[FINER  ]Something.");
    logger.finest("[FINEST ]Something What.");
}
  • 运行输出:

  • Filter生效并成功将包含What的日志记录过滤。

完整的配置文件示例

  • 最后贴一个JUL可用的配置文件示例,其中RootLogger仅使用了ConsoleHandler输出日志:
.level=ALL
handlers=java.util.logging.ConsoleHandler

java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.encoding=UTF-8

java.util.logging.FileHandler.level=INFO
java.util.logging.FileHandler.pattern=/sample%u.log
java.util.logging.FileHandler.limit=1024*1024*10
java.util.logging.FileHandler.count=1
java.util.logging.FileHandler.maxLocks=100
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.encoding=UTF-8
java.util.logging.FileHandler.append=true
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

cn.xyz.handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler
cn.xyz.level=INFO
cn.xyz.useParentHandlers=false
  • 关于其他的Handler不作演示了,因为实际遇到的情况很少,有需要的时候查阅相关官方资料即可。

总结

  • 关于JUL的介绍到此为止,总体来说是一款很轻量的框架;
  • 在项目体量很小时,可以使用JUL记录相关日志,免去使用其他日志门面或框架的繁琐操作,如依赖导入等。
posted @ 2020-12-30 15:04  phax-ccc  阅读(677)  评论(0编辑  收藏  举报