带你掌握Java各种日志框架

网络空间安全

  一:日志基本概念及框架

  1:什么是日志

  Java程序员在开发项目时都是依赖Eclipse/IDEA等集成开发工具的Debug调试功能来跟踪解决Bug,但项目打包部署发布到了测试环境和生产环境怎么办?难道连接到生产服务器装个IDEA做远程调试,实际并不能允许让你这么做。 所以, 日志的作用就是在测试环境和生产环境没有Debug调试工具时为开发人员和测试人员定位问题的手段 。 日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好,不仅无法辅助定位问题反而可能会影响到程序的运行性能和稳定性。

  很多介绍 AOP 的地方都采用日志来作为介绍,实际上日志要采用切面的话是极其不科学的!对于日志来说,只是在方法开始、结束、异常时输出一些日志信息,那是绝对不够的,这样的日志对于日志分析没有任何意义。如果在方法的开始和结束记录个日志,那方法中呢?如果方法中没有日志的话,那就完全失去了日志的意义!如果应用出现问题要查找由什么原因造成的,也没有什么作用。这样的日志还不如不用!

  2:日志的作用

  不管是使用何种编程语言,日志输出几乎无处不再。总结起来,日志大致有以下几种用途:「问题追踪」:辅助排查和定位线上问题,优化程序运行性能。「状态监控」:通过日志分析,可以监控系统的运行状态。「安全审计」:审计主要体现在安全上,可以发现非授权的操作。日志在应用程序中是非常重要的,好的日志信息能有助于我们在程序出现BUG时能快速进行定位,并能找出其中的原因。3:常用日志框架

  按照出来的时间顺序排列: Log4j(reload4j) --> JUL --> JCL --> SLF4J --> Logback --> Log4j2

  二:JUL日志框架

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

  1:框架结构图

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

   @Test public void demoA() { // 1.创建JUL Logger对象无法传入class对象,只能传如:"cn.xw.testDemo" Logger logger = Logger.getLogger(TestDemo.class.getName()); // 2.输出日志的两种方式,其中日志级别共有7种,还有2种特殊级别 logger.severe("[SEVERE 1000] 表明程序严重的失败或错误"); logger.warning("[WARNING 900] 表明潜在的问题"); // 带有占位符的日志打印 String name = "蚂蚁小哥", age = "24"; logger.log(Level.INFO, "姓名:{0} 年龄:{1}", new Object[]{name, age}); }3:日志级别

  JUL内置工七种日志级别,另外有两种特殊级别(共9个)java.util.logging.Level中定义了七种基本的日志级别: SEVERE【致命级别 级别码:1000】(最高级别):表明程序严重的失败或错误 WARNING【警告级别 级别码:900】:表明潜在的问题 INFO【信息级别 级别码:800】(默认):通过消息会被输入到控制台,所以信息级别一般记录对最终用户或系统管理员有用的信息 CONFIG【配置级别 级别码:700】:用于表明系统静态配置的信息 FINE【调试级别 级别码:500】:输出开发人员的信息 FINER【调试级别 级别码:400】:输出进入方法或者出入方法的一些信息和异常捕捉 FINEST【调试级别 级别码:300】(最低等级):输出最细的信息java.util.logging.Level中定义了两种基本的日志级别: OFF:可用于关闭日志记录 ALL:启用所有消息的日志记录注:默认的日志日志级别由RootLogger决定的所有的Logger对象默认都会继承并使用RootLogger所提供的控制台输出处理器 对象ConsoleHandler同时,RootLogger的默认日志输出等级为INFO,则所有未经配置的Logger默认也是使用该日志级别。跟踪代码: Logger logger = Logger.getLogger(TestDemo.class.getName()); ==>进入getLogger return demandLogger(name, null, Reflection.getCallerClass()); ==>进入demandLogger LogManager manager = LogManager.getLogManager(); ==>进入getLogManager manager.ensureLogManagerInitialized(); ==>进入ensureLogManagerInitialized owner.readPrimordialConfiguration(); //读取配置文件 logging.properties owner.rootLogger = owner.new RootLogger(); owner.addLogger(owner.rootLogger); //判断日志级别不存在的话则默认defaultLevel // private final static Level defaultLevel = Level.INFO; //默认就是INFO if (!owner.rootLogger.isLevelInitialized()) { owner.rootLogger.setLevel(defaultLevel); }

  4:自定义日志(控制台记录和日志文件记录)

  自定义日志通常会在 单独的配置文件中去配置Logger的日志等级和处理器类型 等,但作为入门,需要了解如何在Java代码中,通过更改Logger日志级别和配置自定义ConsoleHandler的方式, 去影响日志输出。如果不希望Logger对象使用RootLogger中的日志级别进行输出,则需要对Logger进行以下配置:重新设置Logger的日志输出等级;重新配置Logger的处理器Handler类型,并不再使用RootLogger中提供的默认处理器。

   @Test public void demoA() throws IOException { //获取日志记录器 Logger logger = Logger.getLogger(TestDemo.class.getName()); //设置false;指定此记录器是否应将其输出发送到其父记录器(是否继承父记录器配置)(必须设置) logger.setUseParentHandlers(false); //创建日志格式化组件对象 //若想自定义格式化则通过simpleFormatter.format(java.util.logging.LogRecord record) SimpleFormatter simpleFormatter = new SimpleFormatter(); //创建Handler(ConsoleHandler为控制台输出处理器) ConsoleHandler consoleHandler = new ConsoleHandler(); //创建文件处理器Handler fileLog.log文件名称 FileHandler fileHandler = new FileHandler("./fileLog.log"); //设置处理器中的内容输出格式及输出级别 consoleHandler.setFormatter(simpleFormatter); consoleHandler.setLevel(Level.CONFIG); fileHandler.setFormatter(simpleFormatter); fileHandler.setLevel(Level.CONFIG); //设置记录器中的处理器(控制台记录器、文件记录器)及记录器的输出级别 logger.addHandler(consoleHandler); logger.addHandler(fileHandler); logger.setLevel(Level.CONFIG); //输出打印 logger.severe("[SEVERE 1000] 表明程序严重的失败或错误"); logger.warning("[WARNING 900] 表明潜在的问题"); logger.info("[INFO 800] 信息级别,通过消息会被输入到控制台,所以信息级别一般记录对最终用户或系统管理员有用的信息"); logger.config("[CONFIG 700] 配置级别,用于表明系统静态配置的信息"); //这时默认级别为 CONFIG logger.fine("[FINE 500] 输出开发人员的信息"); logger.finer("[FINER 400] 输出进入方法或者出入方法的一些信息和异常捕捉"); logger.finest("[FINEST 300] 输出最细的信息"); }5:JUL(Logging)父子关系

  从上面我们可以看出,默认日志级别为INFO,但是更改日志级别为CONFIG时输出还是INFO,未起效果;这是因为我们创建Logger对象时继承并使用了RootLogger中的日志等级和处理器对象(子类无法覆盖父类设置的默认配置)

  我们可以通过如下代码来看出Logger日志的继承关系,继承关系就由获取getLogger("xxx . xxx .xxx")中的点来区分继承关系

   @Test public void demoA(){ //创建3个日志记录器分别是 RootManger -> logger1 -> logger2 //子logger也间接继承RootManger Logger logger1 = Logger.getLogger("cn.xw"); Logger logger2 = Logger.getLogger("cn.xw.TestDemo"); //对比继承关系 System.out.println(logger2.getParent() == logger1); // true //这里我针对 logger1 进行了自定义日志记录编写(所以logger2默认继承 logger1才是正确的) // logger1 不在继承RootLogger 所以继承关系就变为 logger1 -> logger2 logger1.setUseParentHandlers(false); SimpleFormatter simpleFormatter = new SimpleFormatter(); ConsoleHandler consoleHandler = new ConsoleHandler(); consoleHandler.setFormatter(simpleFormatter); consoleHandler.setLevel(Level.CONFIG); logger1.addHandler(consoleHandler); logger1.setLevel(Level.CONFIG); //logger2记录器记录日志 logger2.severe("[SEVERE 1000] 表明程序严重的失败或错误"); logger2.warning("[WARNING 900] 表明潜在的问题"); logger2.info("[INFO 800] 信息级别,通过消息会被输入到控制台,所以信息级别一般记录对最终用户或系统管理员有用的信息"); logger2.config("[CONFIG 700] 配置级别,用于表明系统静态配置的信息"); //这是要设置的级别为 CONFIG logger2.fine("[FINE 500] 输出开发人员的信息"); logger2.finer("[FINER 400] 输出进入方法或者出入方法的一些信息和异常捕捉"); logger2.finest("[FINEST 300] 输出最细的信息"); }6:JUL使用自定义配置文件

  开发中较为经常的使用的自定义日志方式,是通过logging.properties文件的方式进行配置 (这个配置文件默认在jre/lib目录下);我们程序中需要编写

  代码去显示加载logging.properties文件以启用自定义的配置。

  Handler是单独进行配置的,开发人员可以单独定义控制台输出日志的处理器对象ConsoleHandler或文件输出日志的处理器对象FileHandler等; 具备相关自定义Handler后,需要将Logger与Handler进行关联,配置文件RootLogger或指定名称的Logger与自定义Handler进行关联;

  无论任何时候,都需要明确日志最终的输出等级,是同时由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=ALLjava.util.logging.ConsoleHandler.encoding=UTF-8# FileHandler文件输出处理器配置# 指定FileHandler默认日志级别java.util.logging.FileHandler.level=ALL# 日志文件输出路径(当前运行程序的路径)java.util.logging.FileHandler.pattern=./appLog.log# 单个日志文件大小,单位是bit,1024bit即为1kb 1024*1024*100 = 100MBjava.util.logging.FileHandler.limit=1024*1024*100# 日志文件数量,如果数量为2,则会生成appLog.log.0文件和appLog.log.1文件,总容量为: (limit * count)bitjava.util.logging.FileHandler.count=2# FileHandler持有的最大并发锁数java.util.logging.FileHandler.maxLocks=100# 指定要使用的Formatter类的名称,FileHandler默认使用的是XMLFormatterjava.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter# 涉及中文日志就最好加上编码集java.util.logging.FileHandler.encoding=UTF-8# 是否以追加方式添加日志内容java.util.logging.FileHandler.app=true# SimpleFormatter的输出格式配置java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n# 自定义日志级别,其中”cn.xw“指的是Logger.getLogger(String name)中的入参name!!!com.anhui.handlers=java.util.logging.ConsoleHandlercom.anhui.level=CONFIG# 如果此时不关闭名为cn.hanna的Logger的父日志处理器,则在控制台会同时出现父日志处理器和自定义的处理器,消息将重复输出com.anhui.useParentHandlers=falselogging.properties配置文件 @Test public void demoA() throws IOException { //获取LogManager,LogManager是单例对象 LogManager logManager = LogManager.getLogManager(); //读取配置文件,并加载应用配置文件logging.properties InputStream resourceAsStream = TestDemo.class.getClassLoader().getResourceAsStream("logging.properties"); logManager.readConfiguration(resourceAsStream); //获取日志记录器 Logger logger = Logger.getLogger("cn.xw.TestDemo"); //输出打印 logger.severe("[SEVERE 1000] 表明程序严重的失败或错误"); logger.warning("[WARNING 900] 表明潜在的问题"); logger.info("[INFO 800] 信息级别,通过消息会被输入到控制台,所以信息级别一般记录对最终用户或系统管理员有用的信息"); logger.config("[CONFIG 700] 配置级别,用于表明系统静态配置的信息"); logger.fine("[FINE 500] 输出开发人员的信息"); logger.finer("[FINER 400] 输出进入方法或者出入方法的一些信息和异常捕捉"); logger.finest("[FINEST 300] 输出最细的信息"); //这时默认级别为 ALL System.out.println("============================================="); //指定自定义日志对象的名称,配置文件中对com.anhui名称的Logger进行了特殊配置 自定义 Logger loggerCustomize = Logger.getLogger("com.anhui"); loggerCustomize.severe("[SEVERE 1000] 表明程序严重的失败或错误"); loggerCustomize.warning("[WARNING 900] 表明潜在的问题"); loggerCustomize.info("[INFO 800] 信息级别,通过消息会被输入到控制台,所以信息级别一般记录对最终用户或系统管理员有用的信息"); loggerCustomize.config("[CONFIG 700] 配置级别,用于表明系统静态配置的信息"); //这时默认级别为 CONFIG loggerCustomize.fine("[FINE 500] 输出开发人员的信息"); loggerCustomize.finer("[FINER 400] 输出进入方法或者出入方法的一些信息和异常捕捉"); loggerCustomize.finest("[FINEST 300] 输出最细的信息"); }三:Log4j日志框架

  Log4j全称是Log for Java,它是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输出的位置是控制台、文件还是GUI组件,输出位置甚至可以是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;Log4j也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。学习Log4j最好有JUL日志框架基础,因为大部分都类似。

  1:入门案例

  <!--Log4j日志坐标导入--><depency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version></depency>//具体测试代码Demo @Test public void demoA() { //创建日志记录器对象 注意是 org.apache.log4j.Logger 包下的 Logger logger = Logger.getLogger(ClientDemo.class); //打印日志 logger.fatal("严重错误,一般造成系统崩溃并终止运行"); logger.error("错误信息,不会影响系统运行"); logger.warn("警告信息,可能会发生问题"); logger.info("运行信息,数据连接,网络连接,IO操作等"); logger.debug("调试信息,一般在开发中使用,记录程序变量传递信息等等"); logger.trace("追踪信息,记录程序所有的流程信息"); }控制台并未打印日志且打印警告:log4j:WARN No appers could be found for logger (cn.xw.ClientDemo).log4j:WARN Please initialize the log4j system properly.log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.用过JUL的都知道,当普通的Logger没有进行额外的配置时,其默认会继承并使用RootLogger的配置。Log4j中也存在RootLogger,但由于默认情况下RootLogger不具有任何的Apper(即Handler)。如果代码仅为了测试某项功能,并不想编写复杂的log4j.properties,可以使用Log4j提供的默认配置,在获取Logger前添加以下代码加载默认配置:BasicConfigurator.configure();查看configure()方法的源码: public static void configure() { Logger root = Logger.getRootLogger(); root.addApper(new ConsoleApper(new PatternLayout("%r [%t] %p %c %x - %m%n"))); }早期的源码格式化有点不太现代,但意义明确:为RootLogger对象添加一个Apper,其中Apper的类型为控制器输出的ConsoleApper输出的格式使用new PatternLayout("%r [%t] %p %c %x - %m%n")所以应急解决方案是在 Logger logger = Logger.getLogger(ClientDemo.class); 上加个 BasicConfigurator.configure();2:日志级别

  Log4j中的日志级别与JUL的不同,Log4j一共提供了六种日志级别: FATAL【严重错误】 指出每个严重的错误事件将会导致应用程序的退出。这个级别比较高了。重大错误,这种级别你可以直接停止程序了 ERROR【错误信息】: 指出虽然发生错误事件,但仍然不影响系统的继续运行。打印错误和异常信息,如果不想输出太多的日志,可以使用这个级别 WARN【警告信息】: 表明会出现潜在错误的情形,有些信息不是错误信息,但是也要给程序员的一些提示 INFO【运行信息】: 消息在粗粒度级别上突出强调应用程序的运行过程。打印一些你感兴趣的或者重要的信息,这个可以用于生产环境中输出程序 运行的一些重要信息,但是不能滥用,避免打印过多的日志。 DEBUG【调试信息】【默认级别】: 指出细粒度信息事件对调试应用程序是非常有帮助的,主要用于开发过程中打印一些运行信息 TRACE【追踪信息】: 记录程序所有的流程信息,很低的日志级别,一般不会使用 ALL:最低等级的,用于打开所有日志记录。 OFF:最高等级的,用于关闭所有日志记录。为了测试默认日志级别,可以使用以下代码测试RootLogger:@Testpublic void demoA () { // 初始化系统配置,不需要配置文件(这里直接使用默认配置) BasicConfigurator.configure(); // 获取日志记录器对象RootLogger final Logger rootLogger = Logger.getRootLogger(); // 输出配置详情 System.out.println("Logger level: " + rootLogger.getLevel()); // 获取全部Apps final Enumeration allAppers = rootLogger.getAllAppers(); while (allAppers != null && allAppers.hasMoreElements()) { final Apper apper = (Apper) allAppers.nextElement(); System.out.println("Apper is: " + apper.getClass().getSimpleName()); }}//输出://Logger level: TRACE//Apper is: ConsoleApper3:Log4j相关组件(重点)

  Log4j主要由Loggers(日志记录器)、Appers(输出端)和Layout(日志格式化器)组成: Loggers:控制日志的输出级别与日志是否输出 Appers:指定日志的输出方式(输出到控制台、文件、数据库等) Layout:控制日志信息的输出格式Ⅰ:Logger ①:日志记录器,负责收集处理日志记录,Logger的实例命名通常是类的全限定类名。 ②:Logger的名字大小写敏感,其命名有继承机制。 例如:name为org.apache.commons的logger会继承name为org.apache的logger。 注:自log4j 1.2版以来, Logger类已经取代了Category类。对于熟悉早期版本的log4j的人来说,Logger类可以被视为Category类的别名Ⅱ:Appers Apper用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。 Log4j常用的输出目的地有以下几种: ConsoleApper: 将日志输出到控制台 FileApper: 将日志输出到文件中 DailyRollingFileApper: 将日志输出到一个日志文件,周期为天,即每天输出 RollingFileApper: 将日志信息输出到一个日志文件,并且指定文件的大小,当超过指定大小,会自动将文件重命名,同时产生一个新的文件 JDBCApper: 将日志信息保存到数据库中Ⅲ:Layouts 布局器Layouts用于控制日志输出内容的格式,我们可以使用各种自定义格式输出日志。 Log4j常用的Layouts有以下几种: HTMLLayout: 格式化日志输出为HTML表格形式 SimpleLayout: 简单的日志输出格式,打印的日志格式为info-message PatternLayout: 最强大的格式化方式,可以根据自定义格式输出日志,如果没有指定转换格式,则使用默认的转换格式补充:PatternLayout中的格式化规则: log4j采用类似C语言的printf函数的打印格式格式化日志信息,具体的占位符及其含义如下: %m 输出代码中指定的日志信息 %p 输出优先级,及DEBUG、INFO等 %n 换行符(Windows平台的换行符为"\n",Unix平台为"\n") %r 输出自应用启动到输出该 log 信息耗费的毫秒数 %c 输出打印语句所属的类的全名 %t 输出产生该日志的线程全名 %d 输出服务器当前时间,默认为ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss} %l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如:Test.main(Test.java:10) %F 输出日志消息产生时所在的文件名称 %L 输出代码中的行号 %% 输出一个"%"字符 可以在%与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式。如: %5c 输出category名称,最小宽度是5,category<5,默认的情况下右对齐 %-5c 输出category名称,最小宽度是5,category<5,"-"号指定左对齐,会有空格 %.5c 输出category名称,最大宽度是5,category>5,就会将左边多出的字符截掉,<5不会有空格 %20.30c category名称<20补空格,并且右对齐,>30字符,就从左边交远销出的字符截掉4:自定义配置文件

  使用Log4j不需要特意加载配置文件,对于Maven项目来说,程序会自动扫描 resources 目录下的log4j.properties配置文件。在下面的案例中我将针对Appers的五种输出端进行介绍

   @Test public void demoA() { //创建日志记录器对象 注意是 org.apache.log4j.Logger 包下的 Logger logger = Logger.getLogger(ClientDemo.class); //打印日志 logger.fatal("严重错误,一般造成系统崩溃并终止运行"); logger.error("错误信息,不会影响系统运行"); logger.warn("警告信息,可能会发生问题"); logger.info("运行信息,数据连接,网络连接,IO操作等"); logger.debug("调试信息,一般在开发中使用,记录程序变量传递信息等等"); logger.trace("追踪信息,记录程序所有的流程信息"); }基本代码(代码都是一样的,配置文件才是我们重视的)ConsoleApper 对应自定义名称:consoleFileApper 对应自定义名称:myFileRollingFileApper 对应自定义名称:rollingFileDailyRollingFileApper 对应自定义名称:dailyFileJDBCApper 对应自定义名称:dbFile### 注:log4j.properties配置文件要放在resources目录下# 指定日志的输出级别与输出端(输出级别,自定义输出端名称,自定义输出端名称....)log4j.rootLogger = trace,console,myFile,rollingFile,dailyFile,dbFile###### 控制台输出配置log4j.apper.console = org.apache.log4j.ConsoleApper# 使用PatternLayout来声明日志用自定义格式log4j.apper.console.layout = org.apache.log4j.PatternLayout# 针对PatternLayout来用conversionPattern设置格式log4j.apper.console.layout.conversionPattern = %d [%t] %-5p [%c] - %m%n###### 文件输出配置log4j.apper.myFile = org.apache.log4j.FileApperlog4j.apper.myFile.layout = org.apache.log4j.PatternLayoutlog4j.apper.myFile.layout.conversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] - [%p] %m%n# 是否以追加日志的形式添加(默认就是true)log4j.apper.myFile.app = true# 指定日志的输出路径log4j.apper.myFile.file = ./fileLog.log# 指定日志的文件编码log4j.apper.myFile.encoding = utf-8###### 文件自动按大小拆分配置log4j.apper.rollingFile = org.apache.log4j.RollingFileApperlog4j.apper.rollingFile.layout = org.apache.log4j.PatternLayoutlog4j.apper.rollingFile.layout.conversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] - [%p] %m%nlog4j.apper.rollingFile.app = truelog4j.apper.rollingFile.file = ./fileRollingFileLog.loglog4j.apper.rollingFile.encoding = UTF-8# 文件内容超过2KB则进行拆分,拆分的最多文件由maxBackupIndex定义log4j.apper.rollingFile.maxFileSize = 2KB# 文件拆分的数量(这里是3),当文件拆分的数量超限时则最新拆分出的文件覆盖最老的日志文件log4j.apper.rollingFile.maxBackupIndex = 3###### 文件自动按日期拆分配置log4j.apper.dailyFile = org.apache.log4j.DailyRollingFileApperlog4j.apper.dailyFile.layout = org.apache.log4j.PatternLayoutlog4j.apper.dailyFile.layout.conversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] - [%p] %m%nlog4j.apper.dailyFile.app = truelog4j.apper.dailyFile.file = ./dailyRollingFile.loglog4j.apper.dailyFile.encoding = UTF-8# 文件拆分的日期 正常按照 '.'yyyy-MM-dd 以天为拆分,这里我以每秒拆分一次log4j.apper.dailyFile.datePattern = '.'yyyy-MM-dd HH-mm-ss# MySQL输出配置log4j.apper.dbFile = org.apache.log4j.jdbc.JDBCApperlog4j.apper.dbFile.layout = org.apache.log4j.PatternLayoutlog4j.apper.dbFile.layout.conversionPattern = %p %r %c %t %d{yyyy/MM/dd HH:mm:ss:SSS} %m %l %F %L %% %nlog4j.apper.dbFile.URL = jdbc:mysql://localhost:3306/log4j?serverTimezone=GMT%2B8&useAffectedRows=true&useSSL=falselog4j.apper.dbFile.User = rootlog4j.apper.dbFile.Password = 123log4j.apper.dbFile.Sql=INSERT INTO log(project_name, create_date, level, category, file_name, thread_name, line, all_category, message) \ values('log4j_xiaofeng', '%d{yyyy-MM-dd HH:mm:ss}', '%p', '%c', '%F', '%t', '%L', '%l', '%m')-- 输出MySQL配置需要在数据库创建表CREATE TABLE `log` ( `log_id` INT ( 11 ) NOT NULL AUTO_INCREMENT, `project_name` VARCHAR ( 255 ) DEFAULT NULL COMMENT '项目名称', `create_date` VARCHAR ( 255 ) DEFAULT NULL COMMENT '创建时间', `level` VARCHAR ( 255 ) DEFAULT NULL COMMENT '优先级', `category` VARCHAR ( 255 ) DEFAULT NULL COMMENT '所在类的全名', `file_name` VARCHAR ( 255 ) DEFAULT NULL COMMENT '输出日志消息产生时所在的文件名称 ', `thread_name` VARCHAR ( 255 ) DEFAULT NULL COMMENT '日志事件的线程名', `line` VARCHAR ( 255 ) DEFAULT NULL COMMENT '行号', `all_category` VARCHAR ( 255 ) DEFAULT NULL COMMENT '日志事件的发生位置', `message` VARCHAR ( 4000 ) DEFAULT NULL COMMENT '输出代码中指定的消息', PRIMARY KEY ( `log_id` ) );5:自定义配置Logger

  如下我创建了两个日志记录器对象 Logger.getLogger("cn.xw.mapper.StudentMapper"); Logger.getLogger("cn.xw.mapper.TeacherMapper");现在要让特定名称的logger使用特定的配置: 通过log4j.logger.{loggerName}的配置方式,让指定名为loggerName的logger使用该配置,由于该logger仍然是隶属 于rootLogger因此输出是累加的形式。如果RootLogger使用了ConsoleApper,同时自定义的Logger也使用了 ConsoleApper,此时控制台将输出两次日志记录,一次为自定义Logger继承自RootLogger的输出,另一次则为自定义 Logger自身的输出。但日志等级level则取决于子日志Logger为准。 例:此时RootLogger和Logger同时使用了ConsoleApper,但输出等级分别为INFO(父)和WARN(子),此时控制台输出WARN等级要求"cn.xw.mapper.StudentMapper"日志输出到控制台;"cn.xw.mapper.TeacherMapper"日志输出到文件# 指定rootLogger根日志的输出级别与输出端log4j.rootLogger = trace,console#注:下面两个自定义的Logger都会继承rootLogger,所以StudentMapper会输出控制台2次,级别的话会取子级别”debug“# 自定义Logger用来匹配 Logger.getLogger("cn.xw.mapper.StudentMapper");log4j.logger.cn.xw.mapper.StudentMapper = debug,console# 自定义Logger用来匹配 Logger.getLogger("cn.xw.mapper.TeacherMapper");log4j.logger.cn.xw.mapper.TeacherMapper = info,myFile# 控制台输出Apperlog4j.apper.console = org.apache.log4j.ConsoleApperlog4j.apper.console.layout = org.apache.log4j.PatternLayoutlog4j.apper.console.layout.conversionPattern = %d [%t] %-5p [%c] - %m%n# 文件输出Apperlog4j.apper.myFile = org.apache.log4j.FileApperlog4j.apper.myFile.layout = org.apache.log4j.PatternLayoutlog4j.apper.myFile.layout.conversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] - [%p] %m%nlog4j.apper.myFile.app = truelog4j.apper.myFile.file = ./fileLog.loglog4j.apper.myFile.encoding = utf-8@Testpublic void demoA() { //创建日志记录器对象 注意是 org.apache.log4j.Logger 包下的 cn.xw.mapper.StudentMapper Logger logger = Logger.getLogger("cn.xw.mapper.StudentMapper"); logger.fatal("严重错误,一般造成系统崩溃并终止运行"); logger.error("错误信息,不会影响系统运行"); logger.warn("警告信息,可能会发生问题"); logger.info("运行信息,数据连接,网络连接,IO操作等"); logger.debug("调试信息,一般在开发中使用,记录程序变量传递信息等等"); logger.trace("追踪信息,记录程序所有的流程信息"); // cn.xw.mapper.TeacherMapper Logger logger1 = Logger.getLogger("cn.xw.mapper.TeacherMapper"); logger1.fatal("严重错误,一般造成系统崩溃并终止运行"); logger1.error("错误信息,不会影响系统运行"); logger1.warn("警告信息,可能会发生问题"); logger1.info("运行信息,数据连接,网络连接,IO操作等"); logger1.debug("调试信息,一般在开发中使用,记录程序变量传递信息等等"); logger1.trace("追踪信息,记录程序所有的流程信息");}/** 打印信息2022-02-09 15:45:49,172 [main] FATAL [cn.xw.mapper.StudentMapper] - 严重错误,一般造成系统崩溃并终止运行2022-02-09 15:45:49,172 [main] FATAL [cn.xw.mapper.StudentMapper] - 严重错误,一般造成系统崩溃并终止运行2022-02-09 15:45:49,172 [main] ERROR [cn.xw.mapper.StudentMapper] - 错误信息,不会影响系统运行2022-02-09 15:45:49,172 [main] ERROR [cn.xw.mapper.StudentMapper] - 错误信息,不会影响系统运行2022-02-09 15:45:49,173 [main] WARN [cn.xw.mapper.StudentMapper] - 警告信息,可能会发生问题2022-02-09 15:45:49,173 [main] WARN [cn.xw.mapper.StudentMapper] - 警告信息,可能会发生问题2022-02-09 15:45:49,173 [main] INFO [cn.xw.mapper.StudentMapper] - 运行信息,数据连接,网络连接,IO操作等2022-02-09 15:45:49,173 [main] INFO [cn.xw.mapper.StudentMapper] - 运行信息,数据连接,网络连接,IO操作等2022-02-09 15:45:49,173 [main] DEBUG [cn.xw.mapper.StudentMapper] - 调试信息,一般在开发中使用,记录程序变量传递信息等等2022-02-09 15:45:49,173 [main] DEBUG [cn.xw.mapper.StudentMapper] - 调试信息,一般在开发中使用,记录程序变量传递信息等等注:这下面就是继承了RootLogger输出的 TeacherMapper信息 ,它子对象是输出到文件里,2022-02-09 15:45:49,173 [main] FATAL [cn.xw.mapper.TeacherMapper] - 严重错误,一般造成系统崩溃并终止运行2022-02-09 15:45:49,173 [main] ERROR [cn.xw.mapper.TeacherMapper] - 错误信息,不会影响系统运行2022-02-09 15:45:49,173 [main] WARN [cn.xw.mapper.TeacherMapper] - 警告信息,可能会发生问题2022-02-09 15:45:49,174 [main] INFO [cn.xw.mapper.TeacherMapper] - 运行信息,数据连接,网络连接,IO操作等Process finished with exit code 0*/测试代码信息这时就有人问了,自定义的LoggerName是"cn.xw.mapper.StudentMapper"使用ConsoleApper输出了两次,一次是继承父Logger,一次是自己原本的;若想输出一次则有两种方式:①:摒弃RootLogger的输出,即断开指定Logger与RootLogger的继承关系 # 自定义Logger用来匹配 Logger.getLogger("cn.xw.mapper.StudentMapper"); log4j.logger.cn.xw.mapper.StudentMapper = debug,console # 名为“cn.xw.mapper.StudentMapper”的Logger不再继承使用RootLogger中的Apper log4j.additivity.cn.xw.mapper.StudentMapper = false②:摒弃Logger的输出,即指定名称的Logger直接使用RootLogger关联的Apper,子Logger不再额外指定和RootLogger相同的Apper # 自定义Logger用来匹配 Logger.getLogger("cn.xw.mapper.StudentMapper"); log4j.logger.cn.xw.mapper.StudentMapper = debug6:日志输出详细信息

   @Test public void demoA() { //设置内部调试模式,开启此功能会在控制台详细记录Log4j在记录日志时打印的日志,记录自己 LogLog.setInternalDebugging(true); //创建日志记录器对象 Logger logger = Logger.getLogger("cn.xw.mapper.StudentMapper"); logger.fatal("严重错误,一般造成系统崩溃并终止运行"); }四:JCL日志门面框架

  JCL(Jakarta Commons Logging)是Apache提供的一个通用日志API(日志门面)。用户可以自由选择第三方的日志组件作为具体实现,像log4j或者JDK自带日志框架(JUL),选择Log4j或JUL的第三方日志框架后,common-logging会通过动态查找的机制,在程序运行时自动找出真正使用的日志库。当然common-logging内部有一个Simple logger 的简单实现,但是功能很弱。所以使用common-logging,通常都是配合着log4j以及其它日志框架来使用。使用它的好处就是我们代码依赖common-logging的API而非log4j的API,避免了和具体的日志API直接耦合,在有必要时,可以更改日志实现的第三方库。

  JCL有两个基本抽象类:Log(基本记录器)和LogFactory(负责创建Log实例)

  1:JCL入门及总结

  <!--JCL门面坐标依赖--> <depency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </depency>// 入门代码public class JCLClient { @Test public void demoA() { //创建日志对象;LogFactory是一个抽象类,LogFactoryImpl才是具体实现 Log logger = LogFactory.getLog(JCLClient.class); //日志记录输出 logger.fatal("严重错误,一般造成系统崩溃并终止运行"); logger.error("错误信息,不会影响系统运行"); logger.warn("警告信息,可能会发生问题"); logger.info("运行信息,数据连接,网络连接,IO操作等"); // 默认级别INFO(和JUL默认级别一样) logger.debug("调试信息,一般在开发中使用,记录程序变量传递信息等等"); logger.trace("追踪信息,记录程序所有的流程信息"); }}二月 09, 2022 9:49:11 下午 cn.xw.JCLClient demoA严重: 严重错误,一般造成系统崩溃并终止运行二月 09, 2022 9:49:11 下午 cn.xw.JCLClient demoA严重: 错误信息,不会影响系统运行二月 09, 2022 9:49:11 下午 cn.xw.JCLClient demoA警告: 警告信息,可能会发生问题二月 09, 2022 9:49:11 下午 cn.xw.JCLClient demoA信息: 运行信息,数据连接,网络连接,IO操作等通过上面的入门案例我们可以发现,打印出来的日志和JUL的日志样式是一样的,那么我猜测它底层的日志实现使用的是JUL,因为我并未导入Log4j包,那导入了Log4j坐标是什么样的呢?测试一下看看

  <!--Log4j日志框架坐标--> <depency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </depency>//导入后我们编写的代码不用改变,直接可以运行,日志打印出log4j:WARN No appers could be found for logger (cn.xw.JCLClient).log4j:WARN Please initialize the log4j system properly.log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.这就证明了,导入Log4j日志框架坐标后JCL会动态查找到Log4j并切换Log4j作为底层日志打印的第三方框架这里在resources目录下编写一个log4j.properties配置文件即可总结:我们为什么要使用日志门面: ①:面向接口开发,不再依赖具体的实现类。减少代码的耦合 ②:项目通过导入不同的日志实现类,可以灵活的切换日志框架 ③:统一API,方便开发者学习和使用 ④:统一配置便于项目日志的管理2:源码分析

  查看Log接口并光标对着Log接口按下CTRL+H来查看具体的实现类

  AvalonLogger、Jdk13LumberjackLogger、Jdk14Logger、Log4JLogger、LogKitLogger、NoOpLog、SimpleLog进入JDK14Logger类:

  //注意这里引入的类是JDK自带的日志JUL的Logger类import java.util.logging.Level;import java.util.logging.Logger;public class Jdk14Logger implements Log, Serializable { protected static final Level dummyLevel = Level.FINE; protected transient Logger logger = null;//默认为null public Jdk14Logger(String name) { this.name = name; //调用了自身的getLogger()方法 logger = getLogger(); } //1、获取Logger实例方法 public Logger getLogger() { if (logger == null) { //重要:这里调用的是java.util.logging.Logger(即JUL)的获取实例方法 logger = Logger.getLogger(name); } return logger; } //info日志等级调用 public void info(Object message) { //实际上就是调用的JUL的INFO日志等级的Log方法 log(Level.INFO, String.valueOf(message), null); } //fatal:我们之前在Log4j中看到是最高等级,这里也用来表示最高的意思 public void fatal(Object message, Throwable exception) { //实际上就是调用JUL的SERVER日志等级(最高)的Log方法 log(Level.SEVERE, String.valueOf(message), exception); }}看到这里其实心里就清楚了,原来就是外面套了个方法呀,通过继承Log接口来让Log接口统一管理各个日志框架,从而达到切换日志框架不改变代码的操作。那对于JCL中Log4j的方法我们也差不多知道了是如何实现了的。

  //注意这里使用的是Log4j的日志import org.apache.log4j.Logger;import org.apache.log4j.Priority;import org.apache.log4j.Level;public class Log4JLogger implements Log, Serializable { //默认为null private transient volatile Logger logger = null; private static final String FQCN = Log4JLogger.class.getName(); //类被加载时进行初始化 static { if (!Priority.class.isAssignableFrom(Level.class)) { throw new InstantiationError("Log4J 1.2 not available"); } Priority _traceLevel; try { } catch(Exception ex) { _traceLevel = Level.DEBUG; } traceLevel = _traceLevel; } //无参构造器 public Log4JLogger() { name = null; } //有参构造器 public Log4JLogger(String name) { this.name = name; //通过调用自身类的getLogger()获取 this.logger = getLogger();//见1 } //1、通过org.apache.log4j.Logger的getLogger()来获取实例 public Logger getLogger() { Logger result = logger; if (result == null) { synchronized(this) { result = logger; if (result == null) { logger = result = Logger.getLogger(name); } } } return result; } //fatal等级方法就是调用的Log4j的FATAL等级,是一致的 public void fatal(Object message) { getLogger().log(FQCN, Level.FATAL, message, null); }}这里是导入Log4j后会实例Log4jLogger对象那为什么添加一个Log4j坐标底层就切换到了Log4j的日志框架输出打印,去除Log4j坐标就变成JUL日志框架打印了,别急我们这就要查看 LogFactory.getLog(xxx.class) 的getLog方法了,通过它获取不同的Log对象

  // LogFactory抽象对象public abstract class LogFactory { ①:获取Log对象实例 public static Log getLog(Class clazz) throws LogConfigurationException { //获取具体实例 return getFactory().getInstance(clazz); }}// LogFactoryImpl是LogFactory具体实现类public class LogFactoryImpl exts LogFactory {... //该数组中包含了对应的全限定类名 private static final String[] classesToDiscover = new String[] {"org.apache.commons.logging.impl.Log4JLogger", "org.apache.commons.logging.impl.Jdk14Logger", "org.apache.commons.logging.impl.Jdk13LumberjackLogger", "org.apache.commons.logging.impl.SimpleLog"};... ②:获取Log实例 public Log getInstance(Class clazz) throws LogConfigurationException { //这里传入类的名称 return this.getInstance(clazz.getName()); }... ③:其中获取到Log实例 public Log getInstance(String name) throws LogConfigurationException { Log instance = (Log)this.instances.get(name); if (instance == null) { //通过类名获取到Log实例 instance = this.newInstance(name); this.instances.put(name, instance); } return instance; }... ④:获取新的实例 protected Log newInstance(String name) throws LogConfigurationException { try { Log instance; Object[] params; //默认为null if (this.logConstructor == null) { //重要:查找Log实现类,及获取Log的单个实现类 instance = this.discoverLogImplementation(name); } else { params = new Object[]{name}; instance = (Log)this.logConstructor.newInstance(params); } if (this.logMethod != null) { params = new Object[]{this}; this.logMethod.invoke(instance, params); } //返回获取到的Log实例即可 return instance; } catch (LogConfigurationException var5) ... } //⑤:该方法通过数组的顺序来进行实现类实例 private Log discoverLogImplementation(String logCategory) throws LogConfigurationException { ... //核心部分:从数组中进行遍历,可见classesToDiscover数组内容(上面),顺序依次为Log4JLogger、Jdk14Logger.. //其中的result == null则是判断是否获取到了实。

标签: 网络空间安全