因为软件开发发展到今天已经很复杂了,特别是服务端软件、涉及到知识内容问题太多,我们为了解决一下问题(1、日志输出的格式 2、日志输出的位置 3、日志的优化,异步日志,日志文件的归档和压缩 4、日志系统的维护也就是要求我们面向接口的开发)
在某些方面使用别人成熟的框架,站在别人的基础之上开发自己的核心部分代码,一般来说框架经过沉淀,展现在我们面前的一般都是比较优秀的,稳健的,成熟的框架,我们没有比较重复造轮子,况且我们造的不一定有人家的好。并且框架也是不断的有人去维护的,我们可以直接享受别人升级带来的好处。
log4j ( Log For Java ) 日志框架
JUL ( Java Util Logging ) 日志框架
JCL ( Jakarta Commons Logging ) 用于统一Log4j和JCL的日志门面,其也有简单的日志实现
slf4j ( Simple Logging Facade For Java )一个升级的日志门面可以用于多数日志框架的统一,并且有规范接口,新出现的日志框架可以适配(实现)之。其也有简单的实现
logback 实现slf4j的api接口,不需要适配器
log4j2 log4j的升级,参考logback源码,比logback更优秀,更强大的性能,大势所趋。
JUL全称 Java Util Logging 它是java原生的日志框架,不需要第三方类库,学习简单,相对方便,能够灵活的应用于小型应用。
Logger:日志记录器(在框架中就是一个类),咱们的应用程序通过该对象,调用其API来发布日志,如 loger.info("博主好帅!")。其中的loger就是一个日志记录器对象
Appender:也被称为Handler,每一个Logger都会关联一个或许多个Handler,会将日志交给这些Handler,由它们将日志处理到响应的输出设备(如负责控制台的Handler会将日志输出到控制台,负责文件的Handler会将日志输出到文件,还有网络、日志服务器等其他的位置)。
Layouts:也被称为Formatter,主要负责对日志的格式进行排布,可以通过具体的参数来自定义日志的表现格式
Level:每一条日志消息都会有其日志级别,该级别标识了这条日志的重要程度,我们可以设置Logger的Level(对该Logger的每一个Handler有效),也可以设置Handler的Level(只对该Handle有效)(过滤的结果是两个中较为严格的那个)来过滤消息。
Filter:过滤器,根据需要可以定制哪些信息被记录(ACCEPT),哪些信息被放过(DENY)
public class JULtest {
public static void main(String[] args) {
Logger logger = Logger.getLogger("com.L.JULtest");
/*占位符的方式*/
//logger.severe("{0}{1}",new Object[]{"博主","好帅!"}); 这样写是不行的
logger.log(Level.SEVERE,"{0}{1}",new Object[]{"博主","好帅!"});
/*日志级别*/
logger.severe("severe");
logger.log(Level.SEVERE,"severe");
logger.warning("warning");
logger.info("info"); // 默认只会输出info以上级别的日志信息
logger.config("config");
logger.fine("fine");
logger.finer("finner");
logger.finest("finest");
//以上日志是按章重要程度从高往低举的例子
}
}
八月 24, 2021 11:16:12 下午 com.L.JULtest main 严重: 博主好帅! 八月 24, 2021 11:16:12 下午 com.L.JULtest main 严重: severe 八月 24, 2021 11:16:12 下午 com.L.JULtest main 严重: severe 八月 24, 2021 11:16:12 下午 com.L.JULtest main 警告: warning 八月 24, 2021 11:16:12 下午 com.L.JULtest main 信息: info |
@Test
public void CustomLoggerLevel(){
// 1. 获取日志记录器对象
Logger logger = Logger.getLogger("com.L.JULtest");
// 2. 关闭系统默认的配置(不继承父日志记录器的相关参数)
logger.setUseParentHandlers(false);
// 3. 自定义配置日志
// 3.1 创建日志处理器(Handler对象),这是一个可以让日志输出到控制台(Console)的日志处理器
ConsoleHandler consoleHandler = new ConsoleHandler();
// 3.2 创建简单格式转换器
SimpleFormatter simpleFormatter = new SimpleFormatter();
// 3.3 关联日志处理器和格式转换器
consoleHandler.setFormatter(simpleFormatter);
// 3.4 给Logger对象添加我们创建的Handler,(因为在第2步阻止使用父日志记录器参数所以不会继承父日志记录器中的Handler有关继承请继续阅读)
logger.addHandler(consoleHandler);
// 4. 配置日志级别
logger.setLevel(Level.FINE);
consoleHandler.setLevel(Level.SEVERE); //最终级别为这两个中更为严格的那个
logger.severe("severe");
logger.warning("warning");
logger.info("info"); // 默认只会输出info以上级别的日志信息
logger.config("config");
logger.fine("fine");
logger.finer("finner");
logger.finest("finest");
}
八月 24, 2021 11:16:33 下午 com.L.JULtest CustomLoggerLevel 严重: severe |
@Test
public void LogToFile() throws IOException {
// 1. 获取日志记录器对象
Logger logger = Logger.getLogger("com.L.JULtest");
// 2. 关闭系统默认的配置(不继承父日志记录器的相关参数)
logger.setUseParentHandlers(false);
// 3. 自定义配置日志
// 3.1 创建日志处理器(Handler对象),这是一个可以让日志输出到文件的日志处理器
FileHandler fileHandler = new FileHandler("D:\\AppData\\IntellJ IDEA work\\JavaLog-Demo\\logtest.log");
// 3.2 创建简单格式转换器
SimpleFormatter simpleFormatter = new SimpleFormatter();
// 3.3 关联日志处理器和格式转换器
fileHandler.setFormatter(simpleFormatter);
// 3.4 给Logger对象添加我们创建的Handler,(因为在第2步阻止使用父日志记录器参数所以不会继承父日志记录器中的Handler有关继承请继续阅读)
logger.addHandler(fileHandler);
logger.severe("severe");
logger.warning("warning");
logger.info("info"); // 默认只会输出info以上级别的日志信息
logger.config("config");
logger.fine("fine");
logger.finer("finner");
logger.finest("finest");
}
代码块1
@Test
public void LogParentRelation(){
Logger logger =Logger.getLogger("");
Logger logger1 = Logger.getLogger("a");
Logger logger2 = Logger.getLogger("a.b");
Logger logger3 = Logger.getLogger("a.b.c");
System.out.println(logger2.getParent() == logger1);
System.out.println(logger3.getParent() == logger2);
System.out.println(logger1.getParent() == logger);
System.out.println("logger1'parent : " +logger1.getParent()+"/n" +
"It's Name : "+logger1.getParent().getName());
}
true true true logger1'parent : java.util.logging.LogManager$RootLogger@5e8c92f4 It's Name : |
结论就是我们在getLogger时传入的字符串会以点分割后被处理成有父子关系的,处理的方式请结合下面这个例子,当把上面一块代码注释并修改成下面这个样子:
代码块2
@Test
public void LogParentRelation(){
//Logger logger =Logger.getLogger("");
Logger logger1 = Logger.getLogger("a");
// Logger logger2 = Logger.getLogger("a.b");
Logger logger3 = Logger.getLogger("a.b.c");
//System.out.println(logger2.getParent() == logger1);
//System.out.println(logger3.getParent() == logger2);
//System.out.println(logger1.getParent() == logger);
System.out.println(logger3.getParent() == logger1);
System.out.println("logger3'parent : " +logger3.getParent()+"\n" +
"It's Name : "+logger3.getParent().getName());
}
true logger3'parent : java.util.logging.Logger@5e8c92f4 It's Name : a |
它的结果是这个样子的,另外就是需要说明的是如下例子的结果
代码块3
public void LogParentRelation(){
//Logger logger =Logger.getLogger("");
Logger logger1 = Logger.getLogger("a");
// Logger logger2 = Logger.getLogger("a.b");
// Logger logger3 = Logger.getLogger("a.b.c");
//System.out.println(logger2.getParent() == logger1);
//System.out.println(logger3.getParent() == logger2);
//System.out.println(logger1.getParent() == logger);
//System.out.println(logger3.getParent() == logger1);
System.out.println("logger1'parent : " +logger1.getParent()+"\n" +
"It's Name : "+logger1.getParent().getName());
}
logger1'parent : java.util.logging.LogManager$RootLogger@5e8c92f4 It's Name : |
也就是说我们的Logger对象会以点来处理父子关系,而默认情况下父子关系的顶端是一个Name为空串的Logger对象,这个对象并不是我们创建的,在代码块3中我注释掉了第一行,而logger1的父对象还是Name为空的Logger对象,这个对象其实是日志框架自动创建的。但是我们之后get的Name为非空串的Logger对象会被以类似java包结构的方式来组织父子关系。
通过查源码(①ctrl点击 Logger对象的getLogger方法,②ctrl点击getLogger方法体里面的demandLogger,③点击方法体第一行的getLogManager,④点击ensureLogMamaherInitialized,⑤之后进入的这个方法里面有个readPrimordialConfiguration,点击进去 ⑥进入的这个方法中有一个readConfiguration,点进去,⑦在这个方法中可以看出其会读取jre/lib下的一个名为logging.properties的配置文件),具体的目录例如我的在C:\Program Files\Java\jdk1.8.0_131\jre\lib\logging.properties,(视个人java安装位置而定)。我们可以复制一份到一个我们自认为可以方便读取的位置进行修改来自定义的默认生成的RootLogger(RootLogger就是上面我们说的那个Name为空串的Logger对象)。我修改后配置文件内容如下:
# RootLogger 顶级父元素指定的默认处理器:控制台输出
handlers= java.util.logging.ConsoleHandler
# RootLogger 顶级父元素的日志输出级别:ALL
.level= ALL
# RootLogger的FileHandler(输出到文件的处理器)的文件输出指定一个格式,格式具体如下
# “/” 本地路径分隔符
# "%t" 系统临时目录
# "%h" 用户家目录,具体到windows不同版本,Linux之间都有所不同,可自行搜索查询具体路径
# "%g" 区分循环日志的生成号
# "%u" 解决冲突的唯一号码
# “%%” 转义字符“%”
# 这个路径不能写在盘的根目录下 如:c:/java%u.log
java.util.logging.FileHandler.pattern = %h/java%u.log
# RootLogger的 FileHandler(输出到文件的处理器)的日志文件内容大小
java.util.logging.FileHandler.limit = 50000
# RootLogger的 FileHandler(输出到文件的处理器)的日志文件数量
java.util.logging.FileHandler.count = 1
# RootLogger的 FileHandler(输出到文件的处理器)的日志消息格式为XML格式
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
# 指定以追加方式添加日志内容,默认不是追加
java.util.logging.FileHandler.append = false
# 指定控制台输出的日志级别为INFO及以上
java.util.logging.ConsoleHandler.level = INFO
# 指定handler对象的消息格式对象为SimpleFormatter
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定handler对象的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8
#指定日志消息格式
java.util.logging.SimpleFormatter.format = %4$s: %5$s [%1$tc]%n
# 示例自定义的com.xyz.foo包的Logger的日志级别
a.b.c.handlers = java.util.logging.ConsoleHandler
a.b.c.level = SEVERE
#关闭默认配置
a.b.c.useParentHandlers = false
下面的是使用这个配置文件创建Logger对象的操作:
@Test
public void UseCustomConfiguration1() throws IOException {
InputStream stream = JULtest.class.getClassLoader().getResourceAsStream("logging.properties");
LogManager logManager = LogManager.getLogManager();
logManager.readConfiguration(stream);
Logger logger = Logger.getLogger("a.b.c");
//logger.setUseParentHandlers(false);
logger.severe("severe");
logger.fine("fine");
}
严重: severe [星期三 八月 25 11:43:36 CST 2021] |
Log4j是Apache下的一款开源的日志框架,通过在项目中使用Log4j,我们可以控制日志信息输出到控制台、文件、甚至是数据库中。我们可以控制每一条日志的输出格式,通过定义日志的输出级别,可以更灵活的控制日志的输出过程。方便项目的调试。
官方网站地址:https://logging.apache.org/log4j/1.2/
Log4j主要由Loggers(日志记录器)、Appenders(输出端)和Layout(日志格式化器)组成。其中Loggers控制日志的输出级别与日志是否输出;Appenders 指定日志的输出方式〈输出到控制台、文件等);Layout控制日志信息的输出格式。
日志记录器,负责收集处理日志记录,实例的命名就是类"XX”的full quailied name(类的全限定名),Logger的名字大小写敏感,其命名有继承机制:例如: name为org.apache.commons的logger会继承name为org.apache的logger。
Log4j中有一个特殊的logger叫做"root",他是所有logger的根,也就意味着其他所有的logger都会直接或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。
自log4j1.2版以来,Logger类已经取代了category类。对于熟悉早期版本的log4j的人来说,Logger类可以被视为category类的别名。
Appender用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。Log4j常用的输出目的地有以下几种:
输出端类型 | 作用 |
ConsoleAppender | 输出到控制台 |
FileAppender | 输出到文件 |
DailyRollingFileAppender | 输出到文件,指定按照时间输出到新文件进行拆分 |
RollingFileAppender | 输出到文件,可以指定日志文件的尺寸,当文件大小超过时,自动拆分产生新的日志文件 |
JDBCAppender | 将日志文件保存到数据库中 |
布局器Layouts用于控制日志输融内容的格式,让我们可以使用各种需要的格式输出日志。Log4j常用的Layouts:
格式化器类型 | 作用 |
HTMLLayout | 格式化输出为HTML表格的形式 |
SimpleLayout | 简单的日志输出格式化,格式为 info - message |
PatternLayout | 最强大的格式化器,可以根据自定义格式输出日志,如果没有指定,就是默认的转化格式 |
导入Maven依赖:
|
@Test
public void Log4jQuickStart(){
org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(Log4jTest.class); //内部也是调用l类对象的getName()
logger.fatal("最严重的");
logger.error("错误的");
logger.warn("警告的");
logger.info("提示信息");
logger.debug("调试信息");
logger.trace("追踪信息"); //级别依次降低
}
log4j:WARN No appenders could be found for logger (com.L.Log4jTest). 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给你提示,让你给你使用的这个Logger添加Appender等。因为没有往控制台等输出端打印的Appender,所以控制台,文件等都不会有我们程序中要打印的日志。
添加配置的方法:
①方法最开始添加:(这使用log4j最基本的配置)
org.apache.log4j.BasicConfigurator.configure();
②添加配置文件
在LogManager源码可以看到,它会尝试在类路径下加载一个名为log4j.xml的文件如果找不到则会加载一个类路径下的log4j.properties的文件,所以我们可以在类路径下添加这样的配置文件来添加配置
# 指定RootLogger顶级父元素的默认配置信息,依次是日志级别 使用的appenders(可以多个,逗号分割) 是console,console是下面你自定义的名字
log4j.rootLogger = trace,console,dailyrollingfile
# 指定一个名为console的appender,使用的是ConsoleAppender输向控制台
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定日志消息格式
#log4j.appender.console.layout = org.apache.log4j.SimpleLayout 简单格式
#log4j.appender.console.layout = org.apache.log4j.xml.XMLLayout XML格式
log4j.appender.console.layout = org.apache.log4j.PatternLayout
#日志格式,只在layout为PatternLayout时生效,因为它是该layout的属性
log4j.appender.console.layout.conversionPattern = %r [%t] %p %m %n
# %m 输出代码中指定的日志信息
# %p 输出优先级,及DEBUG、INFO等
# %n 换行符(Windows平台的换行符为"\r\n",Unix平台为"\n"")
# %r 输出自应用启动到输出该log信息耗费的毫秒数
# %c 输出打印语句所属的类的全名
# %t 输出产生该日志的线程全名
# %d 输出服务器当前时间,默认为IS08601,也可以指定格式,如:%d{yyyy年MWM月dd日HH :mm:ss.SSS}
# %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字符,就从左边交将超出的字符截掉
log4j.appender.file = org.apache.log4j.FileAppender
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.conversionPattern = %r [%t] [%-10m] %n
# 指定日志文件保存路径
log4j.appender.file.file = /logs0/log4j.log
# 指定日志文件的字符集
log4j.appender.file.encoding = UTF-8
#指定追加,其实默认就是追加方式
#log4j.appender.file.append = true
# org.apache.log4j.RollingFileAppender 继承了 org.apache.log4j.FileAppender
log4j.appender.rollingfile = org.apache.log4j.RollingFileAppender
log4j.appender.rollingfile.layout = org.apache.log4j.PatternLayout
log4j.appender.rollingfile.layout.conversionPattern = %r [%t] [%-10m] %n
log4j.appender.rollingfile.file = /logs1/log4j.log
log4j.appender.rollingfile.encoding = UTF-8
# 指定单个日志文件大小
log4j.appender.rollingfile.fileSize = 1MB
# 指定日志文件最大下标,总数量5个 下标从0-4 log4j.log.1 log4j.log.2 log4j.log.3 log4j.log.4
log4j.appender.rollingfile.maxBackupIndex = 5
# org.apache.log4j.DailyRollingFileAppender 继承 org.apache.log4j.FileAppender
log4j.appender.dailyrollingfile = org.apache.log4j.DailyRollingFileAppender
log4j.appender.dailyrollingfile.layout = org.apache.log4j.PatternLayout
log4j.appender.dailyrollingfile.layout.conversionPattern = %r [%t] [%-10m] %n
log4j.appender.dailyrollingfile.file = /logs2/log4j.log
log4j.appender.dailyrollingfile.encoding = UTF-8
#指定日期拆分规则 注意不要有 ":"
log4j.appender.dailyrollingfile.datePattern = '.'yyyy-MM-dd-HH-mm-ss
只需在类路径下添加log4j.properties的文件,文件内容可参考以上配置文件,值得一提的是这里面对应的都是给指定的类的属性赋值,可以查找到指定的类根据里面的setter方法获得更为详细的配置。另外有关JDBCAppender和自定义Logger配置请继续阅读。
添加Maven依赖
|
创建数据库表
CREATE TABLE `log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`create_date` varchar(120) DEFAULT NULL COMMENT '创建时间',
`level` varchar(20) DEFAULT NULL COMMENT '日志级别',
`class` 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(512) DEFAULT NULL COMMENT '日志内容',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
log4j.properties内容
log4j.rootLogger = trace,console,logDB
#JDBC
log4j.appender.logDB = org.apache.log4j.jdbc.JDBCAppender
log4j.appender.logDB.Driver = com.mysql.cj.jdbc.Driver
log4j.appender.logDB.User = root
log4j.appender.logDB.Password = 123
log4j.appender.logDB.sql = \
INSERT INTO log(create_date,level,class,file_name,thread_name,line,all_category,message) \
VALUES('%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')
log4j.appender.logDB.layout = org.apache.log4j.PatternLayout
log4j.appender.logDB.URL = jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&tinyInt1isBit=true
测试代码依然是入门代码,因为所有的Logger都会继承RootLogger,对RootLogger的配置,我们在getLogger时得到的logger对象都可以使用继承得到的JDBCAppender。结果如下:
# 指定RootLogger顶级父元素的默认配置信息,依次是日志级别 使用的appenders(可以多个,逗号分割) 是console,console是下面你自定义的名字
log4j.rootLogger = trace,console,logDB
# 自定义 logger 对象设置
log4j.logger.com.L = info,console
# 禁止继承rootLogger,否则因为上面一句配置中rootLogger也配置了console,你会发现控制台会输出两次
log4j.additivity.com.L = false
通过类比可以看到就是将原来给root对象的配置给了一个"log4j.logger.你自己的层级结构"就可以指定对应串的Logger对象默认设置。
JCL全称为Jakarta Commons Logging,是Apache提供的一个通用日志API。
它是为"所有的Java日志实现"提供一个统一的接口,它自身也提供一个日志的实现(SimpleLog),但是功能非常弱。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具:Log4j、Jdk自带的日志JUL
JCL有两个基本的抽象类:Log(基本记录器)和LogFactory(负责创建Log实例)。
在代码中我们JCL一套接口,当我们根据需要改变底层日志输出框架的时候仅仅只需要加入日志实现的依赖即可,而不需要改变代码,(通过上面的学习我们知道log4j和JUL两个在使用的时候需要导入不同包下的Logger,而且两个的api输出也不一样,我们使用统一的api——JCL的API,然后JCL的底层会根据系统有没有对应的依赖来判断使用JUL还是log4j,这样我们维护项目想要更换日志框架的时候只需要改变jar包依赖即可,这也就是日志门面和日志实现的思想)
导入Maven依赖
|
编写代码
public class JCLTest {
public static Log LOGGER = LogFactory.getLog(JCLTest.class);
@Test
public void JCLTest(){
LOGGER.fatal("最严重的");
LOGGER.error("严重的");
LOGGER.warn("警告的");
LOGGER.info("信息");
LOGGER.debug("调试信息");
LOGGER.trace("追踪信息");
}
}
八月 25, 2021 10:29:10 下午 com.L.JCLTest JCLTest 严重: 最严重的 八月 25, 2021 10:29:10 下午 com.L.JCLTest JCLTest 严重: 严重的 八月 25, 2021 10:29:10 下午 com.L.JCLTest JCLTest 警告: 警告的 八月 25, 2021 10:29:10 下午 com.L.JCLTest JCLTest 信息: 信息 |
可以看出我们只导入了一个jcl的依赖,但是因为JUL是jdk自带的不需要导入依赖的缘故,控制台默认输出了JUL处理的结果,并且JCL的API中也是有6个日志级别,且方法名和log4j基本一样,只有日志记录器的获取方法与JUL和log4j不同。
加入如下依赖
|
重新运行测试代码结果如下
log4j:WARN No appenders could be found for logger (com.L.JCLTest). 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的执行结果。
1、通过LogFactory动态加载Log实现类
2、LogFactory是一个抽象类其实现LogFactoryImpl中有如下代码
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"};
...
for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
result = this.createLogFromClass(classesToDiscover[i], logCategory, true);
}
而LogFactory是根据这个顺序加载日志的实现的,所以我们导入了Log4j的依赖之后Jdk的JUL实现就不能使用了。而当有新的日志实现需要接如的时候就需要新的LogFactory实现了,所以总体来说JCL还是很有局限的,请继续看后面的Slf4j
当我们的系统变的更加复杂的时候,我们的日志就容易发生混乱。随着系统开发的进行,可能会更新不同的日志框架,造成当前系统中存在不同的日志依赖,让我们难以统一的管理和控制。就算我们强制要求所有的模块使用相同的日志框架,系统中也难以避免使用其他类spring,mybatis等其他的第三方框架,它们依赖于我们规定不同的日志框架,而且他们自身的日志系统就有着不一致性,依然会出来日志体系的混乱。所以我们需要借鉴JDBC的思想,为日志系统也提供一套门面,那么我们就可以面向这些接口规范来开发,避免了直接依赖具体的日志框架。这样我们的系统在日志中,就存在了日志的门面和日志的实现。
有关日志门面和日志实现大家在上面JCL的学习中应该有一定的理解,请继续学习Slf4J加深理解
简单日志门面(Simple Logging Facade For Java)SLF4]主要是为了给java日志访问提供一个标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架,中间使用桥接器进行桥接。
导入Maven坐标,使用slf4j自己的简单实现
|
java代码
public class Slf4jTest {
@Test
public void Slf4jTest(){
Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
logger.error("错误");
logger.warn("警告");
logger.info("提示");
logger.debug("调试");
logger.trace("追踪");
//使用占位符
logger.error("{}{}{}","博主","真帅","!");
//输出异常
try{
int a = 1/0;
}catch (Exception e){
logger.error("异常:",e);
}
}
}
[main] ERROR com.L.Slf4jTest - 错误 |
可以看到,占位符不需要像JUL一样使用数字标了,而且改成了可变参数不用再new一个数组。还可以直接输出异常信息以日志的方式
这是slf4j官网上的一个图,仔细观看就能发现这套结构。我从左到右依次介绍
第一列:如果你只导入了slf4j-api这个依赖,默认情况下slf4j会使用nop实现,也就是效果和最后一列中你导入slf4j-nop的依赖一样(这个依赖只有709B) nop的意思是no-operator (误操作)
第二列:讲到的是logback,logback实现了slf4j的接口,所以不需要适配即可直接被使用就和第四列的slf4j-simple一样直接导入对应依赖即可使用。
第三列和第四列:使用log4j和jul都需要导入对应的适配包
综上所述得到如下结论
使用slf4j+JUL:
|
使用slf4j+log4j:
|
或(因为点击进log4j12中会发现这个项目已经导入了log4j)
|
使用logback日志实现:(在logback-classic里面已经有logback-core的坐标了)
|
如前所述,SLF4J支持各种日志框架。SLF4J发行版附带了几个称为“SLF4J绑定"的jar文件,每个绑定对应一个受支持的框架。
使用slf4j的日志绑定流程:
1、添加slf4j-api的依赖
2、使用slf4j的API在项目中进行统一的日志记录
3、绑定具体的日志实现框架
1、绑定已经实现了slf4j的日志框架,直接添加对应依赖
2、绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
4、slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)
这个是slf4j官网上有关桥接部分的一张图。
有时候我们的项目要更换日志的实现,但是可能因为项目很老,使用的是slf4j之前的框架的API,如log4j、JUL、JCL等,这时我们就需要使用图中给出的桥阶层api,如 jcl-over-slf4j.jar、log4j-over-slf4j.jar、...,其作用就是我们在项目中可以使用以前旧的日志框架的api,然后这些api真正调用的并不是这些旧的日志框架的具体实现,而是被调包的桥接包,在桥接包中会调用slf4j-api.jar中的API,进而实现日志编写和实现上的灵活切换。
值得一提的是,请不要在使用时造成循环调用,也就是类似同时导入log4j-over-slf4j.jar(作用:项目调用log4jAPI实际是用的在log4j-over-slf4j.jar中log4j的实现,这个实现调用的slf4jAPI)、slf4j-log4j12.jar(作用:调用slf4jAPI,实际调用的是log4j的API),这就会导致死循环。类似的还有jcl-over-slf4j.jar和slf4j-jcl.jar不同同时出现,jul-to-slf4j和slf4j-jdk14.jar不能同时出现
所有的桥接都只对Logger日志记录器对象有效,如果程序中调用了内部的配置类或者是Appender,Filter等对象,将无法产生效果。
1. SLF4J通过LoggerFactory加载日志具体的实现对象。体日志实现的LoggerFactory就可以被SLF4J所加载
2.LoggerFactory在初始化的过程中,会通过performInitialization()方法绑定具体的日志实现。
3.在绑定具体实现的时候,通过类加载器,加载org/slf4j/impl/StaticLoggerBinder.class
4.所以,只要是一个日志实现框架,在org.slf4j.impl包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J所加载。
这是把几种实现都导入得到的搜索结果:
有关 jar包以及自己代码中出现相同包名及类名的类时请看这个:
https://blog.csdn.net/birdmain/article/details/84916621
logback是由log4j创始人设计的另一个开源日志组件,性能要好于log4j
官网:https://logback.qos.ch
Logback主要分为三个模块:
Maven依赖:
|
代码仿照slf4j入门代码。
11:28:07.430 [main] ERROR com.L.Slf4jTest - 错误 11:28:07.430 [main] WARN com.L.Slf4jTest - 警告 11:28:07.430 [main] INFO com.L.Slf4jTest - 提示 11:28:07.430 [main] DEBUG com.L.Slf4jTest - 调试 11:28:07.430 [main] ERROR com.L.Slf4jTest - 博主真帅! 11:28:07.430 [main] ERROR com.L.Slf4jTest - 异常: |
logback会依次读取以下类型配置文件:
1、logback.groovy
2、logback-tgst.xml
3、logback.xml 如果均不存在会采用默认配置
1.、logback组件之间的关系
1、Logger:日志的记录器,把它关联到应用的对应的context上后,主要用于存放日志对象,也可以定义日志类型、级别。
2、Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等。
3、Layout:负责把事件转换成字符串,格式化的日志信息的输出。在logback中Layout对象被封装在encoder中。
System.err
${pattern}
${LOGDIR}/log.log
${pattern}
${LOGDIR}/log.html
%-5level%d{yyyy-MM-dd HH:mm:ss.SSS}%c%M%L%thread%m
${LOGDIR}/xml.log
${pattern}
${LOGDIR}/rolling.%d{yyyy_MM_dd_HH}.log%i.gz
2MB
INFO
DENY
ACCEPT
还是按照那套思想,更详细的请查阅更多资料。
在logback官网上有一个可以将log4j.properties转化为logback.xml的小转化器可以写配置,但是生成的配置有部分地方需要修改:http://logback.qos.ch/translator/
logback-access模块与Servlet容器(如Tomcat和Jetty)集成,以提供HTTP访问日志功能。我们可以使用logback-access模块来替换tomcat的访问日志。
1、将logback-access.jar与logback-core.jar复制到$TOMCAT_HOME/lib/目录下
2、修改$TOMCAT_HOME/conf/server.xml中的Host元素中添加(注释掉原来的):
3、logback默认会在$TOMCAT_HOME/conf下查找文件logback-access.xml
combined
${LOGDIR}/rolling.%d{yyyy_MM_dd_HH}.log%i.gz
2MB
其中的"combined"需要解释一下,在官网有解释,它是官方给我们的简写形式:
https://logback.qos.ch/access.html
其相当于图片中上面的配置。
这样在我们访问之后就会生成tomcat的访问日志到我们appender指定的位置。
Apache Log4j2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:
- 异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异常处理机制。
- 性能提升,log4j2相较于log4j和logback都具有很明显的性能提升,后面会有官方测试的数据。
- 自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产上可以动态的修改日志的级别而不需要重启应用。
- 无垃圾机制,log4j2在大部分情况下,都可以使用其设计的一套无垃圾机制,避免频繁的日志收集导致的jvm gc。
官网:http://logging.apache.org/log4j/2.x/
目前市面上最主流的日志门面就是SLF4],虽然Log4j2也是日志门面,因为它的日志实现功能非常强大,性能优越。所以大家一般还是将Log4j2看作是日志的实现,SIf4j +Log4j2应该是未来的大势所趋。
Maven依赖(log4j2自己的日志门面+自己的实现)
|
Java代码
public static Logger LOGGER = LogManager.getLogger(Log4j2Test.class);
@Test
public void Log4j2Test(){
LOGGER.fatal("致命的");
LOGGER.error("错误的");
LOGGER.warn("警告的");
LOGGER.info("提示的");
LOGGER.debug("调试的");
LOGGER.trace("追踪的");
}
13:37:34.465 [main] FATAL Log4j2Test - 致命的 13:37:34.469 [main] ERROR Log4j2Test - 错误的 |
默认输出级别是ERROR以上
MAVEN(使用Log4j2日志实现+Slf4J门面)
|
刚发现有些版本jar包maven坐标可以搜到但是阿里云下载不到,低版本的log4j2的桥接包可能会遇到有些类找不到的情况,请一定找好适配版本的桥接包和实现包。
默认加载类路径下log4j2.xml
/log4j2log
这是log4j官网上有关异步日志的测评数据。
Log4j2提供了两种异步日志的实现方式,一个是通过AsynAppender,一个是通过AsyncLogger,分别对应Appender和Logger,还有一种是图片中的mixed方式,也是部分Logger使用,其本质还是AsyncLogger方式。
配置方式:
maven依赖
|
AsyncAppender方式:
在原来的Appenders下添加
AsyncLogger方式:
根据图片可以看出AsyncLogger才是显著提升Log4j2性能的因素,这也是官方推荐的异步方式。它又分为全局异步和混合异步两种,全局异步就是所有的日志记录器都异步的输出
全局异步:所有的日志都异步的记录,在配置文件上不用做任何改动,只需要添加一
个log4j2.component.properties配置;
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
混合异步:你可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活。配置如下,顺带和自定义配置一起,这样a.b.c为名字以及子Logger的日志就是异步的。
使用异步日志需要注意的问题:
1.如果使用异步日志,AsyncAppender、AsyncLogger和全局日志,不要同时出现。性能会和AsyncAppender—致,降至最低。
2.设置includeLocation=false,打印位置信息会急剧降低异步日志的性能,比同步日志还要慢。
在7.3中我们看到Log4j2在多线程的环境下吞吐量与Log4j和Logback的比较,采用全局Logger和部分Logger模式下Log4j2的性能大幅领先。
无垃圾记录
垃圾收集暂停是延迟峰值的常见原因,并且对于许多系统而言,花费大量精力来控制这些暂停。许多日志库(包括以前版本的Log4j)在稳态日志记录期间分配临时对象,如日志事件对象,字符串,字符数组,字节数组等。这会对垃圾收集器造成压力并增加GC暂停发生的频率。
从版本2.6开始,默认情况下Log4j以“无垃圾”模式运行,其中重用对象和缓冲区,并且尽可能不分配临时对象。还有一个“低垃圾”模式,它不是完全无垃圾,但不使用ThreadLocal字段。
Log4j2.6中的无垃圾日志记录部分通过重用ThreadLocal字段中的对象来实现,部分通过在将文本转换为字节时重用缓冲区来实现。
使用Log4j2.5:内存分配速率809 MB/秒,141个无效集合。
Log4j2.6没有分配临时对象:0 垃圾回收。
有两个单独的系统属性可用于手动控制Log4j2用于避免创建临时对象的机制:
log4j2.enableThreadlocals:如果"true”(非Web应用程序的默认值)对象存储在ThreadLocal字段中并重新使用,否则将为每个日志事件创建新对象。
log4j2.enableDirectEncoders:如果将"true”(默认是true)日志事件转换为文本,则将此文本转换为字节而不创建临时对象。注意:由于共享缓冲区上的同步,在此模式下多线程应用程序的同步日志记录性能可能更差。如果您的应用程序是多线程的并且日志记录性能很重要,请考虑使用异步记录器。