6️⃣配置文件
我们看看一个文件处理器的源码是怎么读配置项的:
private void configure() { LogManager manager = LogManager.getLogManager(); String cname = getClass().getName(); pattern = manager.getStringProperty(cname + ".pattern", "%h/java%u.log"); limit = manager.getLongProperty(cname + ".limit", 0); if (limit < 0) { limit = 0; } count = manager.getIntProperty(cname + ".count", 1); if (count <= 0) { count = 1; } append = manager.getBooleanProperty(cname + ".append", false); setLevel(manager.getLevelProperty(cname + ".level", Level.ALL)); setFilter(manager.getFilterProperty(cname + ".filter", null)); setFormatter(manager.getFormatterProperty(cname + ".formatter", new XMLFormatter())); // Initialize maxLocks from the logging.properties file. // If invalid/no property is provided 100 will be used as a default value. maxLocks = manager.getIntProperty(cname + ".maxLocks", MAX_LOCKS); if(maxLocks <= 0) { maxLocks = MAX_LOCKS; } try { 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; } } }
可以从以下源码中看到配置项:
public class FileHandler extends StreamHandler { private MeteredStream meter; private boolean append; // 限制文件大小 private long limit; // zero => no limit. // 控制日志文件的数量 private int count; // 日志文件的格式化方式 private String pattern; private String lockFileName; private FileChannel lockFileChannel; private File files[]; private static final int MAX_LOCKS = 100; // 可以理解为同时可以有多少个线程打开文件,源码中有介绍 private int maxLocks = MAX_LOCKS; private static final Set<String> locks = new HashSet<>(); }
我们已经知道系统默认的配置文件的位置,那我们能不能自定义呢?当然可以了,我们从jdk中赋值一个配置文件过来:
.level= INFO # default file output is in user's home directory. java.util.logging.FileHandler.pattern = %h/java%u.log java.util.logging.FileHandler.limit = 50000 java.util.logging.FileHandler.count = 1 # Default number of locks FileHandler can obtain synchronously. # This specifies maximum number of attempts to obtain lock file by FileHandler # implemented by incrementing the unique field %u as per FileHandler API documentation. java.util.logging.FileHandler.maxLocks = 100 java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter # Limit the message that are printed on the console to INFO and above. java.util.logging.ConsoleHandler.level = INFO java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
pattern = manager.getStringProperty(cname + ".pattern", "%h/java%u.log");
static File generate(String pat, int count, int generation, int unique) throws IOException { Path path = Paths.get(pat); Path result = null; boolean sawg = false; boolean sawu = false; StringBuilder word = new StringBuilder(); Path prev = null; for (Path elem : path) { if (prev != null) { prev = prev.resolveSibling(word.toString()); result = result == null ? prev : result.resolve(prev); } String pattern = elem.toString(); int ix = 0; word.setLength(0); while (ix < pattern.length()) { char ch = pattern.charAt(ix); ix++; char ch2 = 0; if (ix < pattern.length()) { ch2 = Character.toLowerCase(pattern.charAt(ix)); } if (ch == '%') { if (ch2 == 't') { String tmpDir = System.getProperty("java.io.tmpdir"); if (tmpDir == null) { tmpDir = System.getProperty("user.home"); } result = Paths.get(tmpDir); ix++; word.setLength(0); continue; } else if (ch2 == 'h') { result = Paths.get(System.getProperty("user.home")); if (jdk.internal.misc.VM.isSetUID()) { // Ok, we are in a set UID program. For safety's sake // we disallow attempts to open files relative to %h. throw new IOException("can't use %h in set UID program"); } ix++; word.setLength(0); continue; } else if (ch2 == 'g') { word = word.append(generation); sawg = true; ix++; continue; } else if (ch2 == 'u') { word = word.append(unique); sawu = true; ix++; continue; } else if (ch2 == '%') { word = word.append('%'); ix++; continue; } } word = word.append(ch); } prev = elem; } if (count > 1 && !sawg) { word = word.append('.').append(generation); } if (unique > 0 && !sawu) { word = word.append('.').append(unique); } if (word.length() > 0) { String n = word.toString(); Path p = prev == null ? Paths.get(n) : prev.resolveSibling(n); result = result == null ? p : result.resolve(p); } else if (result == null) { result = Paths.get(""); } if (path.getRoot() == null) { return result.toFile(); } else { return path.getRoot().resolve(result).toFile(); } }
System.out.println(System.getProperty("user.home") );
我们将拷贝的文件稍作修改:
.level= INFO # default file output is in user's home directory. java.util.logging.FileHandler.pattern = D:/log/java%u.log java.util.logging.FileHandler.limit = 50000 java.util.logging.FileHandler.count = 1 java.util.logging.FileHandler.maxLocks = 100 java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter # Limit the message that are printed on the console to INFO and above. java.util.logging.ConsoleHandler.level = INFO java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
@Test public void testProperties() throws Exception { // 读取自定义配置文件 InputStream in = JULTest.class.getClassLoader().getResourceAsStream("logging.properties"); // 获取日志管理器对象 LogManager logManager = LogManager.getLogManager(); // 通过日志管理器加载配置文件 logManager.readConfiguration(in); Logger logger = Logger.getLogger("com.ydlclass.log.JULTest"); logger.severe("severe"); logger.warning("warning"); logger.info("info"); logger.config("config"); logger.fine("fine"); logger.finer("finer"); logger.finest("finest"); }
配置文件:
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler .level= INFO 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.maxLocks = 100 java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter # Limit the message that are printed on the console to INFO and above. java.util.logging.ConsoleHandler.level = INFO java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
文件中也出现了:
打开日志发现是xml,因为这里用的就是XMLFormatter:
上边我们配置了两个handler给根Logger,我们还可以给其他的Logger做独立的配置:
handlers = java.util.logging.ConsoleHandler .level = INFO # 对这个logger独立配置 com.ydlclass.handlers = java.util.logging.FileHandler com.ydlclass.level = ALL com.ydlclass.useParentHandlers = false # 修改了名字 java.util.logging.FileHandler.pattern = D:/logs/ydl-java%u.log java.util.logging.FileHandler.limit = 50000 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.append = true # Limit the message that are printed on the console to INFO and above. java.util.logging.ConsoleHandler.level = INFO java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # 修改日志格式 java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
执行发现控制台没有内容,文件中有了,说明没有问题OK了:
日志出现以下内容:
三、Log4j日志框架
Log4j是Apache下的一款开源的日志框架。 官方网站:http://logging.apache.org/log4j/1.2/ ,这是一款比较老的日志框架,目前新的log4j2做了很大的改动,任然有一些项目在使用log4j。
1️⃣Log4j入门
🍀(1)建立maven工程
🍀(2)添加相关依赖
<dependencies> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>${maven.compiler.source}</source> <target>${maven.compiler.target}</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build>
🍀(3)编写java代码
@Test public void testLogger() { Logger logger = Logger.getLogger(Log4jTest.class); // 日志记录输出 logger.info("hello log4j"); // 日志级别 logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃和终止运行 logger.error("error"); // 错误信息,但不会影响系统运行 logger.warn("warn"); // 警告信息,可能会发生问题 logger.info("info"); // 程序运行信息,数据库的连接、网络、IO操作等 logger.debug("debug"); // 调试信息,一般在开发阶段使用,记录程序的变量、参数等 logger.trace("trace"); // 追踪信息,记录程序的所有流程信息 }
发现会有一些警告,JUL可以直接在控制台输出是因为他有默认的配置文件,而这个独立的第三方的日志框架却没有配置文件:
log4j:WARN No appenders could be found for logger (com.wang.entity.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.
我们在执行代码之前,加上以下代码,它会初始化一个默认配置:
BasicConfigurator.configure();
🍀(4)结果与分析
0 [main] INFO com.wang.entity.Log4jTest - hello log4j 1 [main] FATAL com.wang.entity.Log4jTest - fatal 1 [main] ERROR com.wang.entity.Log4jTest - error 1 [main] WARN com.wang.entity.Log4jTest - warn 1 [main] INFO com.wang.entity.Log4jTest - info 1 [main] DEBUG com.wang.entity.Log4jTest - debug
从源码看,这一行代码给我们的RootLogger加入一个控制台的输出源,就和JUL中的handler一样:
public static void configure() { Logger root = Logger.getRootLogger(); root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n"))); }
log4j定义了以下的日志的级别,和JUL的略有不同:
fatal 指出每个严重的错误事件将会导致应用程序的退出。
error 指出虽然发生错误事件,但仍然不影响系统的继续运行。
warn 表明会出现潜在的错误情形。
info 一般和在粗粒度级别上,强调应用程序的运行全程。
debug 一般用于细粒度级别上,对调试应用程序非常有帮助。
trace 是程序追踪,可以用于输出程序运行中的变量,显示执行的流程。
和JUL一样,log4j还有两个特殊的级别:OFF,可用来关闭日志记录。 ALL,启用所有消息的日志记录。
一般情况下,我们只使用4个级别,优先级从高到低为:ERROR > WARN > INFO > DEBUG。
2️⃣Log4j组件讲解
Log4J 主要由 Loggers (日志记录器)、Appenders(输出端)和 Layout(日志格式化器)组成。其中 Loggers 控制日志的输出级别与日志是否输出;Appenders 指定日志的输出方式(输出到控制台、文件 等);Layout 控制日志信息的输出格式。
🍀(1)Loggers
日志记录器:负责收集处理日志记录,实例的命名就是类“XX”的full quailied name(类的全限定名), Logger的名字大小写敏感,其命名有继承机制:例如:name为com.ydlclass.service的logger会继承 name为com.ydlclass的logger,和JUL一致。
Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接 或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。 JUL是不是也有一个名为.的根。
🍀(2)Appenders
Appender和JUL的Handler很像,用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。Log4j 常用的输出目的地 有以下几种:
输出端类型 | 作用 |
ConsoleAppender | 将日志输出到控制台 |
FileAppender | 将日志输出到文件中 |
DailyRollingFileAppender | 将日志输出到一个日志文件,并且每天输出到一个新的文件 |
RollingFileAppender | 将日志信息输出到一个日志文件,并且指定文件的尺寸,当文件大 小达到指定尺寸时,会自动把文件改名,同时产生一个新的文件 |
JDBCAppender | 把日志信息保存到数据库中 |
// 配置一个控制台输出源 ConsoleAppender consoleAppender = new ConsoleAppender(); consoleAppender.setName("ydl"); consoleAppender.setWriter(new PrintWriter(System.out)); logger.addAppender(consoleAppender);
🍀(3)Layouts
Layout layout = new Layout() { @Override public String format(LoggingEvent loggingEvent) { return loggingEvent.getLoggerName() + " " +loggingEvent.getMessage() + "\r\n"; } @Override public boolean ignoresThrowable() { return false; } @Override public void activateOptions() { } };
有一些默认的实现类:
Layout layout = new SimpleLayout();
3️⃣Log4j配置
log4j不仅仅可以在控制台,文件文件中输出日志,甚至可以在数据库中,我们先使用配置的方式完成日志的输入:
#指定日志的输出级别与输出端 log4j.rootLogger=INFO,Console,ydl # 控制台输出配置 log4j.appender.Console=org.apache.log4j.ConsoleAppender log4j.appender.Console.layout=org.apache.log4j.PatternLayout log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n # 文件输出配置 log4j.appender.ydl = org.apache.log4j.DailyRollingFileAppender #指定日志的输出路径 log4j.appender.ydl.File = D:/logs/ydl.log log4j.appender.ydl.Append = true #使用自定义日志格式化器 log4j.appender.ydl.layout = org.apache.log4j.PatternLayout #指定日志的输出格式 log4j.appender.ydl.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] -[%p] %m%n #指定日志的文件编码 log4j.appender.ydl.encoding=UTF-8
有了这个配置文件我们些代码就简单了一些:
@Test public void testConfig(){ // 获取一个logger Logger logger = Logger.getLogger(TestLog4j.class); logger.warn("warning"); } 结果: 2021-10-21 21:37:06,705 [main] WARN [com.wang.TestLog4j] - warning
同时日志文件也会产生:
日志文件内容如下:
当然日志配置文件是什么时候读取的呢?每一个logger都是LogManager创建的,而LogManager有一个静态代码块帮助我们解析配置文件。
我们还可以直接添加一个数据源,将日志输出到数据库中,就是一个和数据库链接的输出源。
加入一个数据库的日志输出源:
#mysql log4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppender log4j.appender.logDB.layout=org.apache.log4j.PatternLayout log4j.appender.logDB.Driver=com.mysql.cj.jdbc.Driver log4j.appender.logDB.URL=jdbc:mysql://localhost:3306/ssm log4j.appender.logDB.User=root log4j.appender.logDB.Password=root log4j.appender.logDB.Sql=INSERT INTO log(project_name,create_date,level,category,file_name,thread_name,line,all_category,message) values('ydlclass','%d{yyyy-MM-ddHH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')
需要创建保存日志的数据表:
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`) );
在pom.xml中添加驱动:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.22</version> </dependency>
再次执行,发现除了控制台,文件,数据库中也有了日志了:
4️⃣自定义Logger
# RootLogger配置 log4j.rootLogger = trace,console # 自定义Logger log4j.logger.com.ydlclass= WARN,logDB log4j.logger.org.apache = error
由此我们发现,我们可以很灵活的自定义,组装不同logger的实现,接下来我们写代码测试:
@Test public void testDefineLogger() throws Exception { Logger logger1 = Logger.getLogger(Log4jTest.class); logger1.fatal("fatal"); // 严重错误,一般会造成系统崩溃和终止运行 logger1.error("error"); // 错误信息,但不会影响系统运行 logger1.warn("warn"); // 警告信息,可能会发生问题 logger1.info("info"); // 程序运行信息,数据库的连接、网络、IO操作等 logger1.debug("debug"); // 调试信息,一般在开发阶段使用,记录程序的变量、参数等 logger1.trace("trace"); // 追踪信息,记录程序的所有流程信息 // 自定义 org.apache Logger logger2 = Logger.getLogger(Logger.class); logger2.fatal("fatal logger2"); // 严重错误,一般会造成系统崩溃和终止运行 logger2.error("error logger2"); // 错误信息,但不会影响系统运行 logger2.warn("warn logger2"); // 警告信息,可能会发生问题 logger2.info("info logger2"); // 程序运行信息,数据库的连接、网络、IO操作等 logger2.debug("debug logger2"); // 调试信息,一般在开发阶段使用,记录程序的变量、参数等 logger2.trace("trace logger2"); // 追踪信息,记录程序的所有流程信息 }
我们发现logger1的日志级别成了warn,并且在数据库中有了日志,logger2级别成了error,他们其实都继承了根logger的一些属性。
四、日志门面
当我们的系统变的复杂的之后,难免会集成其他的系统,不同的系统之间可能会使用不同的日志系统。那么在一个系统中,我们的日志框架可能会出现多个,会出现混乱,而且随着时间的发展,可能会出现新的效率更高的日志系统,如果我们想切换代价会非常的大。如果我们的日志系统能和jdbc一样,有一套自己的规范,其他实现均按照规范去实现,就能很灵活的使用日志框架了。
日志门面就是为了解决这个问题而出现的一种技术,日志门面是规范,其他的实现按照规范实现各自的日志框架即可,我们程序员基于日志门面编程即可。举个例子:日志门面就好比菜单,日志实现就好比厨师,我们去餐馆吃饭按照菜单点菜即可,厨师是谁其实不重要,但是有一个符合我口味的厨师当然会更好。
常见的日志门面: JCL、slf4j
常见的日志实现: JUL、log4j、logback、log4j2
日志框架出现的历史顺序: log4j -->JUL–>JCL–> slf4j --> logback --> log4j2
1️⃣Slf4j日志门面
简单日志门面(Simple Logging Facade For Java) SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。 当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架 会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。官方网站: https://www.slf4j.org/
SLF4J是目前市面上最流行的日志门面。现在的项目中,基本上都是使用SLF4J作为我们的日志系统。
SLF4J日志门面主要提供两大功能:
日志框架的绑定
日志框架的桥接
🍀(1)阿里日志规约
(1)应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API。使用门面模式的日志框架,有利于维护和各个类的日志处理方法统一。
(2)日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。
(3)应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType为日志类型,推荐分类有stats/monitor/visit等;
(4)logName为日志描述。这种命名的好处:通过文件名就可以知道日志文件属于哪个应用,哪种类型,有什么目的,这也有利于归类查找。
(5)对trace/debug/info级别的日志输出,必须使用条件输出形式或者占位符的方式。
(6)避免重复打印日志,否则会浪费磁盘空间。务必在日志配置文件中设置additivity=false。
(7)异常信息应该包括两类:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字向上抛出。
(8)谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免吧服务器磁盘撑爆,并及时删除这些观察日志。
(9)可以使用warn日志级别记录用户输入参数错误的情况,避免当用户投诉时无所适从。
🍀(2)SLF4J实战
(1)添加依赖
<!--slf4j core 使用slf4j必須添加--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.27</version> </dependency> <!--slf4j 自带的简单日志实现 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.27</version> </dependency>
(2)编写测试代码
public class TestSlf4j { // 声明日志对象 public final static Logger LOGGER = LoggerFactory.getLogger(TestSlf4j.class); @Test public void testSlfSimple() { //打印日志信息 LOGGER.error("error"); LOGGER.warn("warn"); LOGGER.info("info"); LOGGER.debug("debug"); LOGGER.trace("trace"); // 使用占位符输出日志信息 String name = "lucy"; Integer age = 18; LOGGER.info("{}今年{}岁了!", name, age); // 将系统异常信息写入日志 try { int i = 1 / 0; } catch (Exception e) { // e.printStackTrace(); LOGGER.info("出现异常:", e); } } }
🍀(3)绑定其他日志的实现(Binding)
如前所述,SLF4J支持各种日志框架。SLF4J发行版附带了几个称为“SLF4J绑定”的jar文件,每个绑定对应一个受支持的框架。
使用slf4j的日志绑定流程:
(1)添加slf4j-api的依赖
(2)使用slf4j的API在项目中进行统一的日志记录
(3)绑定具体的日志实现框架
a. 绑定已经实现了slf4j的日志框架,直接添加对应依赖
b. 绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
(4)slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)
绑定JUL的实现:
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.27</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.25</version> </dependency>
绑定log4j的实现:
<!--slf4j core 使用slf4j必須添加--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.27</version> </dependency> <!-- log4j--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.27</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
要切换日志框架,只需替换类路径上的slf4j绑定。例如,要从java.util.logging切换到log4j,只需将 slf4j-jdk14-1.7.27.jar替换为slf4j-log4j12-1.7.27.jar即可。
SLF4J不依赖于任何特殊的类装载。实际上,每个SLF4J绑定在编译时都是硬连线的, 以使用一个且只有 一个特定的日志记录框架。例如,slf4j-log4j12-1.7.27.jar绑定在编译时绑定以使用log4j。
🍀(4)桥接旧的日志框架(Bridging)
通常,您依赖的某些组件依赖于SLF4J以外的日志记录API。您也可以假设这些组件在不久的将来不会切换到SLF4J。为了解决这种情况,SLF4J附带了几个桥接模块,这些模块将对log4j,JCL和 java.util.logging API的调用重定向,就好像它们是对SLF4J API一样。
就是你还用log4j的api写代码,但是具体的实现给你抽离了,我们依赖了一个中间层,这个层其实是用旧的api操作slf4j,而不是操作具体的实现。
桥接解决的是项目中日志的遗留问题,当系统中存在之前的日志API,可以通过桥接转换到slf4j的实现:
(1)先去除之前老的日志框架的依赖,必须去掉。
(2)添加SLF4J提供的桥接组件,这个组件就是模仿之前老的日志写了一套相同的api,只不过这个api是在调用slf4j的api。
(3)为项目添加SLF4J的具体实现。
迁移的方式:
<!-- 桥接的组件 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.27</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.27</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.27</version> </dependency>
SLF4J提供的桥接器:
<!-- log4j--> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.27</version> </dependency> <!-- jul --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> <version>1.7.27</version> </dependency> <!--jcl --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.27</version> </dependency>
注意问题:
(1)jcl-over-slf4j.jar和 slf4j-jcl.jar不能同时部署。前一个jar文件将导致JCL将日志系统的选择委托给SLF4J,后一个jar文件将导致SLF4J将日志系统的选择委托给JCL,从而导致无限循环
(2)log4j-over-slf4j.jar和slf4j-log4j12.jar不能同时出现
(3)jul-to-slf4j.jar和slf4j-jdk14.jar不能同时出现
(4)所有的桥接都只对Logger日志记录器对象有效,如果程序中调用了内部的配置类或者是Appender,Filter等对象,将无法产生效果
🍀(5)SLF4J原理解析
(1)SLF4J通过LoggerFactory加载日志具体的实现对象
(2)LoggerFactory在初始化的过程中,会通过performInitialization()方法绑定具体的日志实现
(3)在绑定具体实现的时候,通过类加载器,加载org/slf4j/impl/StaticLoggerBinder.class
(4)所以,只要是一个日志实现框架,在org.slf4j.impl包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J所加载
在slf4j中创建logger的方法是:
public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name); }
继续进入查看,核心就是performInitialization();:
public static ILoggerFactory getILoggerFactory() { if (INITIALIZATION_STATE == UNINITIALIZED) { synchronized (LoggerFactory.class) { if (INITIALIZATION_STATE == UNINITIALIZED) { INITIALIZATION_STATE = ONGOING_INITIALIZATION; performInitialization(); } } } }
继续进入查看,核心就是bind(),这个方法应该就能绑定日志实现了:
private final static void performInitialization() { bind(); if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) { versionSanityCheck(); } } }
来到这里,看看绑定的方法:
private final static void bind() { try { ... // 以下内容就绑定成功了 StaticLoggerBinder.getSingleton(); INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION; reportActualBinding(staticLoggerBinderPathSet); fixSubstituteLoggers(); replayEvents(); // release all resources in SUBST_FACTORY SUBST_FACTORY.clear(); } catch (NoClassDefFoundError ncde) { String msg = ncde.getMessage(); if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) { INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION; Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\"."); Util.report("Defaulting to no-operation (NOP) logger implementation"); Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details."); } else { failedBinding(ncde); throw ncde; } } catch (java.lang.NoSuchMethodError nsme) { String msg = nsme.getMessage(); if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) { INITIALIZATION_STATE = FAILED_INITIALIZATION; Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding."); Util.report("Your binding is version 1.5.5 or earlier."); Util.report("Upgrade your binding to version 1.6.x."); } throw nsme; } catch (Exception e) { failedBinding(e); throw new IllegalStateException("Unexpected initialization failure", e); } }
每一个日志实现的中间包都有一个StaticLoggerBinder:
public class StaticLoggerBinder implements LoggerFactoryBinder { /** * The unique instance of this class. * */ private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); /** * Return the singleton of this class. * * @return the StaticLoggerBinder singleton */ public static final StaticLoggerBinder getSingleton() { return SINGLETON; } /** * Declare the version of the SLF4J API this implementation is compiled against. * The value of this field is modified with each major release. */ // to avoid constant folding by the compiler, this field must *not* be final public static String REQUESTED_API_VERSION = "1.6.99"; // !final private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName(); /** * The ILoggerFactory instance returned by the {@link #getLoggerFactory} * method should always be the same object */ private final ILoggerFactory loggerFactory; private StaticLoggerBinder() { loggerFactory = new Log4jLoggerFactory(); try { @SuppressWarnings("unused") Level level = Level.TRACE; } catch (NoSuchFieldError nsfe) { Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version"); } } public ILoggerFactory getLoggerFactory() { return loggerFactory; } public String getLoggerFactoryClassStr() { return loggerFactoryClassStr; } }
2️⃣JCL日志门面
全称为Jakarta Commons Logging,是Apache提供的一个通用日志API。 改日志门面的使用并不是很广泛。
它是为 "所有的Java日志实现"提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常常弱 (SimpleLog)。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk 自带的日志(JUL)
JCL 有两个基本的抽象类:Log(基本记录器)和LogFactory(负责创建Log实例)。
🍀JCL入门
(1)建立maven工程
(2)添加依赖
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
(3)入门代码
public class JULTest { @Test public void testQuick() throws Exception { // 创建日志对象 Log log = LogFactory.getLog(JULTest.class); // 日志记录输出 log.fatal("fatal"); log.error("error"); log.warn("warn"); log.info("info"); log.debug("debug"); } }
我们为什么要使用日志门面:
- 面向接口开发,不再依赖具体的实现类。减少代码的耦合
- 项目通过导入不同的日志实现类,可以灵活的切换日志框架
- 统一API,方便开发者学习和使用
- 统一配置便于项目日志的管理
🍀JCL原理
(1)通过LogFactory动态加载Log实现类
(2)日志门面支持的日志实现数组
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"};
(3)获取具体的日志实现
for(int i = 0; i < classesToDiscover.length && result == null; ++i) { result = this.createLogFromClass(classesToDiscover[i], logCategory, true); }
五、Logback日志框架
Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好。官方网站:https://logback.qos.ch/index.html
Logback主要分为三个模块:
logback-core:其它两个模块的基础模块
logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能
后续的日志代码都是通过SLF4J日志门面搭建日志系统,所以在代码是没有区别,主要是通过修改配置文件和pom.xml依赖
1️⃣Logback入门
🍀(1)添加依赖
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
🍀(2)java代码
public class TestLogback { private final static Logger logger = LoggerFactory.getLogger(TestLog4j.class); @Test public void testLogback(){ //打印日志信息 logger.error("error"); logger.warn("warn"); logger.info("info"); logger.debug("debug"); logger.trace("trace"); } }
其实我们发现即使项目中没有引入slf4j我们这里也是用的slf4j门面进行编程。
从logback’的pom依赖中我们看到slf4j,依赖会进行传递
2️⃣Logback源码解析
🍀(1)spi机制
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。他是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
主要是使用,java.util包下的ServiceLoader实现:
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); }
🍀(2)源码解析
源码看一下启动过程:
我们从日志工厂的常见看起,这里是slf4j的实现:
private final static Logger logger = LoggerFactory.getLogger(TestLog4j.class);
核心方法只有一句:
public static Logger getLogger(Class<?> clazz) { Logger logger = getLogger(clazz.getName()); ...中间的逻辑判断省略掉 return logger; }
看一下getLogger方法,这里是先获取日志工厂,在从工厂中提取日志对象,我们不考虑日志对象,主要看看日志工厂的环境怎么初始化的:
public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name); }
日志工厂的创建方法:
public static ILoggerFactory getILoggerFactory() { ...去掉其他的代码,从这一行看。 return StaticLoggerBinder.getSingleton().getLoggerFactory(); }
这里就进入了,StaticLoggerBinder这个对象,这是日志实现用来和slf4j进行绑定的类,从此就进入日志实现中了。
StaticLoggerBinder.getSingleton()这里看到出来是一个单例,来到这个类当中,我们看到,直接返回了defaultLoggerContext。
public ILoggerFactory getLoggerFactory() { if (!initialized) { return defaultLoggerContext; } ... 省略其他 }
这是个日志上下文,一定保存了我们的环境,配置内容一定在这个里边,那么哪里初始化他了呢,我们能想到的就是静态代码块了:
我们发现这个类中还真有:
static { SINGLETON.init(); }
我们看到init()方法中,有一个autoConfig(),感觉就像在自动配置:
void init() { try { try { new ContextInitializer(defaultLoggerContext).autoConfig(); } catch (JoranException je) { Util.report("Failed to auto configure default logger context", je); } ...其他省略 } }
默认配置:ContextInitializer类是初始化的关键。
自动配置是这么玩的,先找配置文件。
public void autoConfig() throws JoranException { StatusListenerConfigHelper.installIfAsked(loggerContext); // 这就是去找配置文件 URL url = findURLOfDefaultConfigurationFile(true); if (url != null) { // 解析配置 configureByResource(url); } else { // 没有找到文件,就去使用spi机制找一个配置类,这个配置类是在web中用的 Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class); if (c != null) { try { c.setContext(loggerContext); c.configure(loggerContext); } catch (Exception e) { throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass() .getCanonicalName() : "null"), e); } } else { // 如果没有找到,就做基本的配置 BasicConfigurator basicConfigurator = new BasicConfigurator(); basicConfigurator.setContext(loggerContext); basicConfigurator.configure(loggerContext); } } }
寻找配置文件的过程:
final public static String GROOVY_AUTOCONFIG_FILE = "logback.groovy"; final public static String AUTOCONFIG_FILE = "logback.xml"; final public static String TEST_AUTOCONFIG_FILE = "logback-test.xml"; public URL findURLOfDefaultConfigurationFile(boolean updateStatus) { ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this); URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus); if (url != null) { return url; } url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus); if (url != null) { return url; } url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus); if (url != null) { return url; } return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus); }
public void configureByResource(URL url) throws JoranException { if (url == null) { throw new IllegalArgumentException("URL argument cannot be null"); } final String urlString = url.toString(); if (urlString.endsWith("groovy")) { if (EnvUtil.isGroovyAvailable()) { // avoid directly referring to GafferConfigurator so as to avoid // loading groovy.lang.GroovyObject . See also http://jira.qos.ch/browse/LBCLASSIC-214 GafferUtil.runGafferConfiguratorOn(loggerContext, this, url); } else { StatusManager sm = loggerContext.getStatusManager(); sm.add(new ErrorStatus("Groovy classes are not available on the class path. ABORTING INITIALIZATION.", loggerContext)); } } else if (urlString.endsWith("xml")) { JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(loggerContext); configurator.doConfigure(url); } else { throw new LogbackException("Unexpected filename extension of file [" + url.toString() + "]. Should be either .groovy or .xml"); } }
基础配置的代码:
public class BasicConfigurator extends ContextAwareBase implements Configurator { public BasicConfigurator() { } public void configure(LoggerContext lc) { addInfo("Setting up default configuration."); ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender<ILoggingEvent>(); ca.setContext(lc); ca.setName("console"); LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<ILoggingEvent>(); encoder.setContext(lc); // same as // PatternLayout layout = new PatternLayout(); // layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"); TTLLLayout layout = new TTLLLayout(); layout.setContext(lc); layout.start(); encoder.setLayout(layout); ca.setEncoder(encoder); ca.start(); Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME); rootLogger.addAppender(ca); } }
我们先不说配置的事情,从源码中我们可以看出有几种配置,因为有了
我们先模仿BasicConfigurator写一个类,只做略微的改动:
public class MyConfigurator extends ContextAwareBase implements Configurator { public MyConfigurator() { } public void configure(LoggerContext lc) { addInfo("Setting up default configuration."); ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender<ILoggingEvent>(); ca.setContext(lc); ca.setName("console"); LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<ILoggingEvent>(); encoder.setContext(lc); // same as // PatternLayout layout = new PatternLayout(); // layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"); PatternLayout layout = new PatternLayout(); layout.setPattern("%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"); layout.setContext(lc); layout.start(); encoder.setLayout(layout); ca.setEncoder(encoder); ca.start(); Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME); rootLogger.addAppender(ca); } }
在resource中新建META-INF目录,下边在新建services文件夹,再新建一个名字叫ch.qos.logback.classic.spi.Configurator的文件,内容是:com.ydlclass.MyConfigurator。