
java
SpringBoot MySQL密码等敏感信息加密方案(2021.04.27)
陈叔叔 发表了文章 • 0 个评论 • 49 次浏览 • 2022-05-26 15:51
SpringBoot MySQL密码等敏感信息加密方案(2021.04.27)
一、背景说明
SpringBoot 项目经常将连接数据库的密码明文放在配置文件里,安全性就比较低,尤其一些企业对安全性要求很高,因此我们就考虑如何对密码等敏感信息进行加密。
二、解决方案
通过 Jasypt 对密码等敏感信息进行加密。Jasypt 是一个简单易用的加解密 Java 库,可以快速的集成到 Spring 和 SpringBoot 项目中,并提供了自动配置,使用非常简单。
三、使用方法
下面以 SpringBoot、MySQL、Mybatis 来演示 Jasypt 库的集成。
1. 引入 Maven 依赖
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.3</version></dependency>
2. 在配置文件添加 Jasypt 的相关配置;
jasypt: encryptor: # 加密的盐值,为了安全,该配置可以放在启动参数或者命令行参数中 password: fa7bd4edd42448aea8c9484fbce6e8cd # 加密所采用的算法 algorithm: PBEWITHHMACSHA512ANDAES_256
3. 生成加密后的密钥
方案一:采用单元测试生成加密后的密钥;
package com.xiaoqqya.controller;import org.jasypt.encryption.StringEncryptor;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;import java.util.UUID;/** * Description. * * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a> * @version 1.0 * @date 2021/04/20 * @since 1.8 */@SpringBootTest(classes = com.xiaoqqya.JasyptDemoApplication.class)public class Demo { @Resource StringEncryptor encryptor; @Test public void encrypt() { String url = encryptor.encrypt("jdbc:mysql://ubuntu:3306/jasypt?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC"); String username = encryptor.encrypt("root"); String pwd = encryptor.encrypt("password"); System.out.println("url = " + url); System.out.println("username = " + username); System.out.println("pwd = " + pwd); }}
方案二:采用命令行生成加密后的密钥;
# 在 Maven 仓库中找到 jasypt-1.9.3.jar 包$ cd C:UsersJoey.m2repositoryorgjasyptjasypt1.9.3# input 为明文密码,password 为加密盐值,algorithm为算法$ java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="root" password="fa7bd4edd42448aea8c9484fbce6e8cd"
4. 替换数据库密码
ENC() 为固定格式,() 里面为加密后的密钥。
spring: datasource: url: ENC(RSSEciOYx39dlPxCWeP46RZG1wkgtuPMgaZu2XgnrPluvHcAIzIEW79K1j9WIWLVHPbBpb6t1ehiiTiQGnzR1CFvrFm16bE0koyh/8exbh1ulYkfaSdBOivNHIF6CSxPy54vmxn3LaXug6ZYxfNBymQINbRa2fsXxlHT+TgvKqs=) username: ENC(YkJGWv80AEpPREn3Rt2Ic6BzzO+v+3m5Uy33pz4ZbZbD3vhi7vJz9nwGHKg8+n) password: ENC(4lEHAy//ExXjJxN9WQgyqgAjSkzIJ3irTYTYG8so6HgZWxPRl6Pa8tCUK/qmXSYb) driver-class-name: com.mysql.cj.jdbc.Driver
5. 启动测试
# 将盐值放在启动参数中$ java -jar target/jasypt-demo-0.0.1-SNAPSHOT.jar --jasypt.encryptor.password=fa7bd4edd42448aea8c9484fbce6e8cd# 将盐值放在命令行参数中$ java -Djasypt.encryptor.password=fa7bd4edd42448aea8c9484fbce6e8cd -jar target/jasypt-demo-0.0.1-SNAPSHOT.jar# 也可以将盐值放在系统环境变量中,此处不再展示
【Java面试】IO和NIO有什么区别?
陈叔叔 发表了文章 • 0 个评论 • 55 次浏览 • 2022-05-26 14:29
IO问题一直是面试的重灾区之一
但又是非常重要而且面试必问的知识点
一个工作了7年的粉丝私信我,他去面试了 4家互联网公司,
有三个公司问他网络IO的问题,另外一个公司问了Netty,结果都没回答上来。
好吧,对于“IO和NIO的区别”,看看普通人和高手的回答。
普通人:
嗯。。。。。。。。。。
高手:
好的,关于这个问题,我会从下面几个方面来回答。
首先,I/O ,指的是IO流, 它可以实现数据从磁盘中的读取以及写入。
实际上,除了磁盘以外,内存、网络都可以作为I/O流的数据来源和目的地。
在Java里面,提供了字符流和字节流两种方式来实现数据流的操作。
其次,当程序是面向网络进行数据的IO操作的时候,Java里面提供了Socket的方式来实现。
通过这种方式可以实现数据的网络传输。
基于Socket的IO通信,它是属于阻塞式IO,也就是说,在连接以及IO事件未就绪的情况下,当前的连接会处于阻塞等待的状态。
如果一旦某个连接处于阻塞状态,那么后续的连接都得等待。所以服务端能够处理的连接数量非常有限。
NIO,是JDK1.4里面新增的一种NEW IO机制,相比于传统的IO,NIO在效率上做了很大的优化,并且新增了几个核心组件。
Channel、Buffer、Selectors。
另外,还提供了非阻塞的特性,所以,对于网络IO来说,NIO通常也称为No-Block IO,非阻塞IO。
也就是说,通过NIO进行网络数据传输的时候,如果连接未就绪或者IO事件未就绪的情况下,服务端不会阻塞当前连接,而是继续去轮询后续的连接来处理。
所以在NIO里面,服务端能够并行处理的链接数量更多。
因此,总的来说,IO和NIO的区别,站在网络IO的视角来说,前者是阻塞IO,后者是非阻塞IO。
以上就是我对这个问题的理解。
总结
在互联网时代,网络IO是最基础的技术。
无论是微服务架构中的服务通信、还是应用系统和中间件之间的网络通信,都在体现网络IO的重要性。
如果想获得一对一的面试指导以及面试资料,可以私信我。
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自
Mic带你学架构
!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!
instanceof和getClass的区别说明
陈叔叔 发表了文章 • 0 个评论 • 62 次浏览 • 2022-05-26 12:08
转自:
http://www.java265.com/JavaJingYan/202205/16514583603236.html
instanceof是java的一个二元操作符,它是由字母组成的,所以也是java的保留关键字.
instanceof关键字的功能:
判断其左边对象是否为其右边类的实例,返回boolean类型的数据
可以来判断继承中的子类的实例是否为父类的实现
java中的instanceof是通过返回一个布尔值来指出
这个对象是否是这个特定类或者是它的子类的一个实例
下文笔者讲述java中instanceof和getClass的区别,如下所示:
instanceof和getClass都可用于比较两个对象是否属于同一个类的实例 但是 instanceof检测一个类的父类和子类(检测类和派生类),都会返回true 但是getClass是返回具体的字符串,只做严格的类判断
public class Test{ /* *java265.com 示例分享 */ public static void testInstanceof(Object x) { System.out.println("x instanceof Parent: "+(x instanceof Parent)); System.out.println("x instanceof Child: "+(x instanceof Child)); System.out.println("x getClass Parent: "+(x.getClass() == Parent.class)); System.out.println("x getClass Child: "+(x.getClass() == Child.class)); } public static void main(String[] args) { testInstanceof(new Parent()); System.out.println("========================"); testInstanceof(new Child()); }}class Parent { }class Child extends Parent { }/*输出:x instanceof Parent: truex instanceof Child: falsex getClass Parent: truex getClass Child: false========================x instanceof Parent: truex instanceof Child: truex getClass Parent: falsex getClass Child: true*/
日志框架体系整理( 基础 )
陈叔叔 发表了文章 • 0 个评论 • 80 次浏览 • 2022-05-24 17:13
0、前言
- 本篇博客的初衷是为了特定的人弄的,这些内容在网上都可以找到原文,因此:这篇博客只是保证能够让特定的人看懂,其他人看得懂就看,看不懂拉倒,同时,这篇博客中细节说明没有、运行截图没有、特别备注没有......
1、JUL
- 指的是Java Util Logging包,它是java原生的日志框架,使用时不需要另外引用第三方的类库,相对其他的框架使用方便,学习简单,主要是使用在小型应用中
1.1、JUL的组成结构
-
Logger:被称为记录器,应用程序通过获取Logger对象,抵用其API来发布日志信息。Logger通常被认为是访问日志系统的入口程序。
-
Handler:处理器,每个Logger都会关联一个或者是一组Handler,Logger会将日志交给关联的Handler去做处理,由Handler负责将日志做记录。Handler具体实现了日志的输出位置,比如可以输出到控制台或者是文件中等等。
-
Filter:过滤器,根据需要定制哪些信息会被记录,哪些信息会被略过。
-
Formatter:格式化组件,它负责对日志中的数据和信息进行转换和格式化,所以它决定了我们输出日志最终的形式。
-
Level:日志的输出级别,每条日志消息都有一个关联的级别。我们根据输出级别的设置,用来展现最终所呈现的日志信息。根据不同的需求,去设置不同的级别。
1.2、入门
- 可以直接去JDK文档中查看java.util.logging这个包
public class JULTest { @Test public void test01(){ /* 日志入口程序 java.util.logging.Logger */ // Logger对象的创建方式,不能直接new对象 // 取得对象的方法参数,需要引入当前类的全路径字符串(当前我们先这么用,以后根据包结构有Logger父子关系,以后详细介绍) Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 对于日志的输出,有两种方式 第一种方式: 直接调用日志级别相关的方法,方法中传递日志输出信息 假设现在我们要输出info级别的日志信息 */ //logger.info("输出info信息1"); /* 第二种方式: 调用通用的log方法,然后在里面通过Level类型来定义日志的级别参数,以及搭配日志输出信息的参数 */ //logger.log(Level.INFO,"输出info信息2"); /* 输出学生信息 姓名 年龄 */ /*String name = "zs"; int age = 23; logger.log(Level.INFO,"学生的姓名为:"+name+";年龄为:"+age);*/ /* 对于输出消息中,字符串的拼接弊端很多 1.麻烦 2.程序效率低 3.可读性不强 4.维护成本高 我们应该使用动态生成数据的方式,生产日志 我们使用的就是占位符的方式来进行操作 */ String name = "zs"; int age = 23; logger.log(Level.INFO,"学生的姓名:{0},年龄:{1}",new Object[]{name,age}); }}
1.3、日志级别
@Test public void test02(){ /* 日志的级别(通过源码查看,非常简单) SEVERE : 错误 --- 最高级的日志级别 WARNING : 警告 INFO : (默认级别)消息 源码:Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->350行左右有一个 defaultlevel属性 CONFIG : 配置 FINE : 详细信息(少) FINER : 详细信息(中) FINEST : 详细信息 (多) --- 最低级的日志级别 两个特殊的级别 OFF 可用来关闭日志记录 ALL 启用所有消息的日志记录 对于日志的级别,重点关注的是new对象的时候的第二个参数,是一个数值(源码中有) OFF Integer.MAX_VALUE 整型最大值 SEVERE 1000 WARNING 900 ... ... FINEST 300 ALL Integer.MIN_VALUE 整型最小值 这个数值的意义在于,如果设置的日志的级别是INFO -- 800 那么最终展现的日志信息,必须是数值大于800的所有的日志信息 最终展现的就是 SEVERE WARNING INFO */ Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 通过打印结果,我们看到了仅仅只是输出了info级别以及比info级别高的日志信息 比info级别低的日志信息没有输出出来 证明了info级别的日志信息,它是系统默认的日志级别 在默认日志级别info的基础上,打印比它级别高的信息 */ /* 如果仅仅只是通过以下形式来设置日志级别 那么不能够起到效果 将来需要搭配处理器handler共同设置才会生效 */ logger.setLevel(Level.CONFIG); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
1.4、自定义日志级别
1.4.1、输出信息在console上
@Test public void test03(){ /* 自定义日志的级别 */ // 日志记录器 Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 将默认的日志打印方式关闭掉 参数设置为false,打印日志的方式就不会按照父logger默认的方式去进行操作 */ logger.setUseParentHandlers(false); /* 处理器Handler 这里使用的是控制台日志处理器,取得处理器对象 */ ConsoleHandler handler = new ConsoleHandler(); // 创建日志格式化组件对象 SimpleFormatter formatter = new SimpleFormatter(); // 在处理器中设置输出格式 handler.setFormatter(formatter); // 在记录器中添加处理器 logger.addHandler(handler); /*设置日志的打印级别 此处必须将日志记录器和处理器的级别进行统一的设置,才会达到日志显示相应级别的效果 logger.setLevel(Level.CONFIG); handler.setLevel(Level.CONFIG); */ // 设置如下的all级别之后,那么下面所有的输出信息都会打印到控制台,在需要的时候改成对应的日志级别即可 logger.setLevel(Level.ALL); handler.setLevel(Level.ALL); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
1.4.2、输出信息在磁盘上
@Test public void test04() throws IOException { /* 将日志输出到具体的磁盘文件中 这样做相当于是做了日志的持久化操作 */ Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); logger.setUseParentHandlers(false); // 文件日志处理器 FileHandler handler = new FileHandler("D:\test\" + this.getClass().getSimpleName() + ".log"); SimpleFormatter formatter = new SimpleFormatter(); handler.setFormatter(formatter); logger.addHandler(handler); // 也可以同时在控制台和文件中进行打印 ConsoleHandler handler2 = new ConsoleHandler(); handler2.setFormatter(formatter); // 可以在记录器中同时添加多个处理器,这样磁盘和控制台中都可以输出对应级别的日志信息了 logger.addHandler(handler2); logger.setLevel(Level.ALL); handler.setLevel(Level.ALL); handler2.setLevel(Level.CONFIG); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); /* 总结: 用户使用Logger来进行日志的记录,Logger可以持有多个处理器Handler (日志的记录使用的是Logger,日志的输出使用的是Handler) 添加了哪些handler对象,就相当于需要根据所添加的handler 将日志输出到指定的位置上,例如控制台、文件.. */ }
1.5、logger的父子关系
@Test public void test05(){ /* Logger之间的父子关系 JUL中Logger之间是存在"父子"关系的 值得注意的是,这种父子关系不是我们普遍认为的类之间的继承关系 关系是通过树状结构存储的 */ /* 从下面创建的两个logger对象看来 我们可以认为logger1是logger2的父亲 */ /*父亲是RootLogger,名称默认是一个空的字符串 RootLogger可以被称之为所有logger对象的顶层logger */ // 这就是父 Logger logger1 = Logger.getLogger("cn.zixieqing.jul.test"); // 这是子,甚至cn.zixieqing.jul也是这个logger2的父 Logger logger2 = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); //System.out.println(logger2.getParent()==logger1); //true System.out.println("logger1的父Logger引用为:" +logger1.getParent()+"; 名称为"+logger1.getName()+"; 父亲的名称为"+logger1.getParent().getName()); System.out.println("logger2的父Logger引用为:" +logger2.getParent()+"; 名称为"+logger2.getName()+"; 父亲的名称为"+logger2.getParent().getName()); /* 父亲所做的设置,也能够同时作用于儿子 对logger1做日志打印相关的设置,然后我们使用logger2进行日志的打印 */ // 父亲做设置 logger1.setUseParentHandlers(false); ConsoleHandler handler = new ConsoleHandler(); SimpleFormatter formatter = new SimpleFormatter(); handler.setFormatter(formatter); logger1.addHandler(handler); handler.setLevel(Level.ALL); logger1.setLevel(Level.ALL); // 儿子做打印 - 结果就是:儿子logger2没做配置,但是父亲logger1做了,所以儿子logger2的输出级别就是父亲的级别 logger2.severe("severe信息"); logger2.warning("warning信息"); logger2.info("info信息"); logger2.config("config信息"); logger2.fine("fine信息"); logger2.finer("finer信息"); logger2.finest("finest信息"); }
小结:看源码的结果
// JUL在初始化时会创建一个顶层RootLogger作为所有Logger的父Logger,java.util.logging.LogManager$RootLogger,默认的名称为空串 // 查看源码Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->350行左右: owner.rootLogger = owner.new RootLogger(); RootLogger是LogManager的内部类 /* 以上的RootLogger对象作为树状结构的根节点存在的 将来自定义的父子关系通过路径来进行关联 父子关系,同时也是节点之间的挂载关系 */ // 350行左右 owner.addLogger(owner.rootLogger); addLogger ----> LoggerContext cx = getUserContext(); /* LoggerContext一种用来保存节点的Map关系,WeakHashMap<Object, LoggerContext> contextsMap 点进WeakHashMap<Object, LoggerContext>的LoggerContext,里面的结构为:Hashtable<String,LoggerWeakRef> namedLoggers */ // 再点开Hashtable<String,LoggerWeakRef> namedLoggers 的LoggerWeakRef,得到的信息如下: private String name; // for namedLoggers cleanup,这玩意就是节点,也就是说父子关系也就是节点挂载关系 private LogNode node; // for loggerRef cleanup private WeakReference<Logger> parentRef; // for kids cleanup
1.6、使用配置文件
1.6.1、默认配置文件所在地
- 查看源码
Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->345行左右: 有这么一行代码:owner.readPrimordialConfiguration(); 点击readPrimordialConfiguration(),在340行左右有一句readConfiguration(); 点击readConfiguration();在1290行左右,有如下的代码 String fname = System.getProperty("java.util.logging.config.file"); if (fname == null) { fname = System.getProperty("java.home"); if (fname == null) { throw new Error("Can't find java.home ??"); } File f = new File(fname, "lib"); f = new File(f, "logging.properties"); fname = f.getCanonicalPath(); }
结论:默认配置文件所在地
- java.home( 即:安装JDK的目录 ) --> 找到jre文件夹 --> lib --> logging.properties
分析一下上述目录中的logging.properties文件,找打这个目录下的此文件打开
# RootManager默认使用的处理器# 若是想要配置多个处理器,可以使用逗号进行分开,如:java.util.logging.ConsoleHandler,java.util.logging.FileHandlerhandlers= java.util.logging.ConsoleHandler# RootManager的默认日志级别,这是全局的日志级别# 若不手动进行级别配置,那么默认使用INFO及更高的级别进行输出.level= INFO# 文件处理器设置# 日志文件的路径# %h/java%u.log h指的是用户目录 - 不分window还是linux# java%u.log是生成的文件名,其中:u相当于是自增,从0开始的# 如:java0.log、java1.log、java2.log......这里生成多少份由java.util.logging.FileHandler.count = 1这个配置决定java.util.logging.FileHandler.pattern = %h/java%u.log# 日志文件的限制 - 默认50000字节java.util.logging.FileHandler.limit = 50000# 日志文件的数量 - 默认1份java.util.logging.FileHandler.count = 1# 日志文件的格式,默认XML格式,也可以采用SimpleFormatterjava.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter# 控制台处理器设置# 控制台默认级别java.util.logging.ConsoleHandler.level = INFO# 控制台默认输出格式java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter# 这是这个源文档中举的一个例子,意思是像下面这样,可将日志级别设定到具体的某个包下# com.xyz.foo.level = SEVERE
1.6.2、修改成更友好点的配置文件
# 自定义Loggercn.zixieqing.handlers=java.util.logging.FileHandler# 自定义Logger日志级别cn.zixieqing.level=SERVER# 屏蔽父logger的日志设置,相当于前面玩的logger.setUseParentHandlers(false);cn.zixieqing.useParentHandlers=false# RootManager默认使用的处理器# 若是想要配置多个处理器,可以使用逗号进行分开,如:java.util.logging.ConsoleHandler,java.util.logging.FileHandlerhandlers= java.util.logging.ConsoleHandler# RootManager的默认日志级别,这是全局的日志级别.level= SERVER# 文件处理器设置# 日志文件的路径# %h/java%u.log h指的是用户目录 - 不分window还是linux# java%u.log是生成的文件名,其中:u相当于是自增,从0开始的# 如:java0.log、java1.log、java2.log......这里生成多少份由java.util.logging.FileHandler.count = 1这个配置决定java.util.logging.FileHandler.pattern = %h/java%u.log# 日志文件的限制 - 50000字节java.util.logging.FileHandler.limit = 50000# 日志文件的数量java.util.logging.FileHandler.count = 1# 日志文件的格式java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter# 控制台处理器设置# 控制台默认级别java.util.logging.ConsoleHandler.level = SERVER# 控制台默认输出格式java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter# 下一次生成的日志文件默认是覆盖了上一此的文件 - 改为在原日志上进行追加java.util.logging.FileHandler.append=true
1.6.3、使用自定义的配置文件
@Test public void test06() throws Exception { // 加载自定义的配置文件 InputStream input = new FileInputStream("D:\test\logging.properties"); // 取得日志管理器对象 LogManager logManager = LogManager.getLogManager(); // 读取自定义的配置文件 logManager.readConfiguration(input); Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
1.7、总结:JUL原理
-
1、初始化LogManager
- LogManager加载logging.properties配置文件
- 添加Logger到LogManager
-
2、从单例的LogManager获取Logger,即:LogManager.getLogManager();
-
3、设置日志级别Level,在打印的过程中使用到了日志记录的LogRecord类,源码找寻如下:
-
1、点击logger.severe("severe信息");中的severe,当然点击其他warning、info、config也是可以进去的,之后会看到如下的代码 public void severe(String msg) { log(Level.SEVERE, msg); }2、点击上述的log(),看到如下的代码 public void log(Level level, String msg) { if (!isLoggable(level)) { return; } // 这里就是目的地 LogRecord lr = new LogRecord(level, msg); doLog(lr); }
-
-
4、Filter作为过滤器提供了日志级别之外更细粒度的控制
-
5、Handler日志处理器,决定日志的输出位置,例如控制台、文件...
-
6、Formatter是用来格式化输出的
2、LOG4J
- 全称:log for java
2.1、LOG4J的组成
- Loggers (日志记录器):控制日志的输出以及输出级别(JUL做日志级别Level)
- Appenders(输出控制器):指定日志的输出方式(输出到控制台、文件等)
- Layout(日志格式化器):控制日志信息的输出格式
2.1.1、Loggers日志记录器
- 负责收集处理日志记录,实例的命名就是类的全限定名,如:
cn.zixieqing.test
,同时:Logger的名字大小写敏感,其命名有继承机制( 和JUL中的父子关系一样,以包路径来区分的 );另外:root logger是所有logger的根 - 上辈所做的日志属性设置,会直接的影响到子辈
// root logger的获取 Logger.getRootLogger();
日志级别
- DEBUG
- INFO
- WARN
- ERROR
- ........
- 大小关系:ERROR > WARN > INFO > DEBUG
- 输出规则:输出日志的规则是:只输出级别不低于设定级别的日志信息
- 如:假设Loggers级别设定为INFO,则INFO、WARN、ERROR级别的日志信息都会输出,而级别比INFO低的DEBUG则不会输出
2.1.2、Appenders输出控制器
- 允许把日志输出到不同的地方,如控制台(Console)、文件(Files)等,可以根据时间或者文件大小产生新的文件,可以以流的形式发送到其它地方等等
- 常用Appenders输出控制器类型:
- ConsoleAppender 将日志输出到控制台
- FileAppender 将日志输出到文件中
- DailyRollingFileAppender 根据指定时间输出到一个新的文件, 将日志输出到一个日志文件
- RollingFileAppender 根据指定文件大小,当文件大小达到指定大小时,会自动把文件改名,产生一个新的文件,将日志信息输出到一个日志文件
- JDBCAppender 把日志信息保存到数据库中
2.2、玩LOG4J
2.2.1、入门
- 依赖
<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
public class Log4jTest01 { @Test public void test01(){ // 加载初始化配置 BasicConfigurator.configure(); // 注意:这个logger是apache下的,前面玩的JUL中的是java。util.logging包下的 Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }}
- 注意加载初始化信息:
BasicConfigurator.configure();
,不加这一句代码就报错 - 没有添加Appenders输出控制器,加上不报错是因为:源码中有这样一句代码rootManager.addAppenders( XxxxAppender( PatternLayout layout ) )
,configure源码如下:
public static void configure() { Logger root = Logger.getRootLogger(); root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n"))); }
LOG4J日志级别
- Log4j提供了8个级别的日志输出,分别为如下级别:
- ALL 最低等级 用于打开所有级别的日志记录
- TRACE 程序推进下的追踪信息,这个追踪信息的日志级别非常低,一般情况下是不会使用的
- DEBUG 指出细粒度信息事件对调试应用程序是非常有帮助的,主要是配合开发,在开发过程中打印一些重要的运行信息,在没有进行设置的情况下,默认的日志输出级别
- INFO 消息的粗粒度级别运行信息
- WARN 表示警告,程序在运行过程中会出现的有可能会发生的隐形的错误
- 注意,有些信息不是错误,但是这个级别的输出目的就是为了给程序员以提示
- ERROR 系统的错误信息,发生的错误不影响系统的运行
- 一般情况下,如果不想输出太多的日志,则使用该级别即可
- FATAL 表示严重错误,它是那种一旦发生系统就不可能继续运行的严重错误
- 如果这种级别的错误出现了,表示程序可以停止运行了
- OFF 最高等级的级别,用户关闭所有的日志记录
2.2.2、分析源码
BasicConfigurator.configure(); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息");
分析 BasicConfigurator.configure();点击configure()
- 得到的代码如下
public static void configure() { Logger root = Logger.getRootLogger(); root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n"))); }
-
从中可以得到几个信息:
- 1、创建根节点的对象
Logger root = Logger.getRootLogger();
- 2、根节点添加了
ConsoleAppender对象(表示默认打印到控制台,自定义的格式化输出PatternLayout)
- 1、创建根节点的对象
-
那么想要自定义配置文件来实现上述源代码的功能呢?通过上面这个源代码分析,我们需要具备如下的条件:
- 们的配置文件需要提供Logger、Appender、Layout这3个组件信息
分析Logger logger = Logger.getLogger(Log4jTest01.class);点击getLogger()
public static Logger getLogger(Class clazz) { return LogManager.getLogger(clazz.getName()); }
- 发现:
LogManager.getLogger(clazz.getName());
,其中:LogManager
j就是日志管理器
查看LogManager
- 首先看到如下信息,一堆常量
public static final String DEFAULT_CONFIGURATION_FILE = "log4j.properties"; static final String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml"; /** @deprecated */ public static final String DEFAULT_CONFIGURATION_KEY = "log4j.configuration"; /** @deprecated */ public static final String CONFIGURATOR_CLASS_KEY = "log4j.configuratorClass"; /** @deprecated */ public static final String DEFAULT_INIT_OVERRIDE_KEY = "log4j.defaultInitOverride"; private static Object guard = null; private static RepositorySelector repositorySelector;
- 这些东西代表的就是不同形式(不同后缀名)的配置文件,其中
log4j.properties属性
使我们最常使用的,因为它语法简单、使用方便
log4j.properties的加载时机
加载 - 那就是static
观察LogManager中的代码,找到其中的静态代码块static
- 发现如下的一堆源码
static { Hierarchy h = new Hierarchy(new RootLogger(Level.DEBUG)); repositorySelector = new DefaultRepositorySelector(h); String override = OptionConverter.getSystemProperty("log4j.defaultInitOverride", (String)null); if (override != null && !"false".equalsIgnoreCase(override)) { LogLog.debug("Default initialization of overridden by log4j.defaultInitOverrideproperty."); } else { String configurationOptionStr = OptionConverter.getSystemProperty("log4j.configuration", (String)null); String configuratorClassName = OptionConverter.getSystemProperty("log4j.configuratorClass", (String)null); URL url = null; if (configurationOptionStr == null) { url = Loader.getResource("log4j.xml"); if (url == null) { // 前面就是文件格式的一堆判断,这里才是log4j.propertie格式做的事情 url = Loader.getResource("log4j.properties"); } } else { try { url = new URL(configurationOptionStr); } catch (MalformedURLException var7) { url = Loader.getResource(configurationOptionStr); } } if (url != null) { LogLog.debug("Using URL [" + url + "] for automatic log4j configuration."); try { // 这里又是一个信息:selectAndConfigure()翻译就是选择配置文件 OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository()); } catch (NoClassDefFoundError var6) { LogLog.warn("Error during default initialization", var6); } } else { LogLog.debug("Could not find resource: [" + configurationOptionStr + "]."); } } }
-
从源码中,发现
url = Loader.getResource("log4j.properties");
- 从这句代码得到的信息:系统默认是从当前的类路径下找到
log4j.properties
,而若是maven工程,那么:就应该在resources路径下去找
- 从这句代码得到的信息:系统默认是从当前的类路径下找到
-
同时在上面的源码中发现
OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository());
查看selectAndConfigure()
- 发现如下源码
public static void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) { Configurator configurator = null; String filename = url.getFile(); if (clazz == null && filename != null && filename.endsWith(".xml")) { clazz = "org.apache.log4j.xml.DOMConfigurator"; } if (clazz != null) { LogLog.debug("Preferred configurator class: " + clazz); configurator = (Configurator)instantiateByClassName(clazz, Configurator.class, (Object)null); if (configurator == null) { LogLog.error("Could not instantiate configurator [" + clazz + "]."); return; } } else { // 有用信息在这里,即 new PropertyConfigurator();创建了一个properties配置对象 configurator = new PropertyConfigurator(); } ((Configurator)configurator).doConfigure(url, hierarchy); }
查看PropertyConfigurator类
- 首先看到的就是如下的常量信息
static final String CATEGORY_PREFIX = "log4j.category."; static final String LOGGER_PREFIX = "log4j.logger."; static final String FACTORY_PREFIX = "log4j.factory"; static final String ADDITIVITY_PREFIX = "log4j.additivity."; static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory"; // 这是一个重要信息 static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"; // 这也是一个重要信息 static final String APPENDER_PREFIX = "log4j.appender."; static final String RENDERER_PREFIX = "log4j.renderer."; static final String THRESHOLD_PREFIX = "log4j.threshold"; private static final String THROWABLE_RENDERER_PREFIX = "log4j.throwableRenderer"; private static final String LOGGER_REF = "logger-ref"; private static final String ROOT_REF = "root-ref"; private static final String APPENDER_REF_TAG = "appender-ref"; public static final String LOGGER_FACTORY_KEY = "log4j.loggerFactory"; private static final String RESET_KEY = "log4j.reset"; private static final String INTERNAL_ROOT_NAME = "root";
- 通过前面的基础,从这源码中,发现有两个信息是要进行配置的
static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";static final String APPENDER_PREFIX = "log4j.appender.";
- 那么这二者是怎么进行配置的?
找寻static final String APPENDER_PREFIX = "log4j.appender."中的appender配置方式
直接在当前源码页面搜索appender
- 发现如下的源代码
Appender parseAppender(Properties props, String appenderName) { Appender appender = this.registryGet(appenderName); if (appender != null) { LogLog.debug("Appender "" + appenderName + "" was already parsed."); return appender; } else { // 重要信息就在这里,这里告知了一件事:上面找到的log4j.appender.配置方式为如下的方式 String prefix = "log4j.appender." + appenderName; // 这也是重要信息,layout日志格式化的配置方式 String layoutPrefix = prefix + ".layout"; appender = (Appender)OptionConverter.instantiateByKey(props, prefix, Appender.class, (Object)null); if (appender == null) { LogLog.error("Could not instantiate appender named "" + appenderName + ""."); return null; } else { appender.setName(appenderName); if (appender instanceof OptionHandler) { if (appender.requiresLayout()) { Layout layout = (Layout)OptionConverter.instantiateByKey(props, layoutPrefix, Layout.class, (Object)null); if (layout != null) { appender.setLayout(layout); LogLog.debug("Parsing layout options for "" + appenderName + ""."); PropertySetter.setProperties(layout, props, layoutPrefix + "."); LogLog.debug("End of parsing for "" + appenderName + ""."); } } String errorHandlerPrefix = prefix + ".errorhandler"; String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props); if (errorHandlerClass != null) { ErrorHandler eh = (ErrorHandler)OptionConverter.instantiateByKey(props, errorHandlerPrefix, ErrorHandler.class, (Object)null); if (eh != null) { appender.setErrorHandler(eh); LogLog.debug("Parsing errorhandler options for "" + appenderName + ""."); this.parseErrorHandler(eh, errorHandlerPrefix, props, this.repository); Properties edited = new Properties(); String[] keys = new String[]{errorHandlerPrefix + "." + "root-ref", errorHandlerPrefix + "." + "logger-ref", errorHandlerPrefix + "." + "appender-ref"}; Iterator iter = props.entrySet().iterator(); while(true) { if (!iter.hasNext()) { PropertySetter.setProperties(eh, edited, errorHandlerPrefix + "."); LogLog.debug("End of errorhandler parsing for "" + appenderName + ""."); break; } Entry entry = (Entry)iter.next(); int i; for(i = 0; i < keys.length && !keys[i].equals(entry.getKey()); ++i) { } if (i == keys.length) { edited.put(entry.getKey(), entry.getValue()); } } } } PropertySetter.setProperties(appender, props, prefix + "."); LogLog.debug("Parsed "" + appenderName + "" options."); } this.parseAppenderFilters(props, appenderName, appender); this.registryPut(appender); return appender; } } }
-
通过上述的源码,发现配置
log4j.appender.
的方式:log4j.appender.+appenderName
- 其中:appenderName就是输出控制器名字
-
继而:推导出
log4j.properties
配置文件中的一个配置项appender输出方式为:log4j.appender.+appenderName=某一种输出控制器名字
- 其中:输出控制器名字在前面一开始就接触过了
- 因此
Log4j.properties
的appender输出方式配置方式举例就是log4j.appender.console=org.apache.log4j.ConsoleAppender
- 同样道理,通过第二句代码
String layoutPrefix = prefix + ".layout";
,也就知道了layout输出格式的配置方式
- layout日志输出格式配置举例:
log4j.appender.console.layout=org.log4j.SimpleLayout
小小总结一波:log4j.properties配置文件中的appender输出控制器 和 layout日志输出格式的配置方式
log4j.appender,console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.log4j.SimpleLayout
继续找第二个配置static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"中的rootLogger配置方式
通过log4j.rootLogge进行搜索
- 发现如下的方法
void configureRootCategory(Properties props, LoggerRepository hierarchy) { String effectiveFrefix = "log4j.rootLogger"; String value = OptionConverter.findAndSubst("log4j.rootLogger", props); if (value == null) { value = OptionConverter.findAndSubst("log4j.rootCategory", props); effectiveFrefix = "log4j.rootCategory"; } if (value == null) { LogLog.debug("Could not find root logger information. Is this OK?"); } else { Logger root = hierarchy.getRootLogger(); synchronized(root) { // 这里面执行了这个方式 this.parseCategory(props, root, effectiveFrefix, "root", value); } } }
查看parseCategory()
- 在这里找到了想要的配置方式
void parseCategory(Properties props, Logger logger, String optionKey, String loggerName, String value) { LogLog.debug("Parsing for [" + loggerName + "] with value=[" + value + "]."); // 配置方式就在这里,这个操作的意思就是:表示要以逗号的方式来切割字符串,证明了log4j.rootLogger的取值,可以有多个值,使用逗号进行分隔 StringTokenizer st = new StringTokenizer(value, ","); if (!value.startsWith(",") && !value.equals("")) { if (!st.hasMoreTokens()) { return; } // 把字符串通过逗号切割之后,第一个值的用途就在这里 - levelStr、level,即:切割后的第一个值是日志的级别 String levelStr = st.nextToken(); LogLog.debug("Level token is [" + levelStr + "]."); if (!"inherited".equalsIgnoreCase(levelStr) && !"null".equalsIgnoreCase(levelStr)) { logger.setLevel(OptionConverter.toLevel(levelStr, Level.DEBUG)); } else if (loggerName.equals("root")) { LogLog.warn("The root logger cannot be set to null."); } else { logger.setLevel((Level)null); } LogLog.debug("Category " + loggerName + " set to " + logger.getLevel()); } logger.removeAllAppenders(); // 字符串切割之后的第一个值是level日志级别,而剩下的值的用途就在这里 while(st.hasMoreTokens()) { // 通过这句代码得知:第2 - 第n个值,就是我们配置的其他信息,这个信息就是appenderName String appenderName = st.nextToken().trim(); if (appenderName != null && !appenderName.equals(",")) { LogLog.debug("Parsing appender named "" + appenderName + ""."); Appender appender = this.parseAppender(props, appenderName); if (appender != null) { logger.addAppender(appender); } } } }
- 通过上述的代码分析,得知
log4j.rootLogger
的配置方式为:log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
- 表示可以同时在根节点上配置多个日志输出的途径
2,2,3、最基本的log4j.properties配置
- 通过前面的源码分析之后,得出
BasicConfigurator.configure();
替代品的properties配置如下:
# rootLogger所有logger的根配置 - log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....# 这里的例子没用日志输出路径,这个日志输出路径后续再加log4j.rootLogger=debug,console# appender输出控制器配置 log4j.appender.+appenderName=某一种输出类型 - 采用Console控制台的方式举例log4j.appender.console=org.apache.log4j.ConsoleAppender# 输出的格式配置log4j.appender.+appenderName+layout=某种layout格式类型log4j.appender.console.layout=org.apache.log4j.SimpleLayout
测试
// 注掉这一句就可以了,这句代码的配置由上面的自定义配置进行代替了 //BasicConfigurator.configure(); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息");
2.2.3、打开日志输出的详细信息
@Test public void test03(){ /* 通过Logger中的开关 打开日志输出的详细信息 查看LogManager类中的方法getLoggerRepository() 找到代码LogLog.debug(msg, ex); LogLog会使用debug级别的输出为我们展现日志输出详细信息 Logger是记录系统的日志,那么LogLog就是用来记录Logger的日志 进入到LogLog.debug(msg, ex);方法中 通过代码:if (debugEnabled && !quietMode) 观察到if判断中的这两个开关都必须开启才行 !quietMode是已经启动的状态,不需要我们去管 debugEnabled默认是关闭的 所以我们只需要设置debugEnabled为true就可以了 */ // 开启 debugEnabled LogLog.setInternalDebugging(true); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
- 若开启debugEnabled,那么输出信息就是如下的样子( 如下是举的一个例子而已)
0 [main] FATAL cn.zixieqing.HotelJavaApplicationTests - fatal信息1 [main] ERROR cn.zixieqing.HotelJavaApplicationTests - error信息1 [main] WARN cn.zixieqing.HotelJavaApplicationTests - warn信息1 [main] INFO cn.zixieqing.HotelJavaApplicationTests - info信息1 [main] DEBUG cn.zixieqing.HotelJavaApplicationTests - debug信息
- 开启之后,信息就会更全
log4j: Trying to find [log4j.xml] using context classloader sun.misc.Launcher$AppClassLoader@18b4aac2.log4j: Trying to find [log4j.xml] using sun.misc.Launcher$AppClassLoader@18b4aac2 class loader.log4j: Trying to find [log4j.xml] using ClassLoader.getSystemResource().log4j: Trying to find [log4j.properties] using context classloader sun.misc.Launcher$AppClassLoader@18b4aac2.log4j: Trying to find [log4j.properties] using sun.misc.Launcher$AppClassLoader@18b4aac2 class loader.log4j: Trying to find [log4j.properties] using ClassLoader.getSystemResource().log4j: Could not find resource: [null].0 [main] FATAL cn.zixieqing.HotelJavaApplicationTests - fatal信息0 [main] ERROR cn.zixieqing.HotelJavaApplicationTests - error信息1 [main] WARN cn.zixieqing.HotelJavaApplicationTests - warn信息1 [main] INFO cn.zixieqing.HotelJavaApplicationTests - info信息1 [main] DEBUG cn.zixieqing.HotelJavaApplicationTests - debug信息
2.2.4、自定义输出格式 patternLayout
- 自定义配置的也就是
Layout
而已,而自定义就是玩的PatternLayout
,这个类有一个setConversionPattern()
方法,查看这个方法的源码
public void setConversionPattern(String conversionPattern) { this.pattern = conversionPattern; this.head = this.createPatternParser(conversionPattern).parse(); }
- 从中发现,需要配置的就是
String conversionPattern
,因此:在log4j.properties
配置文件中添加上conversionPattern
属性配置即可,当然:这个属性配置遵循一定的写法,写法如下:
%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 输出代码中的行号 %% 输出一个 "%" 字符 可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式 [p]:[]中必须有10个字符,由空格来进行补齐,信息右对齐 [%-10p]:[]中必须有10个字符,由空格来进行补齐,信息左对齐,应用较广泛 上述举例:[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
- 修改了
log4j.properties
的配置如下( 做的修改就是最后两个配置项 )
# rootLogger所有logger的根配置 - log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....log4j.rootLogger=debug,console# appender输出控制器配置 log4j.appender.+appenderName=某一种输出类型 - 采用Console控制台的方式举例log4j.appender.console=org.apache.log4j.ConsoleAppender# 输出的格式配置log4j.appender.+appenderName+layout=某种layout格式类型 - 注意:这里类型改了,是PatternLayout,即自定义log4j.appender.console.layout=org.apache.log4j.PatternLayout# 编写自定义输出格式( 直接把上面举例的拿过来 ) - 注意:加上了刚刚说的conversionPattern属性log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
- 测试代码
public void test04(){ LogLog.setInternalDebugging(true); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
2.2.5、将日志输出到文件中
2.2.5.1、改造log4j.properties文件
# rootLogger所有logger的根配置 - 这里再加一个filelog4j.rootLogger=debug,console,file# 控制台的appender输出控制器配置log4j.appender.console=org.apache.log4j.ConsoleAppender# 采用自定义控制台输出的格式log4j.appender.console.layout=org.apache.log4j.PatternLayout# 编写控制台自定义输出格式log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n# 再来一份,变为file的配置 - 把console改为file即可log4j.appender.file=org.apache.log4j.FileAppenderlog4j.appender.file.layout=org.apache.log4j.PatternLayoutlog4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
2.2.5.2、指定文件输出位置 及 字符编码设置
- 查看
FileAppender
源码,首先看到的是四个属性
// 表示日志文件是否采用内容追加的方式 - 源码中有一个构造方法,这个的默认值是true protected boolean fileAppend; protected String fileName; protected boolean bufferedIO; // 日志文件的大小 - 源码的构造方法中默认值是8192 protected int bufferSize; // 构造方法源码 public FileAppender() { this.fileAppend = true; this.fileName = null; this.bufferedIO = false; this.bufferSize = 8192; }
- 在
FlieAppender
中还有一个setFile()
的方法,得知这个就是设置文件的方法 ,也就是文件日志文件存放路径位置
public void setFile(String file) { String val = file.trim(); this.fileName = val; }
- 这里面需要传一个file参数进去,因此:通过
MyBatis
的知识就知道log4j.properties
配置中的这个对应属性就是file了( 截掉set,得到属性名 )
# rootLogger所有logger的根配置 - 这里再加一个filelog4j.rootLogger=debug,console,file# 控制台的appender输出控制器配置log4j.appender.console=org.apache.log4j.ConsoleAppender# 采用自定义控制台输出的格式log4j.appender.console.layout=org.apache.log4j.PatternLayout# 编写控制台自定义输出格式log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n# 再来一份,变为file的配置 - 把console改为file即可log4j.appender.file=org.apache.log4j.FileAppenderlog4j.appender.file.layout=org.apache.log4j.PatternLayoutlog4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n# 日志文件保存路径 - 注:前一个file为自定义的appenderName;后一个file是日志文件输出路径的属性,要是怕看错眼,可以把后者换为File大写也没错log4j.appender.file.file=D:log4jzixieqinglog4j.log
- 现在看
FileAppender
的父类WriterAppender
,去看字符编码设置
protected boolean immediateFlush; // 这个属性就是编码设置 protected String encoding; protected QuietWriter qw;
- 因此:加上日志文件输出路径和字符编码之后的
log4.properties
配置为:
# 控制台日志输出设置log4j.rootLogger=debug,console,filelog4j.appender.console=org.apache.log4j.ConsoleAppenderlog4j.appender.console.layout=org.apache.log4j.PatternLayoutlog4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n# 日志文件输出设置log4j.appender.file=org.apache.log4j.FileAppenderlog4j.appender.file.layout=org.apache.log4j.PatternLayoutlog4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%nlog4j.appender.file.file=D:log4jzixieqinglog4j.loglog4j.appender.file.encoding=UTF-8
2.2.5.3、拆分日志文件
-
一份日志不可能用于一直记录下去,那样的话,日志文件体积就太大了
-
继续查看
FileAppender
源码,看实现类
2.2.5.3.1、RollingFileAppender实现类
- 这个玩意儿就是利用文件大小来进行日志拆分
- 源码中有两个属性需要关注
// 达到多大文件时进行日志拆分 protected long maxFileSize = 10485760L; // 一共能够拆分出多少份日志文件 protected int maxBackupIndex = 1;
- 因此:现在将
log4j.properties
配置文件修改一下即可实现日志根据文件大小进行拆分
# 注意:记得在log4j.rootLoger=debug,console,file这一句中加上下面的这个文件大小进行拆分的配置# 如:log4j.rootLoger=debug,console,rollingFilelog4j.appender.rollingFile=org.apache.log4j.RollingFileAppenderlog4j.appender.rollingFile.layout=org.apache.log4j.PatternLayoutlog4j.appender.rollingFile.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%nlog4j.appender.rollingFile.file=D:log4jzixieqinglog4j.log# 文件多大时进行日志拆分log4j.appender.rollingFile.maxFileSize=10MB# 最多可以拆分多少份log4j.appender.rollingFile.maxBackupIndex=50
2.2.5.3.2、DailyRollingFileAppender实现类 - 建议用
- 这个东西就是根据时间来进行日志拆分,看源码,有这么一个属性
// 时间格式,默认值就是如下的天 private String datePattern = "'.'yyyy-MM-dd";
- 在
log4j.properties
配置文件中修改成如下的配置即可使用
# 一样的,要使用这种方式:记得在log4j.rootLogger=debug,console,file这一句中加上下面的这个文件大小进行拆分的配置# 如:log4j.rootLogger=debug,console,dateRollingFilelog4j.appender.dateRollingFile=org.apache.log4j.DailyRollingFileAppenderlog4j.appender.dateRollingFile.layout=org.apache.log4j.PatternLayoutlog4j.appender.dateRollingFile.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%nlog4j.appender.dateRollingFile.file=D:log4jzixieqinglog4j.log# 加上时间格式 - 下面的值根据实际情况即可log4j.appender.dateRollingFile.datePattern='.'yyyy-MM-dd
2.2.6、日志持久化到数据库
表基础字段
CREATE TABLE tbl_log( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) DEFAULT NULL COMMENT '项目名称', createTime varchar(100) DEFAULT NULL COMMENT '创建时间', level varchar(10) DEFAULT NULL COMMENT '日志级别', category varchar(100) DEFAULT NULL COMMENT '所在类的全路径', fileName varchar(100) DEFAULT NULL COMMENT '文件名称', message varchar(255) DEFAULT NULL COMMENT '日志消息', PRIMARY KEY(id))
项目MySQL驱动
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> <scope>runtime</scope> </dependency>
log4.properties配置
# 配置appender输出方式 输出到数据库表 - 注意:在rootManager中加上logDB,如:log4j.rootLogger=debug,console,logDBlog4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppenderlog4j.appender.logDB.layout=org.apache.log4j.PatternLayoutlog4j.appender.logDB.Driver=com.mysql.jdbc.Driverlog4j.appender.logDB.URL=jdbc:mysql://localhost:3306/testlog4j.appender.logDB.User=rootlog4j.appender.logDB.Password=072413log4j.appender.logDB.Sql=INSERT INTO tbl_log(name,createTime,level,category,fileName,message) values('project_log','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%m')
测试代码
@Test public void test07(){ Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
2.2.7、自定义logger
- 前面配置文件中使用的都是
rootManager
的logger,接下来就自定义一个logger,看PropertyConfigurator
类的源码,它里面有一个属性
// 自定义logger配置的写法 - 这后面拼接的就是自定义的logger名字 static final String LOGGER_PREFIX = "log4j.logger.";
- 其中:上述说的自定义logger名字遵循父子关系,也就是包关系
如:cn.zixieqing.log4j.test.Log4jTest01 它的父logger就是上层的路径或者是更上层的路径 例如: cn.zixieqing.log4j.test cn.zixieqing.log4j ... cn
修改log4j.properties
# 根logger,输出级别是trace,在console控制台进行输出log4j.rootLogger=trace,console# 自定义logger,级别为info,在file文件中输出log4j.logger.cn.zixieqing.log4j.test=info,file# 自定义logger,是apache的,级别为errorlog4j.logger.org.apache=error
自定义logger的注意点
- 如果根节点的logge( 即:rootManager ) 和 自定义logger配置的输出位置是不同的,则取二者的并集,配置的位置都会进行输出操作
- 如果二者配置的日志级别不同,以我们自定义logger的输出级别为主
2.2.8、一份简单的log4j.properties配置
log4j.rootLogger=DEBUG,console,file #控制台输出的相关设置 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.Threshold=DEBUG log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%c]-%m%n #文件输出的相关设置 log4j.appender.file = org.apache.log4j.RollingFileAppender log4j.appender.file.File=./1og/zixieqing.log log4j.appender.file.MaxFileSize=10mb log4j.appender.file.Threshold=DEBUG log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]‰m%n #日志输出级别 log4j.logger.org.mybatis=DEBUG log4j.logger.java.sq1=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
3、JCL
-
全称为Jakarta Commons Logging,是Apache提供的一个通用日志AP
-
注意:这个玩意儿本身没有记录日志的实现功能,而是相当于一个门面。我们可以自由选择第三方的日志组件( log4j、JUL )作为具体实现,common-logging会通过动态查找的机制,在程序运行时自动找出真正使用的日志库
JCL的组成
- JCL 有两个基本的抽象类
- Log:日志记录器
- LogFactory:日志工厂(负责创建Log实例)
3.1、玩JCL
依赖
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
测试
@Test void jclQuickStartTest() { // LogFactory是org.apache.commons.logging.LogFactory Log log = LogFactory.getLog(JCLTest01.class); log.info("info信息"); }
- 运行之后看效果会发现:输出格式是JUL格式
- 即:如果没有任何第三方日志框架的时候,默认使用的就是JUL
3.2、引入log4j
<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
- 然后使用
log4j.properties
配置文件 - 当再次进行测试时就会变成log4j的输出配置
JCL源码分析
Log接口的4个实现类 JDk13 JDK14 正常java.util.logging Log4j 我们集成的log4j Simple JCL自带实现类 (1)查看Jdk14Logger证明里面使用的是JUL日志框架 - 看import引入的就可以得知 (2)查看Log4JLogger证明里面使用的是Log4j日志框架 - 看import引入的就可以得知 (3)观察LogFactory,看看如何加载的Logger对象 这是一个抽象类,无法实例化 需要观察其实现类LogFactoryImpl (4)观察LogFactoryImpl 真正加载日志实现使用的就是这个实现类LogFactoryImpl (5)进入getLog - 采用打断点debug能够更清晰地看清楚 进入getInstance 找到instance = this.newInstance(name);,继续进入 找到instance = this.discoverLogImplementation(name); 表示发现一个日志的实现 for(int i = 0; i < classesToDiscover.length && result == null; ++i) { result = this.createLogFromClass(classesToDiscover[i], logCategory, true); } 遍历我们拥有的日志实现框架 遍历的是一个数组,这个数组是按照 log4j jdk14 jdk13 SimpleLogger 的顺序依次遍历 表示的是,第一个要遍历的就是log4j,如果有log4j则执行该日志框架 如果没有,则遍历出来第二个,使用jdk14的JUL日志框架 以此类推 result = this.createLogFromClass(classesToDiscover[i], logCategory, true); 表示帮我们创建Logger对象 在这个方法中,我们看到了 c = Class.forName(logAdapterClassName, true, currentCL); 是取得该类型的反射类型对象 使用反射的形式帮我们创建logger对象 constructor = c.getConstructor(this.logConstructorSignature);
4、SLF4J
- 这也是一个日志门面( 门面模式 / 外观模式 ),其核心为:外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用
常见的日志门面和日志实现
-
常见的日志实现:JUL、log4j、logback、log4j2
-
常见的日志门面 :JCL、slf4j
-
出现顺序 :log4j -->JUL-->JCL--> slf4j --> logback --> log4j2
了解SLF4J
- 全称:Simple Logging Facade For Java,简单日志门面
- 主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等
- SLF4J最重要的两个功能就是对于日志框架的绑定以及日志框架的桥接
4.1、玩SLF4J
4.1.1、快速上手
准备知识
- SLF4J对日志的级别划分
trace、debug、info、warn、error五个级别 trace:日志追踪信息 debug:日志详细信息 info:日志的关键信息 默认打印级别 warn:日志警告信息 error:日志错误信息
- 在没有任何其他日志实现框架集成的基础之上,slf4j使用的就是自带的框架slf4j-simple
- 注意点:slf4j-simple也必须以单独依赖的形式导入进来
<!--slf4j 自带的简单日志实现 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.25</version> </dependency>
入门
- 依赖
<!--slf4j 核心依赖--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!--slf4j 自带的简单日志实现 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.25</version> </dependency>
- 测试代码
@Test public void test01(){ // 是slf4j包下的 Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); logger.trace("trace信息"); logger.debug("debug信息"); logger.info("info信息"); logger.warn("warn信息"); logger.error("error信息"); }
4.1.2、动态信息输出
- 本质:使用占位符
- 有些时候输出的日志信息,需要我们搭配动态的数据,这些数据有可能是信息,有可能是数据库表中的数据
@Test public void test02(){ Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); String name = "zs"; int age = 23; /*字符串拼接的形式:logger.info("学生信息-姓名:"+name+";年龄:"+age); 使用JCL的形式:logger.info("学生信息-姓名:{},年龄:{}",new Object[]{name,age}); 这上面两者虽然都可以做到相应的输出,但是:麻烦 */ // 使用SLF4J的形式 logger.info("学生信息-姓名:{},年龄:{}",name,age); }
- 注意点:如果后面拼接的字符串是一个对象,那么{}并不能充当占位,进行字符串拼接
4.2.3、输出异常信息
@Test public void test03(){ /* 日志对于异常信息的处理 一般情况下,我们在开发中的异常信息,都是记录在控制台上(我们开发环境的一种日志打印方式) 我们会根据异常信息提取出有用的线索,来调试bug 但是在真实生产环境中(项目上线),对于服务器或者是系统相关的问题 在控制台上其实也会提供相应的异常或者错误信息的输出 但是这种错误输出方式(输出的时间,位置,格式...)都是服务器系统默认的 我们可以通过日志技术,选择将异常以日志打印的方式,进行输出查看 输出的时间,位置(控制台,文件),格式,完全由我们自己去进行定义 */ Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { // e.printStackTrace(); logger.info("XXX类中的XXX方法出现了异常,请及时关注信息"); // e是引用类型对象,不能根前面的{}做有效的字符串拼接 // logger.info("具体错误是:{}",e); // 我们不用加{},直接后面加上异常对象e即可 - 这是利用了重载方法info(String message, Throwable throwable ) logger.info("具体错误是:",e); } }
4.2.4、SLF4J与日志绑定
- 官网中有一张图,官网地址:https://www.slf4j.org/
-
图中分为了三部分
- 1、在没有绑定任何日志实现的基础之上,日志是不能够绑定实现任何功能的
- slf4j-simple是slf4j官方提供的
- 使用的时候,也是需要导入依赖,自动绑定到slf4j门面上
- 如果不导入,slf4j 核心依赖是不提供任何实现的
- 2、logback和simple(包括nop)
- 都是slf4j门面时间线后面提供的日志实现,所以API完全遵循slf4j进行的设计
- 只需要导入想要使用的日志实现依赖,即可与slf4j无缝衔接
- 注意:nop虽然也划分到实现中了,但是它是指不实现日志记录
- 3、log4j和JUL
- 都是slf4j门面时间线前面的日志实现,所以API不遵循slf4j进行设计
- 通过适配桥接的技术,完成的与日志门面的衔接
- 1、在没有绑定任何日志实现的基础之上,日志是不能够绑定实现任何功能的
4.2.5、绑定logback
依赖
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency>
测试
public class SLF4JTest { @Test public void bingingLogTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { logger.info("具体错误是:",e); } }}
结果
10:50:15.391 [main] INFO com.zixieqing.SLF4JTest - 具体错误是:java.lang.ClassNotFoundException: aaa at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) .......
- 这种就看起来很清爽,如果加上
slf4j-simple
那输出结果又是另一回事,测试跳过 - 上面这种就是不用去管底层到底采用的是哪一种日志实现,可是上面的源代码完全没有改变,照常写,这就是日志门面的好处
4.2.6、slf4j-nop禁止日志打印
依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!-- 注意:有坑儿,和依赖导入顺序有关 如·:要是将这个依赖放到logback的下面,那么屁用都没有 但是:把这个依赖放在logback的前面就可以实现日志禁止打印了 原因:在slf4j环境下,要是同时出现了多个日志实现,默认使用先导入的日志实现 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>1.7.30</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency>
测试
@Test public void slf4jNopTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { logger.info("具体错误是:",e); } }
结果
SLF4J: Class path contains multiple SLF4J bindings.SLF4J: Found binding in [jar:file:/D:/install/maven/apache-maven-3.6.1/maven-repo/org/slf4j/slf4j-nop/1.7.30/slf4j-nop-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class]SLF4J: Found binding in [jar:file:/D:/install/maven/apache-maven-3.6.1/maven-repo/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.SLF4J: Actual binding is of type [org.slf4j.helpers.NOPLoggerFactory]
- 日志内容的相关打印就没了
- 注意:如果想要让nop发挥效果,禁止所有日志的打印,那么就必须要将slf4j-nop的依赖放在所有日志实现依赖的上方
4.2.7、绑定log4j
- 玩这个就需要注意日志框架时间线的问题了
- 出现顺序 :log4j -->JUL-->JCL--> slf4j --> logback --> log4j2
- 也就是在slf4j之后出现的( 如:logback、log4j2 ),这些都准许slf4j的规范,所以直接导入对应的依赖之后就可以使用,但是在slf4j之前的,并没有预料到会出现后续这些规范嘛,而slf4j想要绑定log4j就需要一个
slf4j-log4j12
的适配器
玩玩绑定log4j
依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!--slf4j和log4j的适配器 - 想要slf4j能够绑定log4j就需要这个--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.30</version> </dependency>
测试
@Test public void slf4jBindingLog4jTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); logger.info("info信息"); }
结果
# 虽然没有输出消息,但是有如下这个话就足够了No appenders could be found for logger# 这表示没有appender嘛,加上log4j.properties配置文件就可以使用了log4j:WARN No appenders could be found for logger (com.zixieqing.SLF4JTest).log4j:WARN Please initialize the log4j system properly.log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
其他日志框架的绑定就看官网的那张图,在那张图中对绑定谁时导入哪一个依赖都有备注
4.2.8、slf4j源码分析流程
进入到getLogger 看到Logger logger = getLogger(clazz.getName()); 进入重载的getLogger ILoggerFactory iLoggerFactory = getILoggerFactory(); 用来取得Logger工厂实现的方法 进入getILoggerFactory() 看到以双重检查锁的方式去做判断 执行performInitialization(); 工厂的初始化方法 进入performInitialization() bind()就是用来绑定具体日志实现的方法 进入bind() 看到Set集合Set<URL> staticLoggerBinderPathSet = null; 因为当前有可能会有N多个日志框架的实现 看到staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); 进入findPossibleStaticLoggerBinderPathSet() 看到创建了一个有序不可重复的集合对象 LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet(); 声明了枚举类的路径,经过if else判断,以获取系统中都有哪些日志实现 看到Enumeration paths; if (loggerFactoryClassLoader == null) { paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH); } else { paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH); } 我们主要观察常量STATIC_LOGGER_BINDER_PATH 通过常量我们会找到类StaticLoggerBinder 这个类是以静态的方式绑定Logger实现的类 来自slf4j-JDK14的适配器 进入StaticLoggerBinder 看到new JDK14LoggerFactory(); 进入JDK14LoggerFactory类的无参构造方法 看到java.util.logging.Logger.getLogger(""); 使用的就是jul的Logger 接着观察findPossibleStaticLoggerBinderPathSet 看到以下代码,表示如果还有其他的日志实现 while(paths.hasMoreElements()) { URL path = (URL)paths.nextElement(); 将路径添加进入 staticLoggerBinderPathSet.add(path); } 回到bind方法 表示对于绑定多实现的处理 reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); 如果出现多日志实现的情况 则会打印 Util.report("Class path contains multiple SLF4J bindings.");
通过源码总结:
在真实生产环境中,slf4j只绑定一个日志实现框架就可以了
绑定多个,默认使用导入依赖的第一个,而且会产生没有必要的警告信息
4.2.9、slf4j日志重构
有这么一个情况:项目原本使用的是log4j日志,但是随着技术的迭代,需要使用另外的日志框架,如:slf4j+logback,因此:此时不可能说去该源码,把所有使用log4j的地方都改成slf4j
- 这种情况,在slf4j官网中就提供了对应的解决方式,就是加个依赖而已,加的东西就叫做桥接器
4.2.9.1、玩一下桥接器
演示bug:先保证项目没有其他任何的干扰,如:另外的依赖、另外日志的代码
依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
log4j.properties配置文件
log4j.rootLogger=debug,consolelog4j.appender.console=org.apache.log4j.ConsoleAppenderlog4j.appender.console.layout=org.apache.lo4j.SimpleLayout
测试代码
package com.zixieqing;import org.apache.log4j.LogManager;import org.apache.log4j.Logger;import org.junit.Test; @Test public void bridgingTest() { // 假设项目用的是log4j Logger logger = LogManager.getLogger(SLF4JTest.class); logger.info("inf信息"); }
此时,项目升级,改用slf4j+logback来当日志
-
1、去掉log4j的依赖
-
2、添加slf4j的桥接组件,所以现在的依赖就变成如下的样子
-
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!--这个就是log4j和slf4j的桥接组件--> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency>
-
要引入什么桥接组件,看要怎么重构,然后根据官网的说明来弄就行
-
测试
- 源代码直接不用动
package com.zixieqing;import org.apache.log4j.LogManager;import org.apache.log4j.Logger;import org.junit.Test; @Test public void bridgingTest() { // 假设项目用的是log4j Logger logger = LogManager.getLogger(SLF4JTest.class); logger.info("inf信息"); }
结果
14:45:09.901 [main] INFO com.zixieqing.SLF4JTest - inf信息
5、LOGBACK
- 这是log4j的作者跳槽之后开发的另一款日志框架,比log4j出色很多
- Logback当前分成三个模块:logback-core,logback- classic和logback-access
- logback-core是其它两个模块的基础模块
- logback-classic是log4j的一个改良版本。此外logback-classic完整实现SLF4J API。可以很方便地更换成其它日志系统如log4j或JDK14 Logging
- logback-access访问模块与Servlet容器集成提供通过Http来访问日志的功能
logback的组成
- Logger: 日志的记录器,主要用于存放日志对象,也可以定义日志类型、级别
- Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等
- Layout: 负责把事件转换成字符串,格式化的日志信息的输出
- 注意点:在Logback中Layout对象被封装在encoder中,即 未来使用的encoder其实就是Layout
logback配置文件的类型
- Logback提供了3种配置文件
- logback.groovy
- logback-test.xml
- logback.xml
- 我们用的一般是xml的格式
logback的日志输出格式
日志输出格式: %-10level 级别 案例为设置10个字符,左对齐 %d{yyyy-MM-dd HH:mm:ss.SSS} 日期 %c 当前类全限定名 %M 当前执行日志的方法 %L 行号 %thread 线程名称 %m或者%msg 信息 %n 换行
5.1、玩logback
5.1、logback的日志级别
error > warn > info > debug > trace 其中:logback的默认日志级别就是debug
5.2、快速上手
依赖
<!-- ogback-classic 中包含了基础模板 ogback-core--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency> <!--logback是日志实现,也需要通过日志门面来集成--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>
测试
public class LogBackTest { @Test public void quickStartTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }}
结果
15:44:27.032 [main] ERROR com.zixieqing.LogBackTest - error信息15:44:27.033 [main] WARN com.zixieqing.LogBackTest - warn信息15:44:27.033 [main] INFO com.zixieqing.LogBackTest - info信息15:44:27.033 [main] DEBUG com.zixieqing.LogBackTest - debug信息
- 得出信息,logback的默认日志级别就是debug
5.3、简单认识logback的配置文件
<?xml version="1.0" encoding="utf-8" ?><!--logback的所有配置都需要在configuration标签中机芯--><configuration> <!--通用属性配置 - 目的:在下面配置需要的地方使用${name}的形式,方便的取得value值 name 起的名字 见名知意即可 value 值 --> <!--配置日志输出格式--> <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L %thread %m%n"/> <!--配置日志输出类型 name 乱取,见名知意即可 class 类型全类路径 --> <appender name = "consoleAppender" class = "ch.qos.logback.core.ConsoleAppender"> <!--配置日志的颜色--> <target> <!--还可以用system.out就是常见的黑白色--> system.err </target> <!--配置日志输出格式,直接引用前面配置的property通用属性--> <encoder class = "ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${pattern}</pattern> </encoder> </appender> <!--配置root Logger--> <root level="DEBUG"> <appender-ref ref="consoleAppender"/> <oot></configuration>
- 其中:
<property name="" value = ""
中vlaue
的参数配置参考
日志输出格式: %-10level 级别 案例为设置10个字符,左对齐 %d{yyyy-MM-dd HH:mm:ss.SSS} 日期 %c 当前类全限定名 %M 当前执行日志的方法 %L 行号 %thread 线程名称 %m或者%msg 信息 %n 换行
测试
public class LogBackTest { @Test public void quickStartTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }}
5.4、将日志输出到文件中
在logback.xml
中加入如下的配置即可
<!--配置全局的文件输出路径--> <property name="filePath" value="D:test"/> <!--定义文件的appender输出类型--> <appender name="fileAppender" class="ch.qos.logback.core.FileAppender"> <!--文件路径 和 文件名 --> <file>${filePath}/logback.log</file> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--文件输出的格式--> <pattern>${pattern}</pattern> </encoder> </appender> <!--记得在root logger中引用一下fileAppender的配置--> <!--配置root Logger--> <root level="DEBUG"> <appender-ref ref="consoleAppender"/> <!--引用配置的文件appender输出配置--> <appender-ref ref="fileAppender"/> <oot>
测试代码
@Test public void fileAppenderTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
通过结果观察会发现:logback的日志文件输出默认是以追加的形式添加新日志内容
5.5、以HTML格式记录日志文件
- 当日志文件不是很多的时候,可以采用这种方式,因为这种HTML里面的样式和格式都可以让logback进行生成,而里面的内容使我们加进去的而已,因此:这样一看就知道,HTML文件占用的内存容量就蛮大了
logback.xml
配置文件编写
<!--定义HTML文件输出格式,本质还是文件,所以class是FileAppender--> <appender name="HTMLFileAppender" class="ch.qos.logback.core.FileAppender"> <file>${filePath}logback.log</file> <!--注意:这里采用的是LayoutWrappingEncoder--> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <!--注意:采用layout标签包裹,格式类型为HTMLLayout--> <layout class="ch.qos.logback.classic.html.HTMLLayout"> <pattern>${pattern}</pattern> </layout> </encoder> </appender> <!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <appender-ref ref="HTMLFileAppender"/> <oot>
测试
@Test public void htmlFileAppenderTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
- 运行程序即可生成HTML文件
5.6、拆分日志 和 归档压缩
<!-- 配置文件的appender 可拆分归档的文件 --> <appender name="roll" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 输入格式 --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${pattern}</pattern> </encoder> <!-- 引入文件位置 --> <file>${logDir}oll_logback.log</file> <!-- 指定拆分规则 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!-- 按照时间和压缩格式声明文件名 压缩格式gz --> <fileNamePattern>${logDir}oll.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern> <!-- 按照文件大小来进行拆分 --> <maxFileSize>1KB</maxFileSize> <ollingPolicy> </appender> <!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <!--<appender-ref ref="HTMLFileAppender"/>--> <appender-ref ref="roll"/> <oot>
- 注意点:只要使用到拆分日志的事情,那么
<fileNamePattern></fileNamePattern>
标签必须有,源码中有说明
在TimeBasedRollingPolicy类中有一个常量 static final String FNP_NOT_SET ="The FileNamePattern option must be set before using TimeBasedRollingPolicy. ";这个常量值告知的结果就是上面的注意点
- 另外:其他有哪些属性,而每一个class对应对应下有哪些标签可以用,那就是class指定值中的属性名,也就是标签名,看源码即可
@Test public void rollFileAppenderTest() { for (int i = 0; i < 1000; i++) { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
5.7、过滤器
<!-- 配置控制台的appender 使用过滤器 --> <appender name="consoleFilterAppender" class="ch.qos.logback.core.ConsoleAppender"> <target> System.out </target> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${pattern}</pattern> </encoder> <!-- 配置过滤器 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <!-- 设置日志的输出级别 --> <level>ERROR</level> <!-- 高于level中设置的级别,则打印日志 --> <onMatch>ACCEPT</onMatch> <!-- 低于level中设置的级别,则屏蔽日志 --> <onMismatch>DENY</onMismatch> </filter> </appender> <!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <!--<appender-ref ref="HTMLFileAppender"/>--> <!--<appender-ref ref="roll"/>--> <!--解开console日志打印logger--> <appender-ref ref="consoleAppender"/> <oot>
- 测试代码照常用,然后在console控制台输出的内容会根据上述配置的filter过滤器罢对应的内容过滤掉,从而不打印出来
5.8、异步日志
<!-- 1、配置异步日志 --> <appender name="asyncAppender" class="ch.qos.logback.classic.AsyncAppender"> <!-- 引用appender配置,即:让什么输出类型的日志进行异步操作,可以选择consoleAppender、fileAppender.... --> <appender-ref ref="fileAppender"/> </appender> <!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <!--<appender-ref ref="HTMLFileAppender"/>--> <!--<appender-ref ref="roll"/>--> <!--解开console日志打印logger--> <!--<appender-ref ref="consoleAppender"/>--> <!--2、在root logger中引用一下配置的异步日志--> <appender-ref ref="asyncAppender"/> <oot>
另外:在上面的1、配置异步日志
里面还有两个配置属性
下面这个配置的是一个阈值,当队列的剩余容量小于这个阈值的时候,当前日志的级别 trace、debug、info这3个级别的日志将被丢弃 设置为0,说明永远都不会丢弃trace、debug、info这3个级别的日志 <discardingThreshold>0</discardingThreshold> 配置队列的深度,这个值会影响记录日志的性能,默认值就是256 <queueSize>256</queueSize> 关于这两个属性,一般情况下,使用默认值即可 这两个属性不要乱配置,会影响系统性能,了解其功能即可
@Test public void asyncAppenderTest(){ Logger logger = LoggerFactory.getLogger(LOGBACKTest01.class); // 日志打印操作 for (int i = 0; i < 100; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } // 系统本身业务相关的其他操作 System.out.println("系统本身业务相关的操作1"); System.out.println("系统本身业务相关的操作2"); System.out.println("系统本身业务相关的操作3"); System.out.println("系统本身业务相关的操作4"); }
5.9、自定义logger
- 还是老样子,自定义logger就是为了替换下面这一堆
<!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <!--<appender-ref ref="HTMLFileAppender"/>--> <!--<appender-ref ref="roll"/>--> <!--解开console日志打印logger--> <!--<appender-ref ref="consoleAppender"/>--> <!--2、在root logger中引用一下配置的异步日志--> <appender-ref ref="asyncAppender"/> <oot>
自定义logger配置
<!-- additivity="false" 表示不继承rootlogger --> <logger name="com.bjpowernode" level="info" additivity="false"> <!-- 在自定义logger中配置appender --> <appender-ref ref="consoleAppender"/> </logger>
- 测试代码和前面一样,跳过
6、LOG4J2
- 这玩意儿虽然叫log4j2,也就是对log4j做了增强,但是更多的其实是对logback不足做了优化
log4j2的特征
性能提升
- Log4j2包含基于LMAX Disruptor库的下一代异步记录器。在多线程场景中,异步记录器的吞吐量比Log4j 1.x和Logback高18倍,延迟低
自动重新加载配置
- 与Logback一样,Log4j2可以在修改时自动重新加载其配置。与Logback不同,它会在重新配置发生时不会丢失日志事件
高级过滤
- 与Logback一样,Log4j2支持基于Log事件中的上下文数据,标记,正则表达式和其他组件进行过滤
- 此外,过滤器还可以与记录器关联。与Logback不同,Log4j2可以在任何这些情况下使用通用的Filter类
插件架构
- Log4j使用插件模式配置组件。因此,无需编写代码来创建和配置Appender,Layout,Pattern Converter等。在配置了的情况下,Log4j自动识别插件并使用它们
无垃圾机制
- 在稳态日志记录期间,Log4j2 在独立应用程序中是无垃圾的,在Web应用程序中是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应性能
log4j2的最佳搭配
- 采用
log4j2 + slf4j
的方式
6.1、玩log4j2
6.1.1、快速上手
依赖
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.17.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>
测试
public class Log4j2Test { @Test public void quickStartTest() { // 是org.apache.logging.log4j包下的 Logger logger = LogManager.getLogger(Log4j2Test.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); }}
结果
21:18:43.909 [main] FATAL com.zixieqing.Log4j2Test - fatal信息21:18:43.911 [main] ERROR com.zixieqing.Log4j2Test - error信息
- 得出结论:log4j2的默认级别是error级别
6.1.2、简单了解log4j2的配置文件
- log4j2是参考logback创作出来的,所以配置文件也是使用xml
- log4j2同样是默认加载类路径(resources)下的log4j2.xml文件中的配置
- 但是:log4j2.xml和logback.xml有区别,第一点就是log4j2.xml中的标签名字是首字母大写;第二点就是多个单词采用的是驼峰命名法,还有其他的区别
简单的log4j2.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒 注意:这个标签不要加xmlns属性,加上之后就有表空间约束,指不定会搞出问题--><Configuration> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> </Root> </Loggers></Configuration>
测试代码
@Test public void log4j2XmlTest() { // 是org.apache.logging.log4j包下的 Logger logger = LogManager.getLogger(Log4j2Test.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); }
结果
直接输出如下内容: fatal信息 error信息 warn信息 info信息
6.1.3、SLF4J + LOG4J2 - 从此开始都是重点
- slf4j门面调用的是log4j2的门面,再由log4j2的门面调用log4j2的实现
- 步骤
- 1、导入slf4j的日志门面
- 2、导入log4j2的适配器
- 3、导入log4j2的日志门面
- 4、导入log4j2的日志实现
- 5、编写log4j2.xml
依赖
<!--slf4j门面--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!--log4j-slf4j适配器--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.17.1</version> </dependency> <!--log4j门面--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.17.1</version> </dependency> <!--logj日志实现--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.1</version> </dependency> <dependency>
log4j2.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒 注意:这个标签不要加xmlns属性,加上之后就有表空间约束,指不定会搞出问题--><Configuration> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> </Root> </Loggers></Configuration>
测试
- 保证没有其他的测试代码 或者说 保证
import
导入是org.slf4j
门面的
@Test public void log4j2AndSlf4jTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
结果
error信息warn信息info信息debug信息
6.1.4、将日志输出到文件
- 目前的代码都是基于前面配置好的log4j2 + slf4j的形式
log4j2.xml的配置文件
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> </Root> </Loggers></Configuration>
@Test public void fileAppenderTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
6.1.5、拆分日志
log4j2.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> <!-- 按照指定规则来拆分日志文件 fileName:日志文件的名字 filePattern:日志文件拆分后文件的命名规则 $${date:yyyy-MM-dd}:根据日期当天,创建一个文件夹 例如:2021-01-01这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) 2021-01-02这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) rollog-%d{yyyy-MM-dd-HH-mm}-%i.log 为文件命名的规则:%i表示序号,从0开始,目的是为了让每一份文件名字不会重复 --> <RollingFile name="rollingFile" fileName="${logPath}ollingLog.log" filePattern="${logPath}/$${date:yyyy-MM-dd}ollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log"> <!-- 日志消息格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> <Policies> <!-- 在系统启动时,触发拆分规则,产生一个日志文件 --> <OnStartupTriggeringPolicy/> <!-- 按照文件的大小进行拆分 注:这里虽然限制是10KB,但是真实生成出来的文件一般都是比这数值大1KB--> <SizeBasedTriggeringPolicy size="10KB"/> <!-- 按照时间节点进行拆分 拆分规则就是filePattern--> <TimeBasedTriggeringPolicy/> </Policies> <!-- 在同一目录下,文件的个数限制,如果超出了设置的数值,则根据时间以新的覆盖旧--> <DefaultRolloverStrategy max="30"/> </RollingFile> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> <AppenderRef ref="rollingFile"/> </Root> </Loggers></Configuration>
测试
@Test public void rollingLogTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
6.1.6、异步日志
- 这个技术就是log4j2最骚的一个点
- Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger,分别对应前面我们说的Appender组件和Logger组件
- 注意这是两种不同的实现方式,在设计和源码上都是不同的体现
6.1.6.1、AsyncAppender方式 - 了解
- 是通过引用别的Appender来实现的
- 这种方式使用的就是
<Async></Async>
标签
当有日志事件到达时,会开启另外一个线程来处理它们。需要注意的是,如果在Appender的时候出现异常,对应用来说是无法感知的。AsyncAppender应该在它引用的Appender之后配置,默认使用 java.util.concurrent.ArrayBlockingQueue实现而不需要其它外部的类库。 当使用此Appender的时候,在多线程的环境下需要注意,阻塞队列容易受到锁争用的影响,这可能会对性能产生影响。这时候,我们应该考虑使用无锁的异步记录器(AsyncLogger)
步骤
-
1、添加异步日志依赖
-
<!--asyncAppender异步日志依赖--> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.4.2</version> </dependency>
-
2、在Appenders标签中,对于异步进行配置
-
使用Async标签
-
<!-- 配置异步日志 --> <Async name="asyncAppender"> <!-- 引用在前面配置的appender --> <AppenderRef ref="fileAppender"/> </Async>
-
-
-
3、rootlogger引用Async
-
<!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="asyncAppender"/> </Root> </Loggers>
-
完整的log4j2.xml配置
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> <!-- 按照指定规则来拆分日志文件 fileName:日志文件的名字 filePattern:日志文件拆分后文件的命名规则 $${date:yyyy-MM-dd}:根据日期当天,创建一个文件夹 例如:2021-01-01这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) 2021-01-02这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) rollog-%d{yyyy-MM-dd-HH-mm}-%i.log 为文件命名的规则:%i表示序号,从0开始,目的是为了让每一份文件名字不会重复 --> <RollingFile name="rollingFile" fileName="${logPath}ollingLog.log" filePattern="${logPath}/$${date:yyyy-MM-dd}ollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log"> <!-- 日志消息格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> <Policies> <!-- 在系统启动时,触发拆分规则,产生一个日志文件 --> <OnStartupTriggeringPolicy/> <!-- 按照文件的大小进行拆分 注:这里虽然限制是10KB,但是真实生成出来的文件一般都是比这数值大1KB--> <SizeBasedTriggeringPolicy size="10KB"/> <!-- 按照时间节点进行拆分 拆分规则就是filePattern--> <TimeBasedTriggeringPolicy/> </Policies> <!-- 在同一目录下,文件的个数限制,如果超出了设置的数值,则根据时间以新的覆盖旧--> <DefaultRolloverStrategy max="30"/> </RollingFile> <!-- 配置异步日志 --> <Async name="asyncAppender"> <!-- 引用在前面配置的appender --> <AppenderRef ref="fileAppender"/> </Async> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> <AppenderRef ref="rollingFile"/> <AppenderRef ref="asyncAppender"/> </Root> </Loggers></Configuration>
测试
@Test public void asyncAppenderTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2、AsyncLogger方式 - 必须会
- AsyncLogger才是log4j2实现异步最重要的功能体现,也是官方推荐的异步方式
- 它可以使得调用Logger.log返回的更快。你可以有两种选择:全局异步和混合异步
6.1.6.2.1、全局异步 - 了解
-
所有的日志都异步的记录,在配置文件上不用做任何改动,只需要在jvm启动的时候增加一个参数即可实现
-
设置方式
-
只需要在类路径resources下添加一个properties属性文件,做一步配置即可,**文件名要求必须是:
log4j2.component.properties**
,这个properties配置文件内容如下:-
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
-
-
操作一波
在resources
目录中新建log4j2.component.properties
文件,并配置如下内容:
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
log4j2.xml
文件配置( 不配置任何的async异步设置 )
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> <!-- 按照指定规则来拆分日志文件 fileName:日志文件的名字 filePattern:日志文件拆分后文件的命名规则 $${date:yyyy-MM-dd}:根据日期当天,创建一个文件夹 例如:2021-01-01这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) 2021-01-02这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) rollog-%d{yyyy-MM-dd-HH-mm}-%i.log 为文件命名的规则:%i表示序号,从0开始,目的是为了让每一份文件名字不会重复 --> <RollingFile name="rollingFile" fileName="${logPath}ollingLog.log" filePattern="${logPath}/$${date:yyyy-MM-dd}ollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log"> <!-- 日志消息格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> <Policies> <!-- 在系统启动时,触发拆分规则,产生一个日志文件 --> <OnStartupTriggeringPolicy/> <!-- 按照文件的大小进行拆分 注:这里虽然限制是10KB,但是真实生成出来的文件一般都是比这数值大1KB--> <SizeBasedTriggeringPolicy size="10KB"/> <!-- 按照时间节点进行拆分 拆分规则就是filePattern--> <TimeBasedTriggeringPolicy/> </Policies> <!-- 在同一目录下,文件的个数限制,如果超出了设置的数值,则根据时间以新的覆盖旧--> <DefaultRolloverStrategy max="30"/> </RollingFile> <!-- 配置异步日志 --> <!--<Async name="asyncAppender">--> <!-- <!– 引用在前面配置的appender –>--> <!-- <AppenderRef ref="fileAppender"/>--> <!--</Async>--> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> <AppenderRef ref="rollingFile"/> <!--<AppenderRef ref="asyncAppender"/>--> </Root> </Loggers></Configuration>
测试
@Test public void globalAsync() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2.2、混合异步 - 必须会
- 可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活
- 混合异步的方式需要通过修改配置文件来实现,使用
AsyncLogger
标签来配置
log4j2.xml配置
- 注意点:记得把全局异步的properties内容注释掉,全局异步和混合异步不能同时出现
- 编写方式:在
appender
标签中石油``AsyncLogger`标签,然后在这里面引用要异步输出的appender即可 - 下面配置的需求是:
cn.zixieqing
包下的日志输出到文件进行异步操作,而root logger
日志是同步操作
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> <!-- 按照指定规则来拆分日志文件 fileName:日志文件的名字 filePattern:日志文件拆分后文件的命名规则 $${date:yyyy-MM-dd}:根据日期当天,创建一个文件夹 例如:2021-01-01这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) 2021-01-02这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) rollog-%d{yyyy-MM-dd-HH-mm}-%i.log 为文件命名的规则:%i表示序号,从0开始,目的是为了让每一份文件名字不会重复 --> <RollingFile name="rollingFile" fileName="${logPath}ollingLog.log" filePattern="${logPath}/$${date:yyyy-MM-dd}ollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log"> <!-- 日志消息格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> <Policies> <!-- 在系统启动时,触发拆分规则,产生一个日志文件 --> <OnStartupTriggeringPolicy/> <!-- 按照文件的大小进行拆分 注:这里虽然限制是10KB,但是真实生成出来的文件一般都是比这数值大1KB--> <SizeBasedTriggeringPolicy size="10KB"/> <!-- 按照时间节点进行拆分 拆分规则就是filePattern--> <TimeBasedTriggeringPolicy/> </Policies> <!-- 在同一目录下,文件的个数限制,如果超出了设置的数值,则根据时间以新的覆盖旧--> <DefaultRolloverStrategy max="30"/> </RollingFile> <!-- 配置异步日志 --> <!--<Async name="asyncAppender">--> <!-- <!– 引用在前面配置的appender –>--> <!-- <AppenderRef ref="fileAppender"/>--> <!--</Async>--> <!-- 自定义logger,让自定义的logger为异步logger --> <!-- name 就是包路径,注意点:这个异步配置只限于这个name指定的包下的日志有异步操作,要是另有一个包cn.xiegongzi 那么:cn.xiegongzi包下的日志还是同步的,并不会异步 includeLocation="false" 表示去除日志记录中的行号信息,这个行号信息非常的影响日志记录的效率(生产中都不加这个行号) 严重的时候可能记录的比同步的日志效率还有低 additivity="false" 表示不继承rootlogger --> <AsyncLogger name="cn.zixieqing" level="debug" includeLocation="false" additivity="false"> <!-- 文件输出fileAppender,设置为异步打印 --> <AppenderRef ref="fileAppender"/> </AsyncLogger> </Appenders> <!--配置logger--> <!--现在这个root logger中的日志是同步的--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> <AppenderRef ref="rollingFile"/> <!--<AppenderRef ref="asyncAppender"/>--> </Root> </Loggers></Configuration>
测试
@Test public void blendAsync() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2.3、AsyncAppender、AsyncLogge两种方式的建议
- 如果使用异步日志,AsyncAppender、AsyncLogger不要同时出现,不会有这种奇葩需求,同时效果也不会叠加,如果同时出现,那么效率会以AsyncAppender为主
- 同样的,AsyncLogger的全局异步和混合异步也不要同时出现,也不会有这种奇葩,效果也不会叠加
7、后续的事情
- 下一篇为SpringBoot相关,链接地址:https://www.cnblogs.com/xiegongzi/p/16305432.html
-
这篇博客内容太多,加载量太大,这个垃圾博客,老衲真心怀疑:TMD,它是每次更新时在线更新博文全部内容,TNND,不会在点击确认时再收集全部数据啊,到了3000多行以上的博文内容之后,老衲只要一写内容,就发现在重新加载,然后我笔记本的风扇就开始快速转动,内存快速升到60%以上
-
然后就发现浏览器弹出艹了它先人坟的如下页面,害老衲又要重新把markdown中的内容重新复制过来,搞得老衲很想只弄我的知乎去了
Java中如何快捷的创建不可变集合
陈叔叔 发表了文章 • 0 个评论 • 62 次浏览 • 2022-05-24 15:42
在Java 9中又新增了一些API来帮助便捷的创建不可变集合,以减少代码复杂度。
本期配套视频:Java 9 新特性:快速定义不可变集合
常规写法
以往我们创建一些不可变集合的时候,通常是这样写的:
// 不可变的SetSet<String> set = new HashSet<>();set.add("a");set.add("b");set.add("c");set = Collections.unmodifiableSet(set);// 不可变的ListList<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list = Collections.unmodifiableList(list);
Java8的写法
在Java 8的时候,利用Stream API还能简化一下,可以演变成这样:
Set<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));List<Integer> list = Collections.unmodifiableList(Stream.of(1, 2, 3).collect(toList()));
Java9的写法
而现在到了Java 9,这一操作变的更为简单,只需要这样:
Set<String> set = Set.of("a", "b", "c");List<Integer> list = List.of(1, 2, 3);
同时,对于一下更复杂的集合也一样支持,比如Map类型也可以这样写:
Map<String, String> map = Map.of("a", "1", "b", "2", "c", "3");
就是要注意下,Map.of的参数是key和value成对出现的,所以参数数量一定是偶数:
Map.of()Map.of(k1, v1)Map.of(k1, v1, k2, v2)Map.of(k1, v1, k2, v2, k3, v3)...
与asList的区别
看到这里,可能有的人会问了,之前不是对于集合有asXxx这样的方便方法了么?他们有啥区别吗?
这里就以List.of
和Arrays.asList
为例,给大家列一下它们的异同:
-
Java 9中推出
List.of
创建的是不可变集合,而Arrays.asList
是可变集合 -
List.of
和Arrays.asList
都不允许add和remove元素,但Arrays.asList
可以调用set更改值,而List.of
不可以,会报java.lang.UnsupportedOperationException
异常 -
List.of
中不允许有null值,Arrays.asList
中可以有null值
好了,今天的分享就到这里!
如果您学习过程中如遇困难?可以加入我们超高质量的技术交流群,参与交流与讨论,更好的学习与进步!
本文收录在了我正在连载的《Java新特性专栏》,该系列该用电子书的方式编写,如果想要沉浸式阅读学习的话,可以访问Web版本:https://www.didispace.com/java-features/
欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源
画图带你彻底弄懂三级缓存和循环依赖的问题
陈叔叔 发表了文章 • 0 个评论 • 54 次浏览 • 2022-05-23 12:44

大家好。我们都知道,Spring可以通过三级缓存解决循环依赖的问题,这也是面试中很常见的一个面试题,本文就来着重讨论一下有关循环依赖和三级缓存的问题。
一、什么是循环依赖
大家平时在写业务的时候应该写过这样的代码。
其实这种类型就是循环依赖,就是AService 和BService两个类相互引用。
二、三级缓存可以解决的循环依赖场景
如上面所说,大家平时在写这种代码的时候,项目其实是可以起来的,也就是说其实三级缓存是可以解决这种循环依赖的。
当然除了这种字段注入,set注入也是可以解决的,代码如下。

接下来就来探究三级缓存是如何解决这种循环依赖的?
三、Spring的Bean是如何创建出来的
本文所说的Bean和对象可以理解为同一个意思。
先说如何解决循环依赖之前,先来了解一下一个Bean创建的大致流程。为什么要说Bean的创建过程,因为循环依赖主要是发生在Bean创建的过程中,知道Bean是如何创建的,才能更好的理解三级缓存的作用。
其实Spring Bean的生命周期源码剖析我也在微信公众号 三友的java日记 中发过,并且有简单的提到三级缓存,有兴趣的同学可以在关注公众号之后回复 Bean 即可获取文章链接,里面有Bean创建过程更详细的说明。这里我简单画一张图来说一下。

其实图里的每个阶段还可以分为一些小的阶段,我这里就没画出来了。
来说一下每个阶段干了什么事。
- BeanDefinition的读取阶段:我们在往Spring容器注入Bean的时候,一般会通过比如xml方式,@Bean注解的方式,@Component注解的方式,其实不论哪一种,容器启动的时候都会去解析这些配置,然后为每个Bean生成一个对应的BeanDefinition,这个BeanDefinition包含了这个Bean的创建的信息,Spring就是根据BeanDefinition去决定如何创建一个符合你要求的Bean
- Bean的实例化阶段:这个阶段主要是将你配置的Bean根据Class的类型创建一个对象出来
- Bean的属性赋值阶段:这个阶段主要是用来处理属性的赋值,比如@Autowired注解的生效就是在这个阶段的
- Bean的初始化阶段:这个阶段主要是回调一些方法,比如你的类实现了InitializingBean接口,那么就会回调afterPropertiesSet方法,同时动态代理其实也是在这个阶段完成的。
其实从这可以看出,一个Spring Bean的生成要分为很多的阶段,只有这些事都处理完了,这个Bean才是完完全全创建好的Bean,也就是我们可以使用的Bean。
四、三级缓存指的是哪三级缓存
这里直接上源码

第一级缓存:singletonObjects
存放已经完完全全创建好的Bean,什么叫完完全全创建好的?就是上面说的是,所有的步骤都处理完了,就是创建好的Bean。一个Bean在产的过程中是需要经历很多的步骤,在这些步骤中可能要处理@Autowired注解,又或是处理@Transcational注解,当需要处理的都处理完之后的Bean,就是完完全全创建好的Bean,这个Bean是可以用来使用的,我们平时在用的Bean其实就是创建好的。
第二级缓存:earlySingletonObjects
早期暴露出去的Bean,其实也就是解决循环依赖的Bean。早期的意思就是没有完完全全创建好,但是由于有循环依赖,就需要把这种Bean提前暴露出去。其实 早期暴露出去的Bean 跟 完完全全创建好的Bean 他们是同一个对象,只不过早期Bean里面的注解可能还没处理,完完全全的Bean已经处理了完了,但是他们指的还是同一个对象,只不过它们是在Bean创建过程中处于的不同状态,如果早期暴露出去的Bean跟完完全全创建好的Bean不是同一个对象是会报错的,项目也就起不来,这个不一样导致报错问题,这里我会结合一个案例再来写一篇文章,这里不用太care,就认为是一样的。
第三级缓存:singletonFactories
存的是每个Bean对应的ObjectFactory对象,通过调用这个对象的getObject方法,就可以获取到早期暴露出去的Bean。
注意:这里有个很重要的细节就是三级缓存只会对单例的Bean生效,像多例的是无法利用到三级缓存的,通过三级缓存所在的类名DefaultSingletonBeanRegistry就可以看出,仅仅是对SingletonBean也就是单例Bean有效果。

五、三级缓存在Bean生成的过程中是如何解决循环依赖的
这里我假设项目启动时先创建了AService的Bean,那么就会根据Spring Bean创建的过程来创建。
在Bean的实例化阶段,就会创建出AService的对象,此时里面的@Autowired注解是没有处理的,创建出AService的对象之后就会构建AService对应的一个ObjectFactory对象放到三级缓存中,通过这个ObjectFactory对象可以获取到AService的早期Bean。

然后AService继续往下走,到了某一个阶段,开始处理@Autowired注解,要注入BService对象,如图

要注入BService对象,肯定要去找BService对象,那么他就会从三级缓存中的第一级缓存开始依次查找有没有BService对应的Bean,肯定都没有啊,因为BService还没创建呢。没有该怎么办呢?其实很好办,没有就去创建一个么,这样不就有了么。于是AService的注入BService的过程就算暂停了,因为现在得去创建BService,创建之后才能注入给AService。
于是乎,BService就开始创建了,当然他也是Spring的Bean,所以也按照Bean的创建方式来创建,先实例化一个BService对象,然后缓存对应的一个ObjectFactory到第三级缓存中,然后就到了需要处理@Autowired注解的时候了,如图。

@Autowired注解需要注入AService对象。注入AService对象,就需要先去拿到AService对象,此时也会一次从三级缓存查有没有AService。
先从第一级查,有没有创建好的AService,肯定没有,因为AService此时正在在创建(因为AService在创建的过程中需要注入BService才去创建BService的,虽然此刻代码正在创建BService,但是AService也是在创建的过程中,只不过暂停了,只要BService创建完,AService会继续往下创建);第一级缓存没有,那么就去第二级看看,也没有,没有早期的AService;然后去第三级缓存看看有没有AService对应的ObjectFactory对象,惊天的发现,竟然有(上面提到过,创建出AService的对象之后,会构建AService对应的一个ObjectFactory对象放到三级缓存中),那么此时就会调用AService对应的ObjectFactory对象的getObject方法,拿到早期的AService对象,然后将早期的AService对象放到二级缓存,为什么需要放到二级缓存,主要是怕还有其他的循环依赖,如果还有的话,直接从二级缓存中就能拿到早期的AService对象。

虽然是早期的AService对象,但是我前面说过,仅仅只是早期的AService对象可能有些Bean创建的步骤还没完成,跟最后完完全全创建好的AService Bean是同一个对象。
于是接下来就把早期的AService对象注入给BService。

此时BService的@Autowired注解注入AService对象就完成了,之后再经过其他阶段的处理之后,BService对象就完完全全的创建完了。
BService对象创建完之后,就会将BService放入第一级缓存,然后清空BService对应的第三级缓存,当然也会去清空第二级缓存,只是没有而已,至于为什么清空,很简单,因为BService已经完全创建好了,如果需要BService那就在第一级缓存中就能查找到,不需要在从第二级或者第三级缓存中找到早期的BService对象。

BService对象就完完全全的创建完之后,那么接下来该干什么呢?此时当然继续创建AService对象了,你不要忘了为什么需要创建BService对象,因为AService对象需要注入一个BService对象,所以才去创建BService的,那么此时既然BService已经创建完了,那么是不是就应该注入给AService对象了?所以就会将BService注入给AService对象,这下就明白了,BService在构建的时候,已经注入了AService,虽然是早期的AService,但的确是AService对象,现在又把BService注入给了AService,那么是不是已经解决了循环依赖的问题了,AService和BService都各自注入了对方,如图。

然后AService就会跟BService一样,继续处理其它阶段的,完全创建好之后,也会清空二三级缓存,放入第一级缓存。

到这里,AService和BService就都创建好了,循环依赖也就解决了。
这下你应该明白了三级缓存的作用,主要是第二级和第三级用来存早期的对象,这样在有循环依赖的对象,就可以注入另一个对象的早期状态,从而达到解决循环依赖的问题,而早期状态的对象,在构建完成之后,也就会成为完完全全可用的对象。
六、三级缓存无法解决的循环依赖场景
1)构造器注入无法解决循环依赖
上面的例子是通过@Autowired注解直接注入依赖的对象,但是如果通过构造器注入循环依赖的对象,是无法解决的,如代码下

构造器注入就是指创建AService对象的时候,就传入BService对象,而不是用@Autowired注解注入BService对象。
运行结果

启动时就会报错,所以通过构造器注入对象就能避免产生循环依赖的问题,因为如果有循环依赖的话,那么就会报错。
至于三级缓存为什么不能解决构造器注入的问题呢?其实很好理解,因为上面说三级缓存解决循环依赖的时候主要讲到,在AService实例化之后,会创建对应的ObjectFactory放到第三级缓存,发生循环依赖的时候,可以通过ObjectFactory拿到早期的AService对象;而构造器注入,是发生在实例化的时候,此时还没有AService对象正在创建,还没完成,压根就还没执行到往第三级添加对应的ObjectFactory的步骤,那么BService在创建的时候,就无法通过三级缓存拿到早期的AService对象,拿不到怎么办,那就去创建AService对象,但是AService不是正在创建么,于是会报错。
2)注入多例的对象无法解决循环依赖


启动引导类

要获取AService对象,因为多例的Bean在容器启动的时候是不会去创建的,所以得去获取,这样就会创建了。
运行结果

为什么不能解决,上面在说三级缓存的时候已经说过了,三级缓存只能对单例Bean生效,那么多例是不会起作用的,并且在创建Bean的时候有这么一个判断,那就是如果出现循环依赖并且是依赖的是多例的Bean,那么直接抛异常,源码如下

注释其实说的很明白,推测出现了循环依赖,抛异常。
所以上面提到的两种循环依赖的场景,之所以无法通过三级缓存来解决,是因为压根这两种场景就无法使用三级缓存,所以三级缓存肯定解决不掉。
七、不用三级缓存,用二级缓存能不能解决循环依赖
遇到这种面试题,你就跟面试官说,如果行的话,Spring的作者为什么不这么写呢?
哈哈,开个玩笑,接下来说说到底为什么不行。
这里我先说一下前面没提到的细节,那就是通过ObjectFactory获取的Bean可能是两种类型,第一种就是实例化阶段创建出来的对象,还是一种就是实例化阶段创建出来的对象的代理对象。至于是不是代理对象,取决于你的配置,如果添加了事务注解又或是自定义aop切面,那就需要代理。这里你不用担心,如果这里获取的是代理对象,那么最后完全创建好的对象也是代理对象,ObjectFactory获取的对象和最终完全创建好的还是同一个,不是同一个肯定会报错,所以上面的理论依然符合,这里只是更加的细节化。
有了这个知识点之后,我们就来谈一下为什么要三级缓存。
第一级缓存,也就是缓存完全创建好的Bean的缓存,这个缓存肯定是需要的,因为单例的Bean只能创建一次,那么肯定需要第一级缓存存储这些对象,如果有需要,直接从第一级缓存返回。那么如果只能有二级缓存的话,就只能舍弃第二级或者第三级缓存。
假设舍弃第三级缓存
舍弃第三级缓存,也就是没有ObjectFactory,那么就需要往第二缓存放入早期的Bean,那么是放没有代理的Bean还是被代理的Bean呢?
1)如果直接往二级缓存添加没有被代理的Bean,那么可能注入给其它对象的Bean跟最后最后完全生成的Bean是不一样的,因为最后生成的是代理对象,这肯定是不允许的;
2)那么如果直接往二级缓存添加一个代理Bean呢?
- 假设没有循环依赖,提前暴露了代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错,
- 假设没有循环依赖,使用了ObjectFactory,那么就不会提前暴露了代理对象,到最后生成的对象是什么就是什么,就不会报错,
- 如果有循环依赖,不论怎样都会提前暴露代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错
通过上面分析,如果没有循环依赖,使用ObjectFactory,就减少了提前暴露代理对象的可能性,从而减少报错的可能。
假设舍弃第二级缓存
假设舍弃第二级缓存,也就是没有存放早期的Bean的缓存,其实肯定也不行。上面说过,ObjectFactory其实获取的对象可能是代理的对象,那么如果每次都通过ObjectFactory获取代理对象,那么每次都重新创建一个代理对象,这肯定也是不允许的。
从上面分析,知道为什么不能使用二级缓存了吧,第三级缓存就是为了避免过早地创建代理对象,从而避免没有循环依赖过早暴露代理对象产生的问题,而第二级缓存就是防止多次创建代理对象,导致对象不同。
本文完。
如果觉得这篇文章对你有所帮助,还请帮忙点赞、在看、转发给更多的人,码字不易,非常感谢!
欢迎关注公众号 三友的java日记,更多技术干货及时获得。
往期热门文章推荐
从零玩转人脸识别
陈叔叔 发表了文章 • 0 个评论 • 68 次浏览 • 2022-05-21 15:48
前言
在线demo
(前往享受人脸识别)
文章作者个人博客
(前往作者博客)
本期教程人脸识别第三方平台为虹软科技,本文章讲解的是人脸识别RGB活体追踪技术,免费的功能很多可以自行搭配,希望在你看完本章课程有所收获。
ArcFace 离线SDK,包含人脸检测、性别检测、年龄检测、人脸识别、图像质量检测、RGB活体检测、IR活体检测等能力,初次使用时需联网激活,激活后即可在本地无网络环境下工作,可根据具体的业务需求结合人脸识别SDK灵活地进行应用层开发。
功能介绍
1. 人脸检测
对传入的图像数据进行人脸检测,返回人脸的边框以及朝向信息,可用于后续的人脸识别、特征提取、活体检测等操作;
- 支持IMAGE模式和VIDEO模式人脸检测。
- 支持单人脸、多人脸检测,最多支持检测人脸数为50。
2.人脸追踪
对来自于视频流中的图像数据,进行人脸检测,并对检测到的人脸进行持续跟踪。(我们是实时的所以就只能使用第三方操作,先不使用这个)
3.人脸特征提取
提取人脸特征信息,用于人脸的特征比对。
4.人脸属性检测
人脸属性,支持检测年龄、性别以及3D角度。
人脸3D角度:俯仰角(pitch), 横滚角(roll), 偏航角(yaw)。
5.活体检测
离线活体检测,静默式识别,在人脸识别过程中判断操作用户是否为真人,有效防御照片、视频、纸张等不同类型的作弊攻击,提高业务安全性,让人脸识别更安全、更快捷,体验更佳。支持单目RGB活体检测、双目(IR/RGB)活体检测,可满足各类人脸识别终端产品活体检测应用。
开造
访问地址: https://ai.arcsoft.com.cn/technology/faceTracking.html
进入开发者中心进行注册以及认证个人信息
1. 点击我的应用 > 新建应用
2.填写信息立即创建 点击 添加SDK
3.选中免费版人脸识别
4. 填写授权码信息
选择平台先选择windows的根据你的电脑配置来 是64位还是32位的, 语言选择Java
5. 介绍sdk文件
构建项目工程
导入项目依赖
<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.7.RELEASE</spring-boot.version> <druid-spring-boot-starter.version>1.2.6</druid-spring-boot-starter.version> <mybatis-spring-boot.version>2.1.4</mybatis-spring-boot.version> <pagehelper.boot.version>1.3.0</pagehelper.boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 人脸识别 --> <dependency> <groupId>com.arcsoft.face</groupId> <artifactId>arcsoft-sdk-face</artifactId> <scope>system</scope> <systemPath>${project.basedir}/lib/arcsoft-sdk-face-3.0.0.0.jar</systemPath> <version>3.0.0.0</version> </dependency> <!-- pool 对象池 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.0.1-jre</version> </dependency> <!-- Mysql驱动包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- mybatis-plus 增强CRUD --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!-- pagehelper 分页插件 内置mybatis 依赖--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pagehelper.boot.version}</version> </dependency> <!-- 阿里数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid-spring-boot-starter.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <!-- Spring框架基本的核心工具 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <!-- JSON工具类 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.0</version> </dependency> <!-- SpringWeb模块 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency> <!-- servlet包 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency> <!-- aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.7.RELEASE</version> <configuration> <mainClass>top.yangbuyi.YangbuyiFaceDemoApplication</mainClass> <!-- 文件要配置includeSystemScope属性,否则可能会导致arcsoft-sdk-face-3.0.0.0.jar获取不到 --> <includeSystemScope>true</includeSystemScope> <fork>true</fork> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
使用代码生成器生成CURD
版本请对应图片当中的jebat全家桶群里获取3秒破解使用
生成完毕后在根目录创建lib目录将下载下来的人脸识别依赖导入->右击添加到库
application.yml 修改配置文件
# 开发环境配置server: # 服务器的HTTP端口,默认为8080 port: 8080 servlet: # 应用的访问路径 context-path: / tomcat: # tomcat的URI编码 uri-encoding: UTF-8 # tomcat最大线程数,默认为200 max-threads: 800 # Tomcat启动初始化的线程数,默认值25 min-spare-threads: 30# Spring配置spring: # 同时执行其它配置文件 profiles: active: druid mvc: # 把前端的接收到的时间格式 格式化为 yyyy-MM-dd HH:mm:ss date-format: yyyy-MM-dd HH:mm:ss jackson: # 把后台的时间格式 格式化为 yyyy-MM-dd HH:mm:ss date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 # 服务模块 devtools: restart: # 热部署开关 enabled: true# MyBatis配置mybatis-plus: # 搜索指定包别名 typeAliasesPackage: top.yangbuyi.domain # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapperLocations: classpath*:mapper/*Mapper.xml # 加载全局的配置文件 configLocation: classpath:mybatis/mybatis-config.xml# PageHelper分页插件pagehelper: helperDialect: mysql reasonable: true supportMethodsArguments: true params: count=countSql# 人脸识别配置# WIND64config: sdk-lib-path: M:yangbuyiya-RBAClibsWIN64 app-id: 4QKtmacvsKqaCsoXyyujcs21JTAr79pTczPdZpuaEjhH sdk-key: EgBjrmidnqstaL46msfHukeKanYXCujzeHokf2qcC3br thread-pool-size: 5
application-druid.yml 数据源配置
# 数据源配置spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: url: jdbc:mysql://127.0.0.1:3308/face?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /yangbuyi/druid/* filter: stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
resources 下创建mybatis文件夹创建mybatis-config.xml文件
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <settings> <setting name="cacheEnabled" value="true"/> <!-- 全局映射器启用缓存 --> <setting name="useGeneratedKeys" value="true"/> <!-- 允许 JDBC 支持自动生成主键 --> <setting name="defaultExecutorType" value="REUSE"/> <!-- 配置默认的执行器 --> <setting name="logImpl" value="SLF4J"/> <!-- 指定 MyBatis 所用日志的具体实现 --> <!-- <setting name="mapUnderscoreToCamelCase" value="true"/> 驼峰式命名 --> </settings></configuration>
项目基础文件配置
创建config文件
创建ApplicationConfig全局配置类
/** * 程序注解配置 * * @author yangbuyi */@Configuration// 表示通过aop框架暴露该代理对象,AopContext能够访问@EnableAspectJAutoProxy(exposeProxy = true)// 指定要扫描的Mapper类的包的路径@MapperScan("top.yangbuyi.mapper")public class ApplicationConfig { /** * 时区配置 */ @Bean public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization () { return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); } /** * 处理Long类型精度丢失 * * @return */ @Bean("jackson2ObjectMapperBuilderCustomizer") public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer () { return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); }}
创建全局MybatisPlusConfig配置
/** * @program: yangbuyi-rbac * @ClassName: MybatisPlusConfig * @create: 2022-04-25 15:48 * @author: yangbuyi.top * @since: JDK1.8 * @MybatisPlusConfig: $ **/import com.baomidou.mybatisplus.annotation.DbType;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.transaction.annotation.EnableTransactionManagement;@EnableTransactionManagement(proxyTargetClass = true)@Configurationpublic class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(paginationInnerInterceptor()); // 乐观锁插件 interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor()); // 阻断插件 interceptor.addInnerInterceptor(blockAttackInnerInterceptor()); return interceptor; } /** * 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html */ public PaginationInnerInterceptor paginationInnerInterceptor () { PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); // 设置数据库类型为mysql paginationInnerInterceptor.setDbType(DbType.MYSQL); // 设置最大单页限制数量,默认 500 条,-1 不受限制 paginationInnerInterceptor.setMaxLimit(-1L); return paginationInnerInterceptor; } /** * 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html */ public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor () { return new OptimisticLockerInnerInterceptor(); } /** * 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html */ public BlockAttackInnerInterceptor blockAttackInnerInterceptor () { return new BlockAttackInnerInterceptor(); }}
创建dto文件夹
创建人脸返回实体 FaceSearchResDto
/** * @author yangbuyi.top */@Datapublic class FaceSearchResDto { /** * 唯一人脸Id */ private String faceId; /** * 人脸名称 */ private String name; private Integer similarValue; /** * 年龄 */ private Integer age; /** * 性别 */ private String gender; /** * 图片 */ private String image;}
创建人脸映射 FaceUserInfo
/** * @author yangbuyi.top */@Datapublic class FaceUserInfo { private int id; private int groupId; private String faceId; private String name; private Integer similarValue; private byte[] faceFeature;}
创建年龄映射
/** * @author yangbuyi.top */public class ProcessInfo { private Integer age; private Integer gender; public Integer getAge () { return age; } public void setAge (Integer age) { this.age = age; } public Integer getGender () { return gender; } public void setGender (Integer gender) { this.gender = gender; }}
创建enums文件夹
创建ErrorCodeEnum枚举类
/** * @author yangbuyi.top */public enum ErrorCodeEnum { MOK(0, "成功"), UNKNOWN(1, "未知错误"), INVALID_PARAM(2, "无效参数"), UNSUPPORTED(3, "引擎不支持"), NO_MEMORY(4, "内存不足"), BAD_STATE(5, "状态错误"), USER_CANCEL(6, "用户取消相关操作"), EXPIRED(7, "操作时间过期"), USER_PAUSE(8, "用户暂停操作"), BUFFER_OVERFLOW(9, "缓冲上溢"), BUFFER_UNDERFLOW(10, "缓冲下溢"), NO_DISKSPACE(11, "存贮空间不足"), COMPONENT_NOT_EXIST(12, "组件不存在"), GLOBAL_DATA_NOT_EXIST(13, "全局数据不存在"), NO_FACE_DETECTED(14, "未检出到人脸"), FACE_DOES_NOT_MATCH(15, "人脸不匹配"), INVALID_APP_ID(28673, "无效的AppId"), INVALID_SDK_ID(28674, "无效的SdkKey"), INVALID_ID_PAIR(28675, "AppId和SdkKey不匹配"), MISMATCH_ID_AND_SDK(28676, "SdkKey 和使用的SDK 不匹配"), SYSTEM_VERSION_UNSUPPORTED(28677, "系统版本不被当前SDK所支持"), LICENCE_EXPIRED(28678, "SDK有效期过期,需要重新下载更新"), APS_ENGINE_HANDLE(69633, "引擎句柄非法"), APS_MEMMGR_HANDLE(69634, "内存句柄非法"), APS_DEVICEID_INVALID(69635, " Device ID 非法"), APS_DEVICEID_UNSUPPORTED(69636, "Device ID 不支持"), APS_MODEL_HANDLE(69637, "模板数据指针非法"), APS_MODEL_SIZE(69638, "模板数据长度非法"), APS_IMAGE_HANDLE(69639, "图像结构体指针非法"), APS_IMAGE_FORMAT_UNSUPPORTED(69640, "图像格式不支持"), APS_IMAGE_PARAM(69641, "图像参数非法"), APS_IMAGE_SIZE(69642, "图像尺寸大小超过支持范围"), APS_DEVICE_AVX2_UNSUPPORTED(69643, "处理器不支持AVX2指令"), FR_INVALID_MEMORY_INFO(73729, "无效的输入内存"), FR_INVALID_IMAGE_INFO(73730, "无效的输入图像参数"), FR_INVALID_FACE_INFO(73731, "无效的脸部信息"), FR_NO_GPU_AVAILABLE(73732, "当前设备无GPU可用"), FR_MISMATCHED_FEATURE_LEVEL(73733, "待比较的两个人脸特征的版本不一致"), FACEFEATURE_UNKNOWN(81921, "人脸特征检测错误未知"), FACEFEATURE_MEMORY(81922, "人脸特征检测内存错误"), FACEFEATURE_INVALID_FORMAT(81923, "人脸特征检测格式错误"), FACEFEATURE_INVALID_PARAM(81924, "人脸特征检测参数错误"), FACEFEATURE_LOW_CONFIDENCE_LEVEL(81925, "人脸特征检测结果置信度低"), ASF_EX_BASE_FEATURE_UNSUPPORTED_ON_INIT(86017, "Engine不支持的检测属性"), ASF_EX_BASE_FEATURE_UNINITED(86018, "需要检测的属性未初始化"), ASF_EX_BASE_FEATURE_UNPROCESSED(86019, "待获取的属性未在process中处理过"), ASF_EX_BASE_FEATURE_UNSUPPORTED_ON_PROCESS(86020, "PROCESS不支持的检测属性,例如FR,有自己独立的处理函数"), ASF_EX_BASE_INVALID_IMAGE_INFO(86021, "无效的输入图像"), ASF_EX_BASE_INVALID_FACE_INFO(86022, "无效的脸部信息"), ASF_BASE_ACTIVATION_FAIL(90113, "人脸比对SDK激活失败,请打开读写权限"), ASF_BASE_ALREADY_ACTIVATED(90114, "人脸比对SDK已激活"), ASF_BASE_NOT_ACTIVATED(90115, "人脸比对SDK未激活"), ASF_BASE_SCALE_NOT_SUPPORT(90116, "detectFaceScaleVal 不支持"), ASF_BASE_VERION_MISMATCH(90117, "SDK版本不匹配"), ASF_BASE_DEVICE_MISMATCH(90118, "设备不匹配"), ASF_BASE_UNIQUE_IDENTIFIER_MISMATCH(90119, "唯一标识不匹配"), ASF_BASE_PARAM_NULL(90120, "参数为空"), ASF_BASE_SDK_EXPIRED(90121, "SDK已过期"), ASF_BASE_VERSION_NOT_SUPPORT(90122, "版本不支持"), ASF_BASE_SIGN_ERROR(90123, "签名错误"), ASF_BASE_DATABASE_ERROR(90124, "数据库插入错误"), ASF_BASE_UNIQUE_CHECKOUT_FAIL(90125, "唯一标识符校验失败"), ASF_BASE_COLOR_SPACE_NOT_SUPPORT(90126, "输入的颜色空间不支持"), ASF_BASE_IMAGE_WIDTH_NOT_SUPPORT(90127, "输入图像的byte数据长度不正确"), ASF_NETWORK_BASE_COULDNT_RESOLVE_HOST(94209, "无法解析主机地址"), ASF_NETWORK_BASE_COULDNT_CONNECT_SERVER(94210, "无法连接服务器"), ASF_NETWORK_BASE_CONNECT_TIMEOUT(94211, "网络连接超时"), ASF_NETWORK_BASE_UNKNOWN_ERROR(94212, "未知错误"); private Integer code; private String description; ErrorCodeEnum (Integer code, String description) { this.code = code; this.description = description; } public Integer getCode () { return code; } public void setCode (Integer code) { this.code = code; } public String getDescription () { return description; } public void setDescription (String description) { this.description = description; } public static ErrorCodeEnum getDescriptionByCode (Integer code) { for (ErrorCodeEnum errorCodeEnum : ErrorCodeEnum.values()) { if (code.equals(errorCodeEnum.getCode())) { return errorCodeEnum; } } return ErrorCodeEnum.UNKNOWN; }}
创建utils文件夹
创建Base64DecodeMultipartFile
package top.yangbuyi.utils;import org.springframework.web.multipart.MultipartFile;import java.io.*;/** * @author yangbuyi.top * @program: yangbuyi-rbac * @ClassName: Base64DecodeMultipartFile * @create: 2022-04-25 16:25 * @since: JDK1.8 * @Base64DecodeMultipartFile: $ **/public class Base64DecodeMultipartFile implements MultipartFile { private final byte[] imgContent; private final String header; public Base64DecodeMultipartFile (byte[] imgContent, String header) { this.imgContent = imgContent; this.header = header.split(";")[0]; } @Override public String getName () { return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1]; } @Override public String getOriginalFilename () { return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1]; } @Override public String getContentType () { return header.split(":")[1]; } @Override public boolean isEmpty () { return imgContent == null || imgContent.length == 0; } @Override public long getSize () { return imgContent.length; } @Override public byte[] getBytes () throws IOException { return imgContent; } @Override public InputStream getInputStream () throws IOException { return new ByteArrayInputStream(imgContent); } @Override public void transferTo (File dest) throws IOException, IllegalStateException { new FileOutputStream(dest).write(imgContent); }}
创建ImageUtils
package top.yangbuyi.utils;import org.apache.tomcat.util.codec.binary.Base64;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.multipart.MultipartFile;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.InputStream;import java.net.URL;import java.net.URLConnection;import java.util.Arrays;/** * 图片处理工具类 * * @author yangbuyi.top */public class ImageUtils { private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); public static MultipartFile base64ToMultipartFile (String base64) { //base64编码后的图片有头信息所以要分离出来 [0]data:image/png;base64, 图片内容为索引[1] String[] baseStrs = base64.split(","); //取索引为1的元素进行处理 byte[] b = Base64.decodeBase64(baseStrs[1]); for (int i = 0; i < b.length; ++i) { if (b[i] < 0) { b[i] += 256; } } //处理过后的数据通过Base64DecodeMultipartFile转换为MultipartFile对象 return new Base64DecodeMultipartFile(b, baseStrs[0]); }}
项目基本需要的配置与工具已经创建完毕接下来我们开始人脸识别业务编写
实现BasePooledObjectFactory自定义引擎工厂
创建factory文件夹->创建FaceEngineFactory类
import com.arcsoft.face.EngineConfiguration;import com.arcsoft.face.FaceEngine;import com.arcsoft.face.enums.DetectMode;import com.arcsoft.face.enums.DetectOrient;import lombok.extern.slf4j.Slf4j;import org.apache.commons.pool2.BasePooledObjectFactory;import org.apache.commons.pool2.PooledObject;import org.apache.commons.pool2.impl.DefaultPooledObject;/** * 引擎工厂 * @author yangbuyi.top */@Slf4jpublic class FaceEngineFactory extends BasePooledObjectFactory<FaceEngine> { private final String appId; private final String sdkKey; private final String sdkLibPath; private final EngineConfiguration engineConfiguration; private final Integer detectFaceMaxNum = 10; private final Integer detectFaceScaleVal = 16; private final DetectMode detectMode = DetectMode.ASF_DETECT_MODE_IMAGE; private final DetectMode detectVideo = DetectMode.ASF_DETECT_MODE_VIDEO; private final DetectOrient detectFaceOrientPriority = DetectOrient.ASF_OP_0_ONLY; public FaceEngineFactory (String sdkLibPath, String appId, String sdkKey, EngineConfiguration engineConfiguration) { this.sdkLibPath = sdkLibPath; this.appId = appId; this.sdkKey = sdkKey; this.engineConfiguration = engineConfiguration; } @Override public FaceEngine create () throws Exception { FaceEngine faceEngine = new FaceEngine(sdkLibPath); // 用于在线激活SDK--- int activeCode = faceEngine.activeOnline(appId, sdkKey); log.info("在线激活SDK完毕!,{}", activeCode); int initCode = faceEngine.init(engineConfiguration); log.info("初始化功能引擎完毕!,{}", initCode); return faceEngine; } @Override public PooledObject<FaceEngine> wrap (FaceEngine faceEngine) { return new DefaultPooledObject<>(faceEngine); } @Override public void destroyObject (PooledObject<FaceEngine> p) throws Exception { FaceEngine faceEngine = p.getObject(); int unInitCode = faceEngine.unInit(); super.destroyObject(p); log.info("销毁对象完毕! faceEngineUnInitCode: {}", unInitCode); }}
一、编写人脸识别业务接口
在service目录下创建-> FaceEngineService
package top.yangbuyi.service;import com.arcsoft.face.FaceInfo;import com.arcsoft.face.toolkit.ImageInfo;import top.yangbuyi.dto.FaceUserInfo;import top.yangbuyi.dto.ProcessInfo;import java.util.List;import java.util.concurrent.ExecutionException;/** * 人脸识别接口 */public interface FaceEngineService { /** * 人脸检测 * @param imageInfo * @return */ List<FaceInfo> detectFaces (ImageInfo imageInfo); /** * 提取年龄-性别 * @param imageInfo * @return */ List<ProcessInfo> process (ImageInfo imageInfo); /** * 人脸特征 * @param imageInfo * @return */ byte[] extractFaceFeature (ImageInfo imageInfo) throws InterruptedException; /** * 人脸比对 * @param groupId * @param faceFeature * @return */ List<FaceUserInfo> compareFaceFeature (byte[] faceFeature, Integer groupId) throws InterruptedException, ExecutionException;}
service.impl 包下创建 FaceEngineServiceImpl 实现类
import cn.hutool.core.collection.CollectionUtil;import com.arcsoft.face.*;import com.arcsoft.face.enums.DetectMode;import com.arcsoft.face.enums.DetectOrient;import com.arcsoft.face.toolkit.ImageInfo;import com.google.common.collect.Lists;import org.apache.commons.pool2.impl.GenericObjectPool;import org.apache.commons.pool2.impl.GenericObjectPoolConfig;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import top.yangbuyi.dto.FaceUserInfo;import top.yangbuyi.dto.ProcessInfo;import top.yangbuyi.factory.FaceEngineFactory;import top.yangbuyi.mapper.SysUserFaceInfoMapper;import top.yangbuyi.service.FaceEngineService;import javax.annotation.PostConstruct;import java.math.BigDecimal;import java.util.ArrayList;import java.util.List;import java.util.concurrent.*;/** * @author yangbuyi.top */@Servicepublic class FaceEngineServiceImpl implements FaceEngineService { public final static Logger logger = LoggerFactory.getLogger(FaceEngineServiceImpl.class); @Value("${config.sdk-lib-path}") public String sdkLibPath; @Value("${config.app-id}") public String appId; @Value("${config.sdk-key}") public String sdkKey; @Value("${config.thread-pool-size}") public Integer threadPoolSize; /** * 人脸识别引擎 */ private static FaceEngine faceEngine; /** * 相似度 */ private final Integer passRate = 95; private ExecutorService executorService; @Autowired private SysUserFaceInfoMapper userFaceInfoMapper; /** * 线程池 */ private GenericObjectPool<FaceEngine> faceEngineObjectPool; /** * 项目启动时初始化线程池与人脸识别引擎配置 */ @PostConstruct public void init () { executorService = Executors.newFixedThreadPool(threadPoolSize); GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); poolConfig.setMaxIdle(threadPoolSize); poolConfig.setMaxTotal(threadPoolSize); poolConfig.setMinIdle(threadPoolSize); poolConfig.setLifo(false); //引擎配置 EngineConfiguration engineConfiguration = new EngineConfiguration(); engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE); engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY); //功能配置 对应的功能请查看文档 FunctionConfiguration functionConfiguration = new FunctionConfiguration(); functionConfiguration.setSupportAge(true); functionConfiguration.setSupportFace3dAngle(true); functionConfiguration.setSupportFaceDetect(true); functionConfiguration.setSupportFaceRecognition(true); functionConfiguration.setSupportGender(true); functionConfiguration.setSupportLiveness(true); functionConfiguration.setSupportIRLiveness(true); engineConfiguration.setFunctionConfiguration(functionConfiguration); // 底层库算法对象池 faceEngineObjectPool = new GenericObjectPool(new FaceEngineFactory(sdkLibPath, appId, sdkKey, engineConfiguration), poolConfig); try { faceEngine = faceEngineObjectPool.borrowObject(); } catch (Exception e) { e.printStackTrace(); } } /** * 确保精度 * @param value * @return */ private int plusHundred (Float value) { BigDecimal target = new BigDecimal(value); BigDecimal hundred = new BigDecimal(100f); return target.multiply(hundred).intValue(); }}
人脸识别逻辑
1、必须进行人脸特征获取 -> 特征获取成功 -> 进入人脸对比 -> 人脸检测 -> 返回人脸数据
2、必须进行人脸特征获取 -> 特征获取失败 -> 直接跳出返回 未检出到人脸
编写人脸特征获取逻辑
/** * 人脸特征 * * @param imageInfo * @return */@Overridepublic byte[]extractFaceFeature(ImageInfo imageInfo)throws InterruptedException{ FaceEngine faceEngine=null; try{ //获取引擎对象 faceEngine=faceEngineObjectPool.borrowObject(); //人脸检测得到人脸列表 List<FaceInfo> faceInfoList=new ArrayList<FaceInfo>(); //人脸检测 int i=faceEngine.detectFaces(imageInfo.getImageData(),imageInfo.getWidth(),imageInfo.getHeight(),imageInfo.getImageFormat(),faceInfoList); if(CollectionUtil.isNotEmpty(faceInfoList)){ FaceFeature faceFeature=new FaceFeature(); //提取人脸特征 faceEngine.extractFaceFeature(imageInfo.getImageData(),imageInfo.getWidth(),imageInfo.getHeight(),imageInfo.getImageFormat(),faceInfoList.get(0),faceFeature); return faceFeature.getFeatureData(); } }catch(Exception e){ logger.error("",e); }finally{ if(faceEngine!=null){ //释放引擎对象 faceEngineObjectPool.returnObject(faceEngine); } } return null; }
编写人脸对比逻辑
/** * 人脸比对 * @param groupId * @param faceFeature * @return 人脸组 */@Overridepublic List<FaceUserInfo> compareFaceFeature(byte[]faceFeature,Integer groupId)throws InterruptedException,ExecutionException{ // 识别到的人脸列表 List<FaceUserInfo> resultFaceInfoList=Lists.newLinkedList(); // 创建人脸特征对象 FaceFeature targetFaceFeature=new FaceFeature(); targetFaceFeature.setFeatureData(faceFeature); // 根据分组拿人脸库,从数据库中取出人脸库 List<FaceUserInfo> faceInfoList=userFaceInfoMapper.getUserFaceInfoByGroupId(groupId); // 分成50一组,多线程处理, 数据量大1000组 List<List<FaceUserInfo>>faceUserInfoPartList=Lists.partition(faceInfoList,50); // 多线程 CompletionService<List<FaceUserInfo>>completionService=new ExecutorCompletionService(executorService); for(List<FaceUserInfo> part:faceUserInfoPartList){ // 开始线程扫描人脸匹配度 completionService.submit(new CompareFaceTask(part,targetFaceFeature)); } // 获取线程任务参数 for(List<FaceUserInfo> faceUserInfos:faceUserInfoPartList){ List<FaceUserInfo> faceUserInfoList=completionService.take().get(); if(CollectionUtil.isNotEmpty(faceInfoList)){ resultFaceInfoList.addAll(faceUserInfoList); } } // 从大到小排序 resultFaceInfoList.sort((h1,h2)->h2.getSimilarValue().compareTo(h1.getSimilarValue())); return resultFaceInfoList; }/** * 多线程跑人脸对比逻辑 */private class CompareFaceTask implements Callable<List<FaceUserInfo>> { private final List<FaceUserInfo> faceUserInfoList; private final FaceFeature targetFaceFeature; public CompareFaceTask (List<FaceUserInfo> faceUserInfoList, FaceFeature targetFaceFeature) { this.faceUserInfoList = faceUserInfoList; this.targetFaceFeature = targetFaceFeature; } @Override public List<FaceUserInfo> call () throws Exception { FaceEngine faceEngine = null; //识别到的人脸列表 List<FaceUserInfo> resultFaceInfoList = Lists.newLinkedList(); try { faceEngine = faceEngineObjectPool.borrowObject(); for (FaceUserInfo faceUserInfo : faceUserInfoList) { FaceFeature sourceFaceFeature = new FaceFeature(); // 设置人脸特征 sourceFaceFeature.setFeatureData(faceUserInfo.getFaceFeature()); FaceSimilar faceSimilar = new FaceSimilar(); faceEngine.compareFaceFeature(targetFaceFeature, sourceFaceFeature, faceSimilar); //获取相似值 Integer similarValue = plusHundred(faceSimilar.getScore()); //相似值大于配置预期,加入到识别到人脸的列表 if (similarValue > passRate) { FaceUserInfo info = new FaceUserInfo(); info.setName(faceUserInfo.getName()); info.setFaceId(faceUserInfo.getFaceId()); info.setSimilarValue(similarValue); resultFaceInfoList.add(info); } } } catch (Exception e) { logger.error("", e); } finally { if (faceEngine != null) { faceEngineObjectPool.returnObject(faceEngine); } } return resultFaceInfoList; }}
编写根据根据分组ID获取人脸库数据
在mapper下SysUserFaceInfoMapper -> 创建接口 getUserFaceInfoByGroupId
/** * 根据分组Id * 从数据库中取出人脸库 * * @param groupId * @return 人脸库 */ List<FaceUserInfo> getUserFaceInfoByGroupId(Integer groupId);
sql实现
<resultMap id="userFace2" type="top.yangbuyi.dto.FaceUserInfo"><id column="id" property="id" javaType="int"/><result column="group_id" property="groupId" javaType="java.lang.Integer"/><result column="name" property="name" javaType="java.lang.String"/><result column="face_id" property="faceId" javaType="String"/><result column="face_feature" property="faceFeature"/><esultMap><select id = "getUserFaceInfoByGroupId" resultMap="userFace2" parameterType="java.lang.Integer" resultType="top.yangbuyi.dto.FaceUserInfo">select id, group_id, face_id, name, face_featurefrom `sys_user_face_info`where group_id = #{groupId} < /select>
二、编写人脸添加业务接口
userFaceInfoService 新增人脸业务接口
/** * 新增用户人脸识别 * * @param sysUserFaceInfo 用户人脸识别 * @return 结果 */public int insertSysUserFaceInfo(SysUserFaceInfo sysUserFaceInfo);
就是一共简简单单的新增插入数据 这个就不用我教了吧?????????
三、 编写Controller业务控制层
import top.yangbuyi.domain.AjaxResult;/** * (sys_user_face_info)表控制层 * * @author yangbuyi.top */@RestController@Slf4j@RequestMapping("face")@RequiredArgsConstructorpublic class SysUserFaceInfoController { public final static Logger logger = LoggerFactory.getLogger(SysUserFaceInfoController.class); private final FaceEngineService faceEngineService; private final SysUserFaceInfoService userFaceInfoService; /** * 人脸添加 * * @param file 人脸附件 * @param groupId 分组id * @param name 用户登录名称 */ @RequestMapping(value = "/faceAdd", method = RequestMethod.POST) public AjaxResult faceAdd (@RequestBody Map<String, Object> map) { // 业务.... yangbuyi.top 版权所有 return AjaxResult.success(); } /** * 人脸识别 * * @param file 人脸附件 * @param groupId 分组ID 方便快速识别 */ @RequestMapping(value = "/faceSearch", method = RequestMethod.POST) public AjaxResult faceSearch (@RequestBody Map<String, Object> map) throws Exception { // 业务... yangbuyi.top 版权所有 return AjaxResult.success(); }
faceAdd 具体业务编写
- 根据前端传递的
file -> 人脸图片
groupId -> 用户分组(用于缩小范围查询该人员的位置有效减少数据量大的问题)
name -> 当前用户名称(实际开发当中应该是存储id)
String file = String.valueOf(map.get("file")); String groupId = String.valueOf(map.get("groupId")); String name = String.valueOf(map.get("name")); try { if (file == null) { return AjaxResult.error("file is null"); } if (groupId == null) { return AjaxResult.error("file is null"); } if (name == null) { return AjaxResult.error("file is null"); } // 转换实体 byte[] decode = Base64.decode(base64Process(file)); ImageInfo imageInfo = ImageFactory.getRGBData(decode); //人脸特征获取 byte[] bytes = faceEngineService.extractFaceFeature(imageInfo); if (bytes == null) { return AjaxResult.error(ErrorCodeEnum.NO_FACE_DETECTED.getDescription()); } List<ProcessInfo> processInfoList = faceEngineService.process(imageInfo); // 开始将人脸识别Base64转换文件流->用于文件上传 // final MultipartFile multipartFile = ImageUtils.base64ToMultipartFile(file); final SysUserFaceInfo one = this.userFaceInfoService.lambdaQuery().eq(SysUserFaceInfo::getName, name).one(); // 如果存在则更新人脸和特征 if (null != one) { this.userFaceInfoService.lambdaUpdate().set(SysUserFaceInfo::getFaceFeature, bytes) .set(SysUserFaceInfo::getFpath, "存储头像地址") .eq(SysUserFaceInfo::getFaceId, name).update(); } else { // 组装人脸实体 SysUserFaceInfo userFaceInfo = new SysUserFaceInfo(); userFaceInfo.setName(name); userFaceInfo.setAge(processInfoList.get(0).getAge()); userFaceInfo.setGender(processInfoList.get(0).getGender().shortValue()); userFaceInfo.setGroupId(Integer.valueOf(groupId)); userFaceInfo.setFaceFeature(bytes); userFaceInfo.setFpath("存储头像地址"); // 存储用户ID -> 我这里先使用name代替 -> 假如是唯一性 userFaceInfo.setFaceId(name); //人脸特征插入到数据库 userFaceInfoService.insertSysUserFaceInfo(userFaceInfo); } logger.info("faceAdd:" + name); return AjaxResult.success("人脸绑定成功!"); } catch (Exception e) { logger.error("", e); } // 错误返回 return AjaxResult.error(ErrorCodeEnum.UNKNOWN.getDescription());
faceSearch 具体业务编写
- 根据前端传递的
file -> 人脸图片
groupId -> 用户分组(用于缩小范围查询该人员的位置有效减少数据量大的问题)
String file = String.valueOf(map.get("file")); String groupId = String.valueOf(map.get("groupId")); if (groupId == null) { return AjaxResult.error("groupId is null"); } byte[] decode = Base64.decode(base64Process(file)); BufferedImage bufImage = ImageIO.read(new ByteArrayInputStream(decode)); ImageInfo imageInfo = ImageFactory.bufferedImage2ImageInfo(bufImage); //人脸特征获取 byte[] bytes = faceEngineService.extractFaceFeature(imageInfo); // 校验是否显示出人脸 if (bytes == null) { return AjaxResult.error(ErrorCodeEnum.NO_FACE_DETECTED.getDescription()); } //人脸比对,获取比对结果 List<FaceUserInfo> userFaceInfoList = faceEngineService.compareFaceFeature(bytes, Integer.valueOf(groupId)); if (CollectionUtil.isNotEmpty(userFaceInfoList)) { FaceUserInfo faceUserInfo = userFaceInfoList.get(0); FaceSearchResDto faceSearchResDto = new FaceSearchResDto(); BeanUtil.copyProperties(faceUserInfo, faceSearchResDto); List<ProcessInfo> processInfoList = faceEngineService.process(imageInfo); if (CollectionUtil.isNotEmpty(processInfoList)) { //人脸检测 List<FaceInfo> faceInfoList = faceEngineService.detectFaces(imageInfo); int left = faceInfoList.get(0).getRect().getLeft(); int top = faceInfoList.get(0).getRect().getTop(); int width = faceInfoList.get(0).getRect().getRight() - left; int height = faceInfoList.get(0).getRect().getBottom() - top; Graphics2D graphics2D = bufImage.createGraphics(); // 红色 graphics2D.setColor(Color.RED); BasicStroke stroke = new BasicStroke(5f); graphics2D.setStroke(stroke); graphics2D.drawRect(left, top, width, height); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(bufImage, "jpg", outputStream); byte[] bytes1 = outputStream.toByteArray(); faceSearchResDto.setImage("data:image/jpeg;base64," + Base64Utils.encodeToString(bytes1)); faceSearchResDto.setAge(processInfoList.get(0).getAge()); faceSearchResDto.setGender(processInfoList.get(0).getGender().equals(1) ? "女" : "男"); } return AjaxResult.success(faceSearchResDto); } return AjaxResult.error(ErrorCodeEnum.FACE_DOES_NOT_MATCH.getDescription());
测试接口流程
准备两张图片
百度随便搜索个在线转换 Base64
1、人脸添加
新增有脸的
新增测试无脸的
2、人脸识别
有脸的
无脸的
结尾在线演示
(前往享受人脸识别)
Json 序列化框架导致 CPU 使用率过高
陈叔叔 发表了文章 • 0 个评论 • 60 次浏览 • 2022-05-20 23:48
问题现象:CPU 负载过高
我们线上的 jenkins 系统,时不时会发生 CPU 负载过高的现象。
CPU 负载过高后,SRE 同学会收到电话告警。
在我们的监控系统中,可以看到,某些时候,CPU 的负载确实会很高,如下图:
问题排查
Jenkins 系统本身是一个 Java 程序,应对 Java 程序导致的 CPU 使用率过高这一问题,GitHub 上有现成的解决方案:show-busy-java-threads。
下载链接如下:
- GitHub:show-busy-java-threads
- Gitee:show-busiest-java-threads
登录上机器,在 CPU 使用率高时候,执行 show-busy-java-threads 脚本:./show-busy-java-threads
。
摘选其中的一些输出如下:
The stack of busy(25.0%) thread(20239/0x4f0f) of java process(248927) of user(jenkins):"Handling GET /job/jenkins-test-job/api/json from 172.168.1.1 : qtp1641808846-3127" #3127 prio=5 os_prio=0 tid=0x00007f7380014000 nid=0x4f0f runnable [0x00007f722c392000] java.lang.Thread.State: RUNNABLE at java.util.Arrays.copyOfRange(Arrays.java:3664) at java.lang.String.<init>(String.java:207) at java.lang.String.substring(String.java:1933) at net.sf.json.util.JSONTokener.matches(JSONTokener.java:110) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:912) at net.sf.json.JSONObject.fromObject(JSONObject.java:156) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348) at net.sf.json.JSONArray._fromJSONTokener(JSONArray.java:1131) at net.sf.json.JSONArray.fromObject(JSONArray.java:125) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:351) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955) at net.sf.json.JSONObject.fromObject(JSONObject.java:156) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955) at net.sf.json.JSONObject.fromObject(JSONObject.java:156) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955) at net.sf.json.JSONObject.fromObject(JSONObject.java:156) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955) at net.sf.json.JSONObject._fromString(JSONObject.java:1145) at net.sf.json.JSONObject.fromObject(JSONObject.java:162) at net.sf.json.JSONObject.fromObject(JSONObject.java:132) at sam.Sam.sendRequestReturnJson(Sam.java:517) at sam.Sam.getPermissionByUser(Sam.java:225) at sam.Sam.checkUserPermissionLocal(Sam.java:243) at com.michelin.cio.hudson.plugins.rolestrategy.PermissionCache.getPermissionSam(RoleMap.java:155) at com.michelin.cio.hudson.plugins.rolestrategy.PermissionCache.getPermission(RoleMap.java:106) at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap.hasPermission(RoleMap.java:220) at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap.access$000(RoleMap.java:166) at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap$AclImpl.hasPermission(RoleMap.java:569) at hudson.security.SidACL._hasPermission(SidACL.java:70)
从上面的输出可以看到,25.0% 的 CPU 资源在处理 Handling GET /job/jenkins-test-job/api/json from 172.168.1.1
这个请求。
运维同学根据这个 ip ,定位到发起请求的是某同学 A。这个同学在跑一些定时任务,定时拉取 job 的执行结果。
问题是当我直接访问这个接口:/job/jenkins-test-job/api/json 时,返回并不慢,几乎很快就可以返回。问题应该不是这个接口的问题。
我们接着从 ./show-busy-java-threads
输出往下看:看到其中有问题的调用栈:
at net.sf.json.JSONObject.fromObject(JSONObject.java:132)at sam.Sam.sendRequestReturnJson(Sam.java:517)at sam.Sam.getPermissionByUser(Sam.java:225)at sam.Sam.checkUserPermissionLocal(Sam.java:243)
看起来是这个 Sam 校验用户权限导致的 CPU 使用率过高,而接着看上面的代码 net.sf.json.JSONObject.fromObject
,这个是在做 json 的反序列化。
通常来说,json 的序列化、反序列化都是比较费 CPU 的,更糟糕的是,这里用到的 json 序列化框架是 net.sf.json,而不是 Java 常用的 jackson 和 gson 等。
直觉告诉我,肯定是这个 net.sf.json 反序列化引起的 CPU 使用率过高问题。
备注:
通过跟之前维护 jenkins 的同学了解到,他们基于 role-strategy 插件,重写了 jenkins 权限验证逻辑,用的就是 Sam 权限。翻看 sam 权限插件的代码,确实有用 net.sf.json 做 json 反序列化。
到这里,定位到大概率是 Sam 权限插件的 net.sf.json 反序列化引起的问题。
问题复现
为了验证这个问题,我们拿到 Sam 权限插件的代码。找到出问题的关键代码:
public void getPermissionByUser(String email) { JSONObject params = new JSONObject(); params.put("user_email", email); params.put("subsystem_id", SAM_JENKINS_SUM_SYSTEM_ID); JSONObject res = sendRequestReturnJson(URL, "GET", params); if (res.get("success").equals(true)) { cacheUserPermission(params.getString("user_email"), res.getJSONObject("permission").getJSONObject(email).getJSONObject("SERVICE")); }}public static JSONObject sendRequestReturnJson(String endpoint, String method, JSONObject params) { if (method.equals("POST")) { return JSONObject.fromObject(sendPostRequest(endpoint, params)); } else if (method.equals("GET")) { return JSONObject.fromObject(sendGetRequest(endpoint, params)); } return new JSONObject();}
可以看到,这段代码会根据用户邮箱,发送 http 请求调用 Sam 系统,获取用户的权限数据,然后将数据反序列化成 JSONObject,即:
JSONObject.fromObject(sendGetRequest(endpoint, params, token))
在本地,通过复现 A 同学的请求,发现这个请求确实比较慢,而且费 CPU。通过 debug 得知,这个用户返回的 json 数据有 1M 左右,json 反序列化 CPU 打满。
而通过其他用户请求,发现处理很快,返回的 json 数据也比较小。
到这里,确认就是 net.sf.json 框架的反序列化性能问题,引起的 CPU 使用率过高。我们需要替换成其他高性能的 json 序列化框架。
备选有:gson、jackson、fastjson等。fastjson 因为经常出安全漏洞,暂不考虑,我们考虑从 gson、jackson 选择一个。
在选定之前,先对 gson、jackson, 的性能做个基准测试,并与 net.sf.json 做对比。
JMH 基准测试 json 框架性能
Json 框架的性能测试,我们选用 JMH 框架。
JMH 框架是 JDK 官方提供的性能基准测试套件,参考:https://github.com/openjdk/jmh
代码如下:
import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.gson.Gson;import com.google.gson.JsonObject;import net.sf.json.JSONObject;import org.apache.commons.io.FileUtils;import org.openjdk.jmh.annotations.Benchmark;import org.openjdk.jmh.annotations.BenchmarkMode;import org.openjdk.jmh.annotations.Mode;import org.openjdk.jmh.annotations.OutputTimeUnit;import org.openjdk.jmh.annotations.Param;import org.openjdk.jmh.annotations.Scope;import org.openjdk.jmh.annotations.Setup;import org.openjdk.jmh.annotations.State;import org.openjdk.jmh.results.format.ResultFormatType;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.RunnerException;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;import org.springframework.util.ResourceUtils;import java.io.File;import java.io.IOException;import java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MILLISECONDS)@State(Scope.Benchmark)public class JsonBenchmark { @Param({"10", "100", "500"}) private int length; private String json; private String email = "a@123.com"; private String path = "classpath:sam.json"; @Benchmark public void testGson() throws IOException { Gson gson = new Gson(); JsonObject root = gson.fromJson(json, JsonObject.class); if (root.getAsJsonObject("success").getAsBoolean()) { JsonObject services = root.get("permission").getAsJsonObject() .get(email).getAsJsonObject() .get("SERVICE").getAsJsonObject(); System.out.println(services.size()); } } @Benchmark public void testJackson() throws IOException { ObjectMapper objectMapper = new ObjectMapper(); JsonNode root = objectMapper.readTree(json); if (root.get("success").asBoolean()) { JsonNode services = root.get("permission").get(email).get("SERVICE"); System.out.println(services.size()); } } @Benchmark public void testJsonObject() throws IOException { JSONObject root = JSONObject.fromObject(json); if (root.get("success").equals(true)) { JSONObject services = root.getJSONObject("permission").getJSONObject(email).getJSONObject("SERVICE"); System.out.println(services.size()); } } @Setup public void prepare() throws IOException { File file = ResourceUtils.getFile(path); json = FileUtils.readFileToString(file); } public static void main(String[] args) throws RunnerException { Options options = new OptionsBuilder() .include(JsonBenchmark.class.getSimpleName()) .forks(1) .warmupIterations(5) .measurementIterations(2)// .output("/Users/wxweven/Benchmark.log") .result("result.json") .resultFormat(ResultFormatType.JSON) .build(); new Runner(options).run(); }}
测试的结果如下:
Benchmark (length) Mode Cnt Score Error UnitsJsonBenchmark.testGson 10 avgt 2 7.979 ms/opJsonBenchmark.testGson100 avgt 2 8.958 ms/opJsonBenchmark.testGson500 avgt 2 9.975 ms/opJsonBenchmark.testJackson 10 avgt 2 10.393 ms/opJsonBenchmark.testJackson 100 avgt 2 12.214 ms/opJsonBenchmark.testJackson 500 avgt 2 10.548 ms/opJsonBenchmark.testJsonObject 10 avgt 2 1350.788 ms/opJsonBenchmark.testJsonObject 100 avgt 2 1350.583 ms/opJsonBenchmark.testJsonObject 500 avgt 2 1381.046 ms/op
可以看到,gson 和 jackson 性能接近,但是 jsonlib 性能就很差,比另外两个慢 100 多倍。
综合考虑性能、api 易用性等,选定 gson 作为替代方案。
替换成 gson
将之前的代码替换成 gson,代码如下:
public void getPermissionByUser(String email) { JSONObject params = new JSONObject(); params.put("user_email", email); params.put("subsystem_id", SAM_JENKINS_SUM_SYSTEM_ID); JsonObject res = sendRequestReturnJsonV2(URL, "GET", params); if (res.get("success").getAsBoolean()) { cacheUserPermission(params.getString("user_email"), res.getAsJsonObject("permission").getAsJsonObject(email).getAsJsonObject("SERVICE")); } }public static JsonObject sendRequestReturnJsonV2(String endpoint, String method, JSONObject params) throws IOException { if (method.equals("POST")) { return GSON.fromJson(sendPostRequest(endpoint, params, token), JsonObject.class); } else if (method.equals("GET")) { return GSON.fromJson(sendGetRequest(endpoint, params, token), JsonObject.class); } return new JsonObject();}
重新编译权限插件后上线,再次查看 CPU 负载监控,发现 CPU 负载确实降下来了(05/13晚上 0 点左右上线的)。
再次重新编译,问题得到解决。
结束语
这个问题,前前后后花费了不少时间,也困扰了 DevOps 团队比较久,经过大家的齐心协力,总算是把问题给解决了。
这篇文章也是对之前排查、解决问题的一个总结。
同时,也提醒大家,在使用第三方 jar 包的时候,一定要注意该 jar 包有没有性能、安全等问题。如果不确定的话,可以用 JMH 等手段自己测试以下。
我是梅小西,最近在某东南亚电商公司做 DevOps 的相关事情。从本期开始,将陆续分享基于 Jenkins 的 CI/CD 工作流,包括 Jenkins On k8s 等。
如果你对 Java 或者 Jenkins 等感兴趣,欢迎与我联系,微信:wxweven(备注 DevOps)
本文由博客群发一文多发等运营工具平台 OpenWrite 发布
Netty源码研究笔记(2)——Bootstrap系列
陈叔叔 发表了文章 • 0 个评论 • 51 次浏览 • 2022-05-20 19:30
1. Netty源码研究笔记(2)——Bootstrap系列
顾名思义,Bootstrap
是netty提供给使用者的脚手架,类似于Spring的ApplicationContext
,通过Bootstrap
我们使用一些自定义选项,将相关的组件打包起来,从而快速的启动服务器、客户端。
Bootstrap
分为:Bootstrap
、ServerBootstrap
两种,其中前者用于构建客户端、后者用于构建服务端。
Bootstrap
的实现使用的是典型的建造者模式,使用这种模式的好处是使得构造对象时可读性更好,更不容易出错(相比于使用构造函数而言),并且可以在构造的时候进行校验。
Bootstrap
系列还包含相关的BootstrapConfig
。
BootstrapConfig
的实现类似外观模式,只暴露其内部持有Bootstrap
的配置信息。(配置信息实际放在Bootstrap
中)。
1.1. 继承关系
1.2. AbstractBootstrap
注意:bind
方法定义在AbstractBoostrap
中。
在使用点对点的传输时,一般在Server端使用bind
,在Client端使用connect
。
如果bind
方法不是通过ServerBootstrap
使用,那么对应的是无连接的传输场景,比如:UDP。
AbstractBootstrap
是一个泛型类,它的泛型参数是其实现类,这样便可以使其链式调用方法返回的类型其具体的实现类。
忽略EMPTY_OPTION_ARRAY
、EMPTY_ATTRIBUTE_ARRAY
,这两者用于提供泛型方法的类型入参,从而指示其返回类型,没有实际意义。
我们可以看到在AbstractBootstrap
中需要配置:
- 关联的EventLoopGroup(必选)
- 监听的local socket address:对于端到端的server必选,对于无连接的也必选
- channel handler:必选,一般是channel initializer,负责将需要的channel handler添加到channel pipeline中。
- channel option(可选)
- channel attribute(可选)
- channel factory:一般使用默认的channel factory,它使用反射机制来创建channel
bind
、connect
方法在前文已经分析过了,这里我们看未谈到的validate
、init
方法。
validate:
AbstractBootstrap
只校验了EventLoopGroup
。
init:
子类中实现。
1.3. ServerBootstrap
在ServerBootstrap
需要配置:
- child channel option:可选,表示和客户端建立的传输信道的配置项。
- child channel attibute:可选,表示和客户端建立的传输信道的属性项。
- child channel handler:必填,一般是channel initializer,表示对客户端请求的处理逻辑
- child event loop group:可选,表示客户端请求处理器的执行线程组。
validate:
校验child channel handler
init:
ServerBootstrap::init
方法给channel pipeline 添加用户指定的channel handler,以及一个ServerBootstrapAcceptor
类型的channel handler,它使得服务端可以和客户端建立端到端的连接。
ServerBootstrapAcceptor
以后再谈。
1.4. Bootstrap
validate:
验证channel handler是否存在,即判断对server的response的处理器是否定义了。
init:
给channel pipeline添加用户指定的channel handler。
1.5. AbstractBootstrapConfig、ServerBootstrapConfig、BootstrapConfig
AbstractBootstrapConfig:
ServerBootstrapConfig:
BootstrapConfig:
吊炸天,Spring Security还有这种用法!
陈叔叔 发表了文章 • 0 个评论 • 44 次浏览 • 2022-05-20 10:59
在用Spring Security项目开发中,有时候需要放通某一个接口时,我们需要在配置中把接口地址配置上,这样做有时候显得麻烦,而且不够优雅。我们能不能通过一个注解的方式,在需要放通的接口上加上该注解,这样接口就能放通了。答案肯定是可以的啦,今天我们一起来看看实现过程吧。
SpringBoot版本
本文基于的Spring Boot的版本是2.6.7
实现思路
- 新建一个
AnonymousAccess
注解,该注解是应用于Controller
方法上的 - 新建一个存放所有请求方式的枚举类
- 通过判断
Controller
方法上是否存在该注解 - 在
SecurityConfig
上进行策略的配置
实现过程
新建注解
@Inherited@Documented@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface AnonymousAccess { }
新建请求枚举类
该类是存放所有的请求类型的,代码如下:
@Getter@AllArgsConstructorpublic enum RequestMethodEnum { /** * 搜寻 @AnonymousGetMapping */ GET("GET"), /** * 搜寻 @AnonymousPostMapping */ POST("POST"), /** * 搜寻 @AnonymousPutMapping */ PUT("PUT"), /** * 搜寻 @AnonymousPatchMapping */ PATCH("PATCH"), /** * 搜寻 @AnonymousDeleteMapping */ DELETE("DELETE"), /** * 否则就是所有 Request 接口都放行 */ ALL("All"); /** * Request 类型 */ private final String type; public static RequestMethodEnum find(String type) { for (RequestMethodEnum value : RequestMethodEnum.values()) { if (value.getType().equals(type)) { return value; } } return ALL; }}
判断Controller
方法上是否存在该注解
在SecurityConfig
类中定义一个私有方法getAnonymousUrl
,该方法主要作用是判断controller那些方法加上了AnonymousAccess
的注解
private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) { Map<String, Set<String>> anonymousUrls = new HashMap<>(8); Set<String> get = new HashSet<>(); Set<String> post = new HashSet<>(); Set<String> put = new HashSet<>(); Set<String> patch = new HashSet<>(); Set<String> delete = new HashSet<>(); Set<String> all = new HashSet<>(); for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) { HandlerMethod handlerMethod = infoEntry.getValue(); AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class); if (null != anonymousAccess) { List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods()); RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name()); switch (Objects.requireNonNull(request)) { case GET: get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case POST: post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case PUT: put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case PATCH: patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case DELETE: delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; default: all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; } } } anonymousUrls.put(RequestMethodEnum.GET.getType(), get); anonymousUrls.put(RequestMethodEnum.POST.getType(), post); anonymousUrls.put(RequestMethodEnum.PUT.getType(), put); anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch); anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete); anonymousUrls.put(RequestMethodEnum.ALL.getType(), all); return anonymousUrls; }
在SecurityConfig
上进行策略的配置
通过一个SpringUtil
工具类获取到requestMappingHandlerMapping
的Bean
,然后通过getAnonymousUrl
方法把标注AnonymousAccess
接口找出来。最后,通过antMatchers
细腻化到每个 Request 类型。
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { // 搜寻匿名标记 url: @AnonymousAccess RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) SpringUtil.getBean("requestMappingHandlerMapping"); Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); // 获取匿名标记 Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap); httpSecurity //禁用CSRF .csrf().disable() .authorizeRequests() // 自定义匿名访问所有url放行:细腻化到每个 Request 类型 // GET .antMatchers(HttpMethod.GET,anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll() // POST .antMatchers(HttpMethod.POST,anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll() // PUT .antMatchers(HttpMethod.PUT,anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll() // PATCH .antMatchers(HttpMethod.PATCH,anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll() // DELETE .antMatchers(HttpMethod.DELETE,anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll() // 所有类型的接口都放行 .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll() // 所有请求都需要认证 .anyRequest().authenticated(); }
在Controller方法上应用
在Controller上把需要的放通的接口上加上注解,即可不需要认证就可以访问了,是不是很方便呢。例如,验证码不需要认证访问的,代码如下:
@ApiOperation(value = "获取验证码", notes = "获取验证码") @AnonymousAccess @GetMapping("/code") public Object getCode(){ Captcha captcha = loginProperties.getCaptcha(); String uuid = ""+IdUtil.simpleUUID(); //当验证码类型为 arithmetic时且长度 >= 2 时,captcha.text()的结果有几率为浮点型 String captchaValue = captcha.text(); if(captcha.getCharType()-1 == LoginCodeEnum.ARITHMETIC.ordinal() && captchaValue.contains(".")){ captchaValue = captchaValue.split("\.")[0]; } // 保存 redisUtils.set(uuid,captchaValue,loginProperties.getLoginCode().getExpiration(), TimeUnit.MINUTES); // 验证码信息 Map<String,Object> imgResult = new HashMap<String,Object>(2){{ put("img",captcha.toBase64()); put("uuid",uuid); }}; return imgResult; }
效果展示
更多经常的内容请关注公众号"攻城狮成长日记"
SpringBoot MySQL密码等敏感信息加密方案(2021.04.27)
陈叔叔 发表了文章 • 0 个评论 • 49 次浏览 • 2022-05-26 15:51
SpringBoot MySQL密码等敏感信息加密方案(2021.04.27)
一、背景说明
SpringBoot 项目经常将连接数据库的密码明文放在配置文件里,安全性就比较低,尤其一些企业对安全性要求很高,因此我们就考虑如何对密码等敏感信息进行加密。
二、解决方案
通过 Jasypt 对密码等敏感信息进行加密。Jasypt 是一个简单易用的加解密 Java 库,可以快速的集成到 Spring 和 SpringBoot 项目中,并提供了自动配置,使用非常简单。
三、使用方法
下面以 SpringBoot、MySQL、Mybatis 来演示 Jasypt 库的集成。
1. 引入 Maven 依赖
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.3</version></dependency>
2. 在配置文件添加 Jasypt 的相关配置;
jasypt: encryptor: # 加密的盐值,为了安全,该配置可以放在启动参数或者命令行参数中 password: fa7bd4edd42448aea8c9484fbce6e8cd # 加密所采用的算法 algorithm: PBEWITHHMACSHA512ANDAES_256
3. 生成加密后的密钥
方案一:采用单元测试生成加密后的密钥;
package com.xiaoqqya.controller;import org.jasypt.encryption.StringEncryptor;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;import java.util.UUID;/** * Description. * * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a> * @version 1.0 * @date 2021/04/20 * @since 1.8 */@SpringBootTest(classes = com.xiaoqqya.JasyptDemoApplication.class)public class Demo { @Resource StringEncryptor encryptor; @Test public void encrypt() { String url = encryptor.encrypt("jdbc:mysql://ubuntu:3306/jasypt?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC"); String username = encryptor.encrypt("root"); String pwd = encryptor.encrypt("password"); System.out.println("url = " + url); System.out.println("username = " + username); System.out.println("pwd = " + pwd); }}
方案二:采用命令行生成加密后的密钥;
# 在 Maven 仓库中找到 jasypt-1.9.3.jar 包$ cd C:UsersJoey.m2repositoryorgjasyptjasypt1.9.3# input 为明文密码,password 为加密盐值,algorithm为算法$ java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="root" password="fa7bd4edd42448aea8c9484fbce6e8cd"
4. 替换数据库密码
ENC() 为固定格式,() 里面为加密后的密钥。
spring: datasource: url: ENC(RSSEciOYx39dlPxCWeP46RZG1wkgtuPMgaZu2XgnrPluvHcAIzIEW79K1j9WIWLVHPbBpb6t1ehiiTiQGnzR1CFvrFm16bE0koyh/8exbh1ulYkfaSdBOivNHIF6CSxPy54vmxn3LaXug6ZYxfNBymQINbRa2fsXxlHT+TgvKqs=) username: ENC(YkJGWv80AEpPREn3Rt2Ic6BzzO+v+3m5Uy33pz4ZbZbD3vhi7vJz9nwGHKg8+n) password: ENC(4lEHAy//ExXjJxN9WQgyqgAjSkzIJ3irTYTYG8so6HgZWxPRl6Pa8tCUK/qmXSYb) driver-class-name: com.mysql.cj.jdbc.Driver
5. 启动测试
# 将盐值放在启动参数中$ java -jar target/jasypt-demo-0.0.1-SNAPSHOT.jar --jasypt.encryptor.password=fa7bd4edd42448aea8c9484fbce6e8cd# 将盐值放在命令行参数中$ java -Djasypt.encryptor.password=fa7bd4edd42448aea8c9484fbce6e8cd -jar target/jasypt-demo-0.0.1-SNAPSHOT.jar# 也可以将盐值放在系统环境变量中,此处不再展示
【Java面试】IO和NIO有什么区别?
陈叔叔 发表了文章 • 0 个评论 • 55 次浏览 • 2022-05-26 14:29
IO问题一直是面试的重灾区之一
但又是非常重要而且面试必问的知识点
一个工作了7年的粉丝私信我,他去面试了 4家互联网公司,
有三个公司问他网络IO的问题,另外一个公司问了Netty,结果都没回答上来。
好吧,对于“IO和NIO的区别”,看看普通人和高手的回答。
普通人:
嗯。。。。。。。。。。
高手:
好的,关于这个问题,我会从下面几个方面来回答。
首先,I/O ,指的是IO流, 它可以实现数据从磁盘中的读取以及写入。
实际上,除了磁盘以外,内存、网络都可以作为I/O流的数据来源和目的地。
在Java里面,提供了字符流和字节流两种方式来实现数据流的操作。
其次,当程序是面向网络进行数据的IO操作的时候,Java里面提供了Socket的方式来实现。
通过这种方式可以实现数据的网络传输。
基于Socket的IO通信,它是属于阻塞式IO,也就是说,在连接以及IO事件未就绪的情况下,当前的连接会处于阻塞等待的状态。
如果一旦某个连接处于阻塞状态,那么后续的连接都得等待。所以服务端能够处理的连接数量非常有限。
NIO,是JDK1.4里面新增的一种NEW IO机制,相比于传统的IO,NIO在效率上做了很大的优化,并且新增了几个核心组件。
Channel、Buffer、Selectors。
另外,还提供了非阻塞的特性,所以,对于网络IO来说,NIO通常也称为No-Block IO,非阻塞IO。
也就是说,通过NIO进行网络数据传输的时候,如果连接未就绪或者IO事件未就绪的情况下,服务端不会阻塞当前连接,而是继续去轮询后续的连接来处理。
所以在NIO里面,服务端能够并行处理的链接数量更多。
因此,总的来说,IO和NIO的区别,站在网络IO的视角来说,前者是阻塞IO,后者是非阻塞IO。
以上就是我对这个问题的理解。
总结
在互联网时代,网络IO是最基础的技术。
无论是微服务架构中的服务通信、还是应用系统和中间件之间的网络通信,都在体现网络IO的重要性。
如果想获得一对一的面试指导以及面试资料,可以私信我。
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自
Mic带你学架构
!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!
instanceof和getClass的区别说明
陈叔叔 发表了文章 • 0 个评论 • 62 次浏览 • 2022-05-26 12:08
转自:
http://www.java265.com/JavaJingYan/202205/16514583603236.html
instanceof是java的一个二元操作符,它是由字母组成的,所以也是java的保留关键字.
instanceof关键字的功能:
判断其左边对象是否为其右边类的实例,返回boolean类型的数据
可以来判断继承中的子类的实例是否为父类的实现
java中的instanceof是通过返回一个布尔值来指出
这个对象是否是这个特定类或者是它的子类的一个实例
下文笔者讲述java中instanceof和getClass的区别,如下所示:
instanceof和getClass都可用于比较两个对象是否属于同一个类的实例 但是 instanceof检测一个类的父类和子类(检测类和派生类),都会返回true 但是getClass是返回具体的字符串,只做严格的类判断
public class Test{ /* *java265.com 示例分享 */ public static void testInstanceof(Object x) { System.out.println("x instanceof Parent: "+(x instanceof Parent)); System.out.println("x instanceof Child: "+(x instanceof Child)); System.out.println("x getClass Parent: "+(x.getClass() == Parent.class)); System.out.println("x getClass Child: "+(x.getClass() == Child.class)); } public static void main(String[] args) { testInstanceof(new Parent()); System.out.println("========================"); testInstanceof(new Child()); }}class Parent { }class Child extends Parent { }/*输出:x instanceof Parent: truex instanceof Child: falsex getClass Parent: truex getClass Child: false========================x instanceof Parent: truex instanceof Child: truex getClass Parent: falsex getClass Child: true*/
日志框架体系整理( 基础 )
陈叔叔 发表了文章 • 0 个评论 • 80 次浏览 • 2022-05-24 17:13
0、前言
- 本篇博客的初衷是为了特定的人弄的,这些内容在网上都可以找到原文,因此:这篇博客只是保证能够让特定的人看懂,其他人看得懂就看,看不懂拉倒,同时,这篇博客中细节说明没有、运行截图没有、特别备注没有......
1、JUL
- 指的是Java Util Logging包,它是java原生的日志框架,使用时不需要另外引用第三方的类库,相对其他的框架使用方便,学习简单,主要是使用在小型应用中
1.1、JUL的组成结构
-
Logger:被称为记录器,应用程序通过获取Logger对象,抵用其API来发布日志信息。Logger通常被认为是访问日志系统的入口程序。
-
Handler:处理器,每个Logger都会关联一个或者是一组Handler,Logger会将日志交给关联的Handler去做处理,由Handler负责将日志做记录。Handler具体实现了日志的输出位置,比如可以输出到控制台或者是文件中等等。
-
Filter:过滤器,根据需要定制哪些信息会被记录,哪些信息会被略过。
-
Formatter:格式化组件,它负责对日志中的数据和信息进行转换和格式化,所以它决定了我们输出日志最终的形式。
-
Level:日志的输出级别,每条日志消息都有一个关联的级别。我们根据输出级别的设置,用来展现最终所呈现的日志信息。根据不同的需求,去设置不同的级别。
1.2、入门
- 可以直接去JDK文档中查看java.util.logging这个包
public class JULTest { @Test public void test01(){ /* 日志入口程序 java.util.logging.Logger */ // Logger对象的创建方式,不能直接new对象 // 取得对象的方法参数,需要引入当前类的全路径字符串(当前我们先这么用,以后根据包结构有Logger父子关系,以后详细介绍) Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 对于日志的输出,有两种方式 第一种方式: 直接调用日志级别相关的方法,方法中传递日志输出信息 假设现在我们要输出info级别的日志信息 */ //logger.info("输出info信息1"); /* 第二种方式: 调用通用的log方法,然后在里面通过Level类型来定义日志的级别参数,以及搭配日志输出信息的参数 */ //logger.log(Level.INFO,"输出info信息2"); /* 输出学生信息 姓名 年龄 */ /*String name = "zs"; int age = 23; logger.log(Level.INFO,"学生的姓名为:"+name+";年龄为:"+age);*/ /* 对于输出消息中,字符串的拼接弊端很多 1.麻烦 2.程序效率低 3.可读性不强 4.维护成本高 我们应该使用动态生成数据的方式,生产日志 我们使用的就是占位符的方式来进行操作 */ String name = "zs"; int age = 23; logger.log(Level.INFO,"学生的姓名:{0},年龄:{1}",new Object[]{name,age}); }}
1.3、日志级别
@Test public void test02(){ /* 日志的级别(通过源码查看,非常简单) SEVERE : 错误 --- 最高级的日志级别 WARNING : 警告 INFO : (默认级别)消息 源码:Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->350行左右有一个 defaultlevel属性 CONFIG : 配置 FINE : 详细信息(少) FINER : 详细信息(中) FINEST : 详细信息 (多) --- 最低级的日志级别 两个特殊的级别 OFF 可用来关闭日志记录 ALL 启用所有消息的日志记录 对于日志的级别,重点关注的是new对象的时候的第二个参数,是一个数值(源码中有) OFF Integer.MAX_VALUE 整型最大值 SEVERE 1000 WARNING 900 ... ... FINEST 300 ALL Integer.MIN_VALUE 整型最小值 这个数值的意义在于,如果设置的日志的级别是INFO -- 800 那么最终展现的日志信息,必须是数值大于800的所有的日志信息 最终展现的就是 SEVERE WARNING INFO */ Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 通过打印结果,我们看到了仅仅只是输出了info级别以及比info级别高的日志信息 比info级别低的日志信息没有输出出来 证明了info级别的日志信息,它是系统默认的日志级别 在默认日志级别info的基础上,打印比它级别高的信息 */ /* 如果仅仅只是通过以下形式来设置日志级别 那么不能够起到效果 将来需要搭配处理器handler共同设置才会生效 */ logger.setLevel(Level.CONFIG); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
1.4、自定义日志级别
1.4.1、输出信息在console上
@Test public void test03(){ /* 自定义日志的级别 */ // 日志记录器 Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); /* 将默认的日志打印方式关闭掉 参数设置为false,打印日志的方式就不会按照父logger默认的方式去进行操作 */ logger.setUseParentHandlers(false); /* 处理器Handler 这里使用的是控制台日志处理器,取得处理器对象 */ ConsoleHandler handler = new ConsoleHandler(); // 创建日志格式化组件对象 SimpleFormatter formatter = new SimpleFormatter(); // 在处理器中设置输出格式 handler.setFormatter(formatter); // 在记录器中添加处理器 logger.addHandler(handler); /*设置日志的打印级别 此处必须将日志记录器和处理器的级别进行统一的设置,才会达到日志显示相应级别的效果 logger.setLevel(Level.CONFIG); handler.setLevel(Level.CONFIG); */ // 设置如下的all级别之后,那么下面所有的输出信息都会打印到控制台,在需要的时候改成对应的日志级别即可 logger.setLevel(Level.ALL); handler.setLevel(Level.ALL); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
1.4.2、输出信息在磁盘上
@Test public void test04() throws IOException { /* 将日志输出到具体的磁盘文件中 这样做相当于是做了日志的持久化操作 */ Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); logger.setUseParentHandlers(false); // 文件日志处理器 FileHandler handler = new FileHandler("D:\test\" + this.getClass().getSimpleName() + ".log"); SimpleFormatter formatter = new SimpleFormatter(); handler.setFormatter(formatter); logger.addHandler(handler); // 也可以同时在控制台和文件中进行打印 ConsoleHandler handler2 = new ConsoleHandler(); handler2.setFormatter(formatter); // 可以在记录器中同时添加多个处理器,这样磁盘和控制台中都可以输出对应级别的日志信息了 logger.addHandler(handler2); logger.setLevel(Level.ALL); handler.setLevel(Level.ALL); handler2.setLevel(Level.CONFIG); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); /* 总结: 用户使用Logger来进行日志的记录,Logger可以持有多个处理器Handler (日志的记录使用的是Logger,日志的输出使用的是Handler) 添加了哪些handler对象,就相当于需要根据所添加的handler 将日志输出到指定的位置上,例如控制台、文件.. */ }
1.5、logger的父子关系
@Test public void test05(){ /* Logger之间的父子关系 JUL中Logger之间是存在"父子"关系的 值得注意的是,这种父子关系不是我们普遍认为的类之间的继承关系 关系是通过树状结构存储的 */ /* 从下面创建的两个logger对象看来 我们可以认为logger1是logger2的父亲 */ /*父亲是RootLogger,名称默认是一个空的字符串 RootLogger可以被称之为所有logger对象的顶层logger */ // 这就是父 Logger logger1 = Logger.getLogger("cn.zixieqing.jul.test"); // 这是子,甚至cn.zixieqing.jul也是这个logger2的父 Logger logger2 = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); //System.out.println(logger2.getParent()==logger1); //true System.out.println("logger1的父Logger引用为:" +logger1.getParent()+"; 名称为"+logger1.getName()+"; 父亲的名称为"+logger1.getParent().getName()); System.out.println("logger2的父Logger引用为:" +logger2.getParent()+"; 名称为"+logger2.getName()+"; 父亲的名称为"+logger2.getParent().getName()); /* 父亲所做的设置,也能够同时作用于儿子 对logger1做日志打印相关的设置,然后我们使用logger2进行日志的打印 */ // 父亲做设置 logger1.setUseParentHandlers(false); ConsoleHandler handler = new ConsoleHandler(); SimpleFormatter formatter = new SimpleFormatter(); handler.setFormatter(formatter); logger1.addHandler(handler); handler.setLevel(Level.ALL); logger1.setLevel(Level.ALL); // 儿子做打印 - 结果就是:儿子logger2没做配置,但是父亲logger1做了,所以儿子logger2的输出级别就是父亲的级别 logger2.severe("severe信息"); logger2.warning("warning信息"); logger2.info("info信息"); logger2.config("config信息"); logger2.fine("fine信息"); logger2.finer("finer信息"); logger2.finest("finest信息"); }
小结:看源码的结果
// JUL在初始化时会创建一个顶层RootLogger作为所有Logger的父Logger,java.util.logging.LogManager$RootLogger,默认的名称为空串 // 查看源码Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->350行左右: owner.rootLogger = owner.new RootLogger(); RootLogger是LogManager的内部类 /* 以上的RootLogger对象作为树状结构的根节点存在的 将来自定义的父子关系通过路径来进行关联 父子关系,同时也是节点之间的挂载关系 */ // 350行左右 owner.addLogger(owner.rootLogger); addLogger ----> LoggerContext cx = getUserContext(); /* LoggerContext一种用来保存节点的Map关系,WeakHashMap<Object, LoggerContext> contextsMap 点进WeakHashMap<Object, LoggerContext>的LoggerContext,里面的结构为:Hashtable<String,LoggerWeakRef> namedLoggers */ // 再点开Hashtable<String,LoggerWeakRef> namedLoggers 的LoggerWeakRef,得到的信息如下: private String name; // for namedLoggers cleanup,这玩意就是节点,也就是说父子关系也就是节点挂载关系 private LogNode node; // for loggerRef cleanup private WeakReference<Logger> parentRef; // for kids cleanup
1.6、使用配置文件
1.6.1、默认配置文件所在地
- 查看源码
Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->345行左右: 有这么一行代码:owner.readPrimordialConfiguration(); 点击readPrimordialConfiguration(),在340行左右有一句readConfiguration(); 点击readConfiguration();在1290行左右,有如下的代码 String fname = System.getProperty("java.util.logging.config.file"); if (fname == null) { fname = System.getProperty("java.home"); if (fname == null) { throw new Error("Can't find java.home ??"); } File f = new File(fname, "lib"); f = new File(f, "logging.properties"); fname = f.getCanonicalPath(); }
结论:默认配置文件所在地
- java.home( 即:安装JDK的目录 ) --> 找到jre文件夹 --> lib --> logging.properties
分析一下上述目录中的logging.properties文件,找打这个目录下的此文件打开
# RootManager默认使用的处理器# 若是想要配置多个处理器,可以使用逗号进行分开,如:java.util.logging.ConsoleHandler,java.util.logging.FileHandlerhandlers= java.util.logging.ConsoleHandler# RootManager的默认日志级别,这是全局的日志级别# 若不手动进行级别配置,那么默认使用INFO及更高的级别进行输出.level= INFO# 文件处理器设置# 日志文件的路径# %h/java%u.log h指的是用户目录 - 不分window还是linux# java%u.log是生成的文件名,其中:u相当于是自增,从0开始的# 如:java0.log、java1.log、java2.log......这里生成多少份由java.util.logging.FileHandler.count = 1这个配置决定java.util.logging.FileHandler.pattern = %h/java%u.log# 日志文件的限制 - 默认50000字节java.util.logging.FileHandler.limit = 50000# 日志文件的数量 - 默认1份java.util.logging.FileHandler.count = 1# 日志文件的格式,默认XML格式,也可以采用SimpleFormatterjava.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter# 控制台处理器设置# 控制台默认级别java.util.logging.ConsoleHandler.level = INFO# 控制台默认输出格式java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter# 这是这个源文档中举的一个例子,意思是像下面这样,可将日志级别设定到具体的某个包下# com.xyz.foo.level = SEVERE
1.6.2、修改成更友好点的配置文件
# 自定义Loggercn.zixieqing.handlers=java.util.logging.FileHandler# 自定义Logger日志级别cn.zixieqing.level=SERVER# 屏蔽父logger的日志设置,相当于前面玩的logger.setUseParentHandlers(false);cn.zixieqing.useParentHandlers=false# RootManager默认使用的处理器# 若是想要配置多个处理器,可以使用逗号进行分开,如:java.util.logging.ConsoleHandler,java.util.logging.FileHandlerhandlers= java.util.logging.ConsoleHandler# RootManager的默认日志级别,这是全局的日志级别.level= SERVER# 文件处理器设置# 日志文件的路径# %h/java%u.log h指的是用户目录 - 不分window还是linux# java%u.log是生成的文件名,其中:u相当于是自增,从0开始的# 如:java0.log、java1.log、java2.log......这里生成多少份由java.util.logging.FileHandler.count = 1这个配置决定java.util.logging.FileHandler.pattern = %h/java%u.log# 日志文件的限制 - 50000字节java.util.logging.FileHandler.limit = 50000# 日志文件的数量java.util.logging.FileHandler.count = 1# 日志文件的格式java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter# 控制台处理器设置# 控制台默认级别java.util.logging.ConsoleHandler.level = SERVER# 控制台默认输出格式java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter# 下一次生成的日志文件默认是覆盖了上一此的文件 - 改为在原日志上进行追加java.util.logging.FileHandler.append=true
1.6.3、使用自定义的配置文件
@Test public void test06() throws Exception { // 加载自定义的配置文件 InputStream input = new FileInputStream("D:\test\logging.properties"); // 取得日志管理器对象 LogManager logManager = LogManager.getLogManager(); // 读取自定义的配置文件 logManager.readConfiguration(input); Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest"); logger.severe("severe信息"); logger.warning("warning信息"); logger.info("info信息"); logger.config("config信息"); logger.fine("fine信息"); logger.finer("finer信息"); logger.finest("finest信息"); }
1.7、总结:JUL原理
-
1、初始化LogManager
- LogManager加载logging.properties配置文件
- 添加Logger到LogManager
-
2、从单例的LogManager获取Logger,即:LogManager.getLogManager();
-
3、设置日志级别Level,在打印的过程中使用到了日志记录的LogRecord类,源码找寻如下:
-
1、点击logger.severe("severe信息");中的severe,当然点击其他warning、info、config也是可以进去的,之后会看到如下的代码 public void severe(String msg) { log(Level.SEVERE, msg); }2、点击上述的log(),看到如下的代码 public void log(Level level, String msg) { if (!isLoggable(level)) { return; } // 这里就是目的地 LogRecord lr = new LogRecord(level, msg); doLog(lr); }
-
-
4、Filter作为过滤器提供了日志级别之外更细粒度的控制
-
5、Handler日志处理器,决定日志的输出位置,例如控制台、文件...
-
6、Formatter是用来格式化输出的
2、LOG4J
- 全称:log for java
2.1、LOG4J的组成
- Loggers (日志记录器):控制日志的输出以及输出级别(JUL做日志级别Level)
- Appenders(输出控制器):指定日志的输出方式(输出到控制台、文件等)
- Layout(日志格式化器):控制日志信息的输出格式
2.1.1、Loggers日志记录器
- 负责收集处理日志记录,实例的命名就是类的全限定名,如:
cn.zixieqing.test
,同时:Logger的名字大小写敏感,其命名有继承机制( 和JUL中的父子关系一样,以包路径来区分的 );另外:root logger是所有logger的根 - 上辈所做的日志属性设置,会直接的影响到子辈
// root logger的获取 Logger.getRootLogger();
日志级别
- DEBUG
- INFO
- WARN
- ERROR
- ........
- 大小关系:ERROR > WARN > INFO > DEBUG
- 输出规则:输出日志的规则是:只输出级别不低于设定级别的日志信息
- 如:假设Loggers级别设定为INFO,则INFO、WARN、ERROR级别的日志信息都会输出,而级别比INFO低的DEBUG则不会输出
2.1.2、Appenders输出控制器
- 允许把日志输出到不同的地方,如控制台(Console)、文件(Files)等,可以根据时间或者文件大小产生新的文件,可以以流的形式发送到其它地方等等
- 常用Appenders输出控制器类型:
- ConsoleAppender 将日志输出到控制台
- FileAppender 将日志输出到文件中
- DailyRollingFileAppender 根据指定时间输出到一个新的文件, 将日志输出到一个日志文件
- RollingFileAppender 根据指定文件大小,当文件大小达到指定大小时,会自动把文件改名,产生一个新的文件,将日志信息输出到一个日志文件
- JDBCAppender 把日志信息保存到数据库中
2.2、玩LOG4J
2.2.1、入门
- 依赖
<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
public class Log4jTest01 { @Test public void test01(){ // 加载初始化配置 BasicConfigurator.configure(); // 注意:这个logger是apache下的,前面玩的JUL中的是java。util.logging包下的 Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }}
- 注意加载初始化信息:
BasicConfigurator.configure();
,不加这一句代码就报错 - 没有添加Appenders输出控制器,加上不报错是因为:源码中有这样一句代码rootManager.addAppenders( XxxxAppender( PatternLayout layout ) )
,configure源码如下:
public static void configure() { Logger root = Logger.getRootLogger(); root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n"))); }
LOG4J日志级别
- Log4j提供了8个级别的日志输出,分别为如下级别:
- ALL 最低等级 用于打开所有级别的日志记录
- TRACE 程序推进下的追踪信息,这个追踪信息的日志级别非常低,一般情况下是不会使用的
- DEBUG 指出细粒度信息事件对调试应用程序是非常有帮助的,主要是配合开发,在开发过程中打印一些重要的运行信息,在没有进行设置的情况下,默认的日志输出级别
- INFO 消息的粗粒度级别运行信息
- WARN 表示警告,程序在运行过程中会出现的有可能会发生的隐形的错误
- 注意,有些信息不是错误,但是这个级别的输出目的就是为了给程序员以提示
- ERROR 系统的错误信息,发生的错误不影响系统的运行
- 一般情况下,如果不想输出太多的日志,则使用该级别即可
- FATAL 表示严重错误,它是那种一旦发生系统就不可能继续运行的严重错误
- 如果这种级别的错误出现了,表示程序可以停止运行了
- OFF 最高等级的级别,用户关闭所有的日志记录
2.2.2、分析源码
BasicConfigurator.configure(); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息");
分析 BasicConfigurator.configure();点击configure()
- 得到的代码如下
public static void configure() { Logger root = Logger.getRootLogger(); root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n"))); }
-
从中可以得到几个信息:
- 1、创建根节点的对象
Logger root = Logger.getRootLogger();
- 2、根节点添加了
ConsoleAppender对象(表示默认打印到控制台,自定义的格式化输出PatternLayout)
- 1、创建根节点的对象
-
那么想要自定义配置文件来实现上述源代码的功能呢?通过上面这个源代码分析,我们需要具备如下的条件:
- 们的配置文件需要提供Logger、Appender、Layout这3个组件信息
分析Logger logger = Logger.getLogger(Log4jTest01.class);点击getLogger()
public static Logger getLogger(Class clazz) { return LogManager.getLogger(clazz.getName()); }
- 发现:
LogManager.getLogger(clazz.getName());
,其中:LogManager
j就是日志管理器
查看LogManager
- 首先看到如下信息,一堆常量
public static final String DEFAULT_CONFIGURATION_FILE = "log4j.properties"; static final String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml"; /** @deprecated */ public static final String DEFAULT_CONFIGURATION_KEY = "log4j.configuration"; /** @deprecated */ public static final String CONFIGURATOR_CLASS_KEY = "log4j.configuratorClass"; /** @deprecated */ public static final String DEFAULT_INIT_OVERRIDE_KEY = "log4j.defaultInitOverride"; private static Object guard = null; private static RepositorySelector repositorySelector;
- 这些东西代表的就是不同形式(不同后缀名)的配置文件,其中
log4j.properties属性
使我们最常使用的,因为它语法简单、使用方便
log4j.properties的加载时机
加载 - 那就是static
观察LogManager中的代码,找到其中的静态代码块static
- 发现如下的一堆源码
static { Hierarchy h = new Hierarchy(new RootLogger(Level.DEBUG)); repositorySelector = new DefaultRepositorySelector(h); String override = OptionConverter.getSystemProperty("log4j.defaultInitOverride", (String)null); if (override != null && !"false".equalsIgnoreCase(override)) { LogLog.debug("Default initialization of overridden by log4j.defaultInitOverrideproperty."); } else { String configurationOptionStr = OptionConverter.getSystemProperty("log4j.configuration", (String)null); String configuratorClassName = OptionConverter.getSystemProperty("log4j.configuratorClass", (String)null); URL url = null; if (configurationOptionStr == null) { url = Loader.getResource("log4j.xml"); if (url == null) { // 前面就是文件格式的一堆判断,这里才是log4j.propertie格式做的事情 url = Loader.getResource("log4j.properties"); } } else { try { url = new URL(configurationOptionStr); } catch (MalformedURLException var7) { url = Loader.getResource(configurationOptionStr); } } if (url != null) { LogLog.debug("Using URL [" + url + "] for automatic log4j configuration."); try { // 这里又是一个信息:selectAndConfigure()翻译就是选择配置文件 OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository()); } catch (NoClassDefFoundError var6) { LogLog.warn("Error during default initialization", var6); } } else { LogLog.debug("Could not find resource: [" + configurationOptionStr + "]."); } } }
-
从源码中,发现
url = Loader.getResource("log4j.properties");
- 从这句代码得到的信息:系统默认是从当前的类路径下找到
log4j.properties
,而若是maven工程,那么:就应该在resources路径下去找
- 从这句代码得到的信息:系统默认是从当前的类路径下找到
-
同时在上面的源码中发现
OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository());
查看selectAndConfigure()
- 发现如下源码
public static void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) { Configurator configurator = null; String filename = url.getFile(); if (clazz == null && filename != null && filename.endsWith(".xml")) { clazz = "org.apache.log4j.xml.DOMConfigurator"; } if (clazz != null) { LogLog.debug("Preferred configurator class: " + clazz); configurator = (Configurator)instantiateByClassName(clazz, Configurator.class, (Object)null); if (configurator == null) { LogLog.error("Could not instantiate configurator [" + clazz + "]."); return; } } else { // 有用信息在这里,即 new PropertyConfigurator();创建了一个properties配置对象 configurator = new PropertyConfigurator(); } ((Configurator)configurator).doConfigure(url, hierarchy); }
查看PropertyConfigurator类
- 首先看到的就是如下的常量信息
static final String CATEGORY_PREFIX = "log4j.category."; static final String LOGGER_PREFIX = "log4j.logger."; static final String FACTORY_PREFIX = "log4j.factory"; static final String ADDITIVITY_PREFIX = "log4j.additivity."; static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory"; // 这是一个重要信息 static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"; // 这也是一个重要信息 static final String APPENDER_PREFIX = "log4j.appender."; static final String RENDERER_PREFIX = "log4j.renderer."; static final String THRESHOLD_PREFIX = "log4j.threshold"; private static final String THROWABLE_RENDERER_PREFIX = "log4j.throwableRenderer"; private static final String LOGGER_REF = "logger-ref"; private static final String ROOT_REF = "root-ref"; private static final String APPENDER_REF_TAG = "appender-ref"; public static final String LOGGER_FACTORY_KEY = "log4j.loggerFactory"; private static final String RESET_KEY = "log4j.reset"; private static final String INTERNAL_ROOT_NAME = "root";
- 通过前面的基础,从这源码中,发现有两个信息是要进行配置的
static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";static final String APPENDER_PREFIX = "log4j.appender.";
- 那么这二者是怎么进行配置的?
找寻static final String APPENDER_PREFIX = "log4j.appender."中的appender配置方式
直接在当前源码页面搜索appender
- 发现如下的源代码
Appender parseAppender(Properties props, String appenderName) { Appender appender = this.registryGet(appenderName); if (appender != null) { LogLog.debug("Appender "" + appenderName + "" was already parsed."); return appender; } else { // 重要信息就在这里,这里告知了一件事:上面找到的log4j.appender.配置方式为如下的方式 String prefix = "log4j.appender." + appenderName; // 这也是重要信息,layout日志格式化的配置方式 String layoutPrefix = prefix + ".layout"; appender = (Appender)OptionConverter.instantiateByKey(props, prefix, Appender.class, (Object)null); if (appender == null) { LogLog.error("Could not instantiate appender named "" + appenderName + ""."); return null; } else { appender.setName(appenderName); if (appender instanceof OptionHandler) { if (appender.requiresLayout()) { Layout layout = (Layout)OptionConverter.instantiateByKey(props, layoutPrefix, Layout.class, (Object)null); if (layout != null) { appender.setLayout(layout); LogLog.debug("Parsing layout options for "" + appenderName + ""."); PropertySetter.setProperties(layout, props, layoutPrefix + "."); LogLog.debug("End of parsing for "" + appenderName + ""."); } } String errorHandlerPrefix = prefix + ".errorhandler"; String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props); if (errorHandlerClass != null) { ErrorHandler eh = (ErrorHandler)OptionConverter.instantiateByKey(props, errorHandlerPrefix, ErrorHandler.class, (Object)null); if (eh != null) { appender.setErrorHandler(eh); LogLog.debug("Parsing errorhandler options for "" + appenderName + ""."); this.parseErrorHandler(eh, errorHandlerPrefix, props, this.repository); Properties edited = new Properties(); String[] keys = new String[]{errorHandlerPrefix + "." + "root-ref", errorHandlerPrefix + "." + "logger-ref", errorHandlerPrefix + "." + "appender-ref"}; Iterator iter = props.entrySet().iterator(); while(true) { if (!iter.hasNext()) { PropertySetter.setProperties(eh, edited, errorHandlerPrefix + "."); LogLog.debug("End of errorhandler parsing for "" + appenderName + ""."); break; } Entry entry = (Entry)iter.next(); int i; for(i = 0; i < keys.length && !keys[i].equals(entry.getKey()); ++i) { } if (i == keys.length) { edited.put(entry.getKey(), entry.getValue()); } } } } PropertySetter.setProperties(appender, props, prefix + "."); LogLog.debug("Parsed "" + appenderName + "" options."); } this.parseAppenderFilters(props, appenderName, appender); this.registryPut(appender); return appender; } } }
-
通过上述的源码,发现配置
log4j.appender.
的方式:log4j.appender.+appenderName
- 其中:appenderName就是输出控制器名字
-
继而:推导出
log4j.properties
配置文件中的一个配置项appender输出方式为:log4j.appender.+appenderName=某一种输出控制器名字
- 其中:输出控制器名字在前面一开始就接触过了
- 因此
Log4j.properties
的appender输出方式配置方式举例就是log4j.appender.console=org.apache.log4j.ConsoleAppender
- 同样道理,通过第二句代码
String layoutPrefix = prefix + ".layout";
,也就知道了layout输出格式的配置方式
- layout日志输出格式配置举例:
log4j.appender.console.layout=org.log4j.SimpleLayout
小小总结一波:log4j.properties配置文件中的appender输出控制器 和 layout日志输出格式的配置方式
log4j.appender,console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.log4j.SimpleLayout
继续找第二个配置static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"中的rootLogger配置方式
通过log4j.rootLogge进行搜索
- 发现如下的方法
void configureRootCategory(Properties props, LoggerRepository hierarchy) { String effectiveFrefix = "log4j.rootLogger"; String value = OptionConverter.findAndSubst("log4j.rootLogger", props); if (value == null) { value = OptionConverter.findAndSubst("log4j.rootCategory", props); effectiveFrefix = "log4j.rootCategory"; } if (value == null) { LogLog.debug("Could not find root logger information. Is this OK?"); } else { Logger root = hierarchy.getRootLogger(); synchronized(root) { // 这里面执行了这个方式 this.parseCategory(props, root, effectiveFrefix, "root", value); } } }
查看parseCategory()
- 在这里找到了想要的配置方式
void parseCategory(Properties props, Logger logger, String optionKey, String loggerName, String value) { LogLog.debug("Parsing for [" + loggerName + "] with value=[" + value + "]."); // 配置方式就在这里,这个操作的意思就是:表示要以逗号的方式来切割字符串,证明了log4j.rootLogger的取值,可以有多个值,使用逗号进行分隔 StringTokenizer st = new StringTokenizer(value, ","); if (!value.startsWith(",") && !value.equals("")) { if (!st.hasMoreTokens()) { return; } // 把字符串通过逗号切割之后,第一个值的用途就在这里 - levelStr、level,即:切割后的第一个值是日志的级别 String levelStr = st.nextToken(); LogLog.debug("Level token is [" + levelStr + "]."); if (!"inherited".equalsIgnoreCase(levelStr) && !"null".equalsIgnoreCase(levelStr)) { logger.setLevel(OptionConverter.toLevel(levelStr, Level.DEBUG)); } else if (loggerName.equals("root")) { LogLog.warn("The root logger cannot be set to null."); } else { logger.setLevel((Level)null); } LogLog.debug("Category " + loggerName + " set to " + logger.getLevel()); } logger.removeAllAppenders(); // 字符串切割之后的第一个值是level日志级别,而剩下的值的用途就在这里 while(st.hasMoreTokens()) { // 通过这句代码得知:第2 - 第n个值,就是我们配置的其他信息,这个信息就是appenderName String appenderName = st.nextToken().trim(); if (appenderName != null && !appenderName.equals(",")) { LogLog.debug("Parsing appender named "" + appenderName + ""."); Appender appender = this.parseAppender(props, appenderName); if (appender != null) { logger.addAppender(appender); } } } }
- 通过上述的代码分析,得知
log4j.rootLogger
的配置方式为:log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....
- 表示可以同时在根节点上配置多个日志输出的途径
2,2,3、最基本的log4j.properties配置
- 通过前面的源码分析之后,得出
BasicConfigurator.configure();
替代品的properties配置如下:
# rootLogger所有logger的根配置 - log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....# 这里的例子没用日志输出路径,这个日志输出路径后续再加log4j.rootLogger=debug,console# appender输出控制器配置 log4j.appender.+appenderName=某一种输出类型 - 采用Console控制台的方式举例log4j.appender.console=org.apache.log4j.ConsoleAppender# 输出的格式配置log4j.appender.+appenderName+layout=某种layout格式类型log4j.appender.console.layout=org.apache.log4j.SimpleLayout
测试
// 注掉这一句就可以了,这句代码的配置由上面的自定义配置进行代替了 //BasicConfigurator.configure(); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息");
2.2.3、打开日志输出的详细信息
@Test public void test03(){ /* 通过Logger中的开关 打开日志输出的详细信息 查看LogManager类中的方法getLoggerRepository() 找到代码LogLog.debug(msg, ex); LogLog会使用debug级别的输出为我们展现日志输出详细信息 Logger是记录系统的日志,那么LogLog就是用来记录Logger的日志 进入到LogLog.debug(msg, ex);方法中 通过代码:if (debugEnabled && !quietMode) 观察到if判断中的这两个开关都必须开启才行 !quietMode是已经启动的状态,不需要我们去管 debugEnabled默认是关闭的 所以我们只需要设置debugEnabled为true就可以了 */ // 开启 debugEnabled LogLog.setInternalDebugging(true); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
- 若开启debugEnabled,那么输出信息就是如下的样子( 如下是举的一个例子而已)
0 [main] FATAL cn.zixieqing.HotelJavaApplicationTests - fatal信息1 [main] ERROR cn.zixieqing.HotelJavaApplicationTests - error信息1 [main] WARN cn.zixieqing.HotelJavaApplicationTests - warn信息1 [main] INFO cn.zixieqing.HotelJavaApplicationTests - info信息1 [main] DEBUG cn.zixieqing.HotelJavaApplicationTests - debug信息
- 开启之后,信息就会更全
log4j: Trying to find [log4j.xml] using context classloader sun.misc.Launcher$AppClassLoader@18b4aac2.log4j: Trying to find [log4j.xml] using sun.misc.Launcher$AppClassLoader@18b4aac2 class loader.log4j: Trying to find [log4j.xml] using ClassLoader.getSystemResource().log4j: Trying to find [log4j.properties] using context classloader sun.misc.Launcher$AppClassLoader@18b4aac2.log4j: Trying to find [log4j.properties] using sun.misc.Launcher$AppClassLoader@18b4aac2 class loader.log4j: Trying to find [log4j.properties] using ClassLoader.getSystemResource().log4j: Could not find resource: [null].0 [main] FATAL cn.zixieqing.HotelJavaApplicationTests - fatal信息0 [main] ERROR cn.zixieqing.HotelJavaApplicationTests - error信息1 [main] WARN cn.zixieqing.HotelJavaApplicationTests - warn信息1 [main] INFO cn.zixieqing.HotelJavaApplicationTests - info信息1 [main] DEBUG cn.zixieqing.HotelJavaApplicationTests - debug信息
2.2.4、自定义输出格式 patternLayout
- 自定义配置的也就是
Layout
而已,而自定义就是玩的PatternLayout
,这个类有一个setConversionPattern()
方法,查看这个方法的源码
public void setConversionPattern(String conversionPattern) { this.pattern = conversionPattern; this.head = this.createPatternParser(conversionPattern).parse(); }
- 从中发现,需要配置的就是
String conversionPattern
,因此:在log4j.properties
配置文件中添加上conversionPattern
属性配置即可,当然:这个属性配置遵循一定的写法,写法如下:
%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 输出代码中的行号 %% 输出一个 "%" 字符 可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式 [p]:[]中必须有10个字符,由空格来进行补齐,信息右对齐 [%-10p]:[]中必须有10个字符,由空格来进行补齐,信息左对齐,应用较广泛 上述举例:[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
- 修改了
log4j.properties
的配置如下( 做的修改就是最后两个配置项 )
# rootLogger所有logger的根配置 - log4j.rootLogger=日志级别,appenderName1,appenderName2,appenderName3....log4j.rootLogger=debug,console# appender输出控制器配置 log4j.appender.+appenderName=某一种输出类型 - 采用Console控制台的方式举例log4j.appender.console=org.apache.log4j.ConsoleAppender# 输出的格式配置log4j.appender.+appenderName+layout=某种layout格式类型 - 注意:这里类型改了,是PatternLayout,即自定义log4j.appender.console.layout=org.apache.log4j.PatternLayout# 编写自定义输出格式( 直接把上面举例的拿过来 ) - 注意:加上了刚刚说的conversionPattern属性log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
- 测试代码
public void test04(){ LogLog.setInternalDebugging(true); Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
2.2.5、将日志输出到文件中
2.2.5.1、改造log4j.properties文件
# rootLogger所有logger的根配置 - 这里再加一个filelog4j.rootLogger=debug,console,file# 控制台的appender输出控制器配置log4j.appender.console=org.apache.log4j.ConsoleAppender# 采用自定义控制台输出的格式log4j.appender.console.layout=org.apache.log4j.PatternLayout# 编写控制台自定义输出格式log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n# 再来一份,变为file的配置 - 把console改为file即可log4j.appender.file=org.apache.log4j.FileAppenderlog4j.appender.file.layout=org.apache.log4j.PatternLayoutlog4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
2.2.5.2、指定文件输出位置 及 字符编码设置
- 查看
FileAppender
源码,首先看到的是四个属性
// 表示日志文件是否采用内容追加的方式 - 源码中有一个构造方法,这个的默认值是true protected boolean fileAppend; protected String fileName; protected boolean bufferedIO; // 日志文件的大小 - 源码的构造方法中默认值是8192 protected int bufferSize; // 构造方法源码 public FileAppender() { this.fileAppend = true; this.fileName = null; this.bufferedIO = false; this.bufferSize = 8192; }
- 在
FlieAppender
中还有一个setFile()
的方法,得知这个就是设置文件的方法 ,也就是文件日志文件存放路径位置
public void setFile(String file) { String val = file.trim(); this.fileName = val; }
- 这里面需要传一个file参数进去,因此:通过
MyBatis
的知识就知道log4j.properties
配置中的这个对应属性就是file了( 截掉set,得到属性名 )
# rootLogger所有logger的根配置 - 这里再加一个filelog4j.rootLogger=debug,console,file# 控制台的appender输出控制器配置log4j.appender.console=org.apache.log4j.ConsoleAppender# 采用自定义控制台输出的格式log4j.appender.console.layout=org.apache.log4j.PatternLayout# 编写控制台自定义输出格式log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n# 再来一份,变为file的配置 - 把console改为file即可log4j.appender.file=org.apache.log4j.FileAppenderlog4j.appender.file.layout=org.apache.log4j.PatternLayoutlog4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n# 日志文件保存路径 - 注:前一个file为自定义的appenderName;后一个file是日志文件输出路径的属性,要是怕看错眼,可以把后者换为File大写也没错log4j.appender.file.file=D:log4jzixieqinglog4j.log
- 现在看
FileAppender
的父类WriterAppender
,去看字符编码设置
protected boolean immediateFlush; // 这个属性就是编码设置 protected String encoding; protected QuietWriter qw;
- 因此:加上日志文件输出路径和字符编码之后的
log4.properties
配置为:
# 控制台日志输出设置log4j.rootLogger=debug,console,filelog4j.appender.console=org.apache.log4j.ConsoleAppenderlog4j.appender.console.layout=org.apache.log4j.PatternLayoutlog4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n# 日志文件输出设置log4j.appender.file=org.apache.log4j.FileAppenderlog4j.appender.file.layout=org.apache.log4j.PatternLayoutlog4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%nlog4j.appender.file.file=D:log4jzixieqinglog4j.loglog4j.appender.file.encoding=UTF-8
2.2.5.3、拆分日志文件
-
一份日志不可能用于一直记录下去,那样的话,日志文件体积就太大了
-
继续查看
FileAppender
源码,看实现类
2.2.5.3.1、RollingFileAppender实现类
- 这个玩意儿就是利用文件大小来进行日志拆分
- 源码中有两个属性需要关注
// 达到多大文件时进行日志拆分 protected long maxFileSize = 10485760L; // 一共能够拆分出多少份日志文件 protected int maxBackupIndex = 1;
- 因此:现在将
log4j.properties
配置文件修改一下即可实现日志根据文件大小进行拆分
# 注意:记得在log4j.rootLoger=debug,console,file这一句中加上下面的这个文件大小进行拆分的配置# 如:log4j.rootLoger=debug,console,rollingFilelog4j.appender.rollingFile=org.apache.log4j.RollingFileAppenderlog4j.appender.rollingFile.layout=org.apache.log4j.PatternLayoutlog4j.appender.rollingFile.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%nlog4j.appender.rollingFile.file=D:log4jzixieqinglog4j.log# 文件多大时进行日志拆分log4j.appender.rollingFile.maxFileSize=10MB# 最多可以拆分多少份log4j.appender.rollingFile.maxBackupIndex=50
2.2.5.3.2、DailyRollingFileAppender实现类 - 建议用
- 这个东西就是根据时间来进行日志拆分,看源码,有这么一个属性
// 时间格式,默认值就是如下的天 private String datePattern = "'.'yyyy-MM-dd";
- 在
log4j.properties
配置文件中修改成如下的配置即可使用
# 一样的,要使用这种方式:记得在log4j.rootLogger=debug,console,file这一句中加上下面的这个文件大小进行拆分的配置# 如:log4j.rootLogger=debug,console,dateRollingFilelog4j.appender.dateRollingFile=org.apache.log4j.DailyRollingFileAppenderlog4j.appender.dateRollingFile.layout=org.apache.log4j.PatternLayoutlog4j.appender.dateRollingFile.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%nlog4j.appender.dateRollingFile.file=D:log4jzixieqinglog4j.log# 加上时间格式 - 下面的值根据实际情况即可log4j.appender.dateRollingFile.datePattern='.'yyyy-MM-dd
2.2.6、日志持久化到数据库
表基础字段
CREATE TABLE tbl_log( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) DEFAULT NULL COMMENT '项目名称', createTime varchar(100) DEFAULT NULL COMMENT '创建时间', level varchar(10) DEFAULT NULL COMMENT '日志级别', category varchar(100) DEFAULT NULL COMMENT '所在类的全路径', fileName varchar(100) DEFAULT NULL COMMENT '文件名称', message varchar(255) DEFAULT NULL COMMENT '日志消息', PRIMARY KEY(id))
项目MySQL驱动
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> <scope>runtime</scope> </dependency>
log4.properties配置
# 配置appender输出方式 输出到数据库表 - 注意:在rootManager中加上logDB,如:log4j.rootLogger=debug,console,logDBlog4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppenderlog4j.appender.logDB.layout=org.apache.log4j.PatternLayoutlog4j.appender.logDB.Driver=com.mysql.jdbc.Driverlog4j.appender.logDB.URL=jdbc:mysql://localhost:3306/testlog4j.appender.logDB.User=rootlog4j.appender.logDB.Password=072413log4j.appender.logDB.Sql=INSERT INTO tbl_log(name,createTime,level,category,fileName,message) values('project_log','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%m')
测试代码
@Test public void test07(){ Logger logger = Logger.getLogger(Log4jTest01.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
2.2.7、自定义logger
- 前面配置文件中使用的都是
rootManager
的logger,接下来就自定义一个logger,看PropertyConfigurator
类的源码,它里面有一个属性
// 自定义logger配置的写法 - 这后面拼接的就是自定义的logger名字 static final String LOGGER_PREFIX = "log4j.logger.";
- 其中:上述说的自定义logger名字遵循父子关系,也就是包关系
如:cn.zixieqing.log4j.test.Log4jTest01 它的父logger就是上层的路径或者是更上层的路径 例如: cn.zixieqing.log4j.test cn.zixieqing.log4j ... cn
修改log4j.properties
# 根logger,输出级别是trace,在console控制台进行输出log4j.rootLogger=trace,console# 自定义logger,级别为info,在file文件中输出log4j.logger.cn.zixieqing.log4j.test=info,file# 自定义logger,是apache的,级别为errorlog4j.logger.org.apache=error
自定义logger的注意点
- 如果根节点的logge( 即:rootManager ) 和 自定义logger配置的输出位置是不同的,则取二者的并集,配置的位置都会进行输出操作
- 如果二者配置的日志级别不同,以我们自定义logger的输出级别为主
2.2.8、一份简单的log4j.properties配置
log4j.rootLogger=DEBUG,console,file #控制台输出的相关设置 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.Threshold=DEBUG log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%c]-%m%n #文件输出的相关设置 log4j.appender.file = org.apache.log4j.RollingFileAppender log4j.appender.file.File=./1og/zixieqing.log log4j.appender.file.MaxFileSize=10mb log4j.appender.file.Threshold=DEBUG log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]‰m%n #日志输出级别 log4j.logger.org.mybatis=DEBUG log4j.logger.java.sq1=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
3、JCL
-
全称为Jakarta Commons Logging,是Apache提供的一个通用日志AP
-
注意:这个玩意儿本身没有记录日志的实现功能,而是相当于一个门面。我们可以自由选择第三方的日志组件( log4j、JUL )作为具体实现,common-logging会通过动态查找的机制,在程序运行时自动找出真正使用的日志库
JCL的组成
- JCL 有两个基本的抽象类
- Log:日志记录器
- LogFactory:日志工厂(负责创建Log实例)
3.1、玩JCL
依赖
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
测试
@Test void jclQuickStartTest() { // LogFactory是org.apache.commons.logging.LogFactory Log log = LogFactory.getLog(JCLTest01.class); log.info("info信息"); }
- 运行之后看效果会发现:输出格式是JUL格式
- 即:如果没有任何第三方日志框架的时候,默认使用的就是JUL
3.2、引入log4j
<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
- 然后使用
log4j.properties
配置文件 - 当再次进行测试时就会变成log4j的输出配置
JCL源码分析
Log接口的4个实现类 JDk13 JDK14 正常java.util.logging Log4j 我们集成的log4j Simple JCL自带实现类 (1)查看Jdk14Logger证明里面使用的是JUL日志框架 - 看import引入的就可以得知 (2)查看Log4JLogger证明里面使用的是Log4j日志框架 - 看import引入的就可以得知 (3)观察LogFactory,看看如何加载的Logger对象 这是一个抽象类,无法实例化 需要观察其实现类LogFactoryImpl (4)观察LogFactoryImpl 真正加载日志实现使用的就是这个实现类LogFactoryImpl (5)进入getLog - 采用打断点debug能够更清晰地看清楚 进入getInstance 找到instance = this.newInstance(name);,继续进入 找到instance = this.discoverLogImplementation(name); 表示发现一个日志的实现 for(int i = 0; i < classesToDiscover.length && result == null; ++i) { result = this.createLogFromClass(classesToDiscover[i], logCategory, true); } 遍历我们拥有的日志实现框架 遍历的是一个数组,这个数组是按照 log4j jdk14 jdk13 SimpleLogger 的顺序依次遍历 表示的是,第一个要遍历的就是log4j,如果有log4j则执行该日志框架 如果没有,则遍历出来第二个,使用jdk14的JUL日志框架 以此类推 result = this.createLogFromClass(classesToDiscover[i], logCategory, true); 表示帮我们创建Logger对象 在这个方法中,我们看到了 c = Class.forName(logAdapterClassName, true, currentCL); 是取得该类型的反射类型对象 使用反射的形式帮我们创建logger对象 constructor = c.getConstructor(this.logConstructorSignature);
4、SLF4J
- 这也是一个日志门面( 门面模式 / 外观模式 ),其核心为:外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用
常见的日志门面和日志实现
-
常见的日志实现:JUL、log4j、logback、log4j2
-
常见的日志门面 :JCL、slf4j
-
出现顺序 :log4j -->JUL-->JCL--> slf4j --> logback --> log4j2
了解SLF4J
- 全称:Simple Logging Facade For Java,简单日志门面
- 主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等
- SLF4J最重要的两个功能就是对于日志框架的绑定以及日志框架的桥接
4.1、玩SLF4J
4.1.1、快速上手
准备知识
- SLF4J对日志的级别划分
trace、debug、info、warn、error五个级别 trace:日志追踪信息 debug:日志详细信息 info:日志的关键信息 默认打印级别 warn:日志警告信息 error:日志错误信息
- 在没有任何其他日志实现框架集成的基础之上,slf4j使用的就是自带的框架slf4j-simple
- 注意点:slf4j-simple也必须以单独依赖的形式导入进来
<!--slf4j 自带的简单日志实现 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.25</version> </dependency>
入门
- 依赖
<!--slf4j 核心依赖--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!--slf4j 自带的简单日志实现 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.25</version> </dependency>
- 测试代码
@Test public void test01(){ // 是slf4j包下的 Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); logger.trace("trace信息"); logger.debug("debug信息"); logger.info("info信息"); logger.warn("warn信息"); logger.error("error信息"); }
4.1.2、动态信息输出
- 本质:使用占位符
- 有些时候输出的日志信息,需要我们搭配动态的数据,这些数据有可能是信息,有可能是数据库表中的数据
@Test public void test02(){ Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); String name = "zs"; int age = 23; /*字符串拼接的形式:logger.info("学生信息-姓名:"+name+";年龄:"+age); 使用JCL的形式:logger.info("学生信息-姓名:{},年龄:{}",new Object[]{name,age}); 这上面两者虽然都可以做到相应的输出,但是:麻烦 */ // 使用SLF4J的形式 logger.info("学生信息-姓名:{},年龄:{}",name,age); }
- 注意点:如果后面拼接的字符串是一个对象,那么{}并不能充当占位,进行字符串拼接
4.2.3、输出异常信息
@Test public void test03(){ /* 日志对于异常信息的处理 一般情况下,我们在开发中的异常信息,都是记录在控制台上(我们开发环境的一种日志打印方式) 我们会根据异常信息提取出有用的线索,来调试bug 但是在真实生产环境中(项目上线),对于服务器或者是系统相关的问题 在控制台上其实也会提供相应的异常或者错误信息的输出 但是这种错误输出方式(输出的时间,位置,格式...)都是服务器系统默认的 我们可以通过日志技术,选择将异常以日志打印的方式,进行输出查看 输出的时间,位置(控制台,文件),格式,完全由我们自己去进行定义 */ Logger logger = LoggerFactory.getLogger(SLF4JTest01.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { // e.printStackTrace(); logger.info("XXX类中的XXX方法出现了异常,请及时关注信息"); // e是引用类型对象,不能根前面的{}做有效的字符串拼接 // logger.info("具体错误是:{}",e); // 我们不用加{},直接后面加上异常对象e即可 - 这是利用了重载方法info(String message, Throwable throwable ) logger.info("具体错误是:",e); } }
4.2.4、SLF4J与日志绑定
- 官网中有一张图,官网地址:https://www.slf4j.org/
-
图中分为了三部分
- 1、在没有绑定任何日志实现的基础之上,日志是不能够绑定实现任何功能的
- slf4j-simple是slf4j官方提供的
- 使用的时候,也是需要导入依赖,自动绑定到slf4j门面上
- 如果不导入,slf4j 核心依赖是不提供任何实现的
- 2、logback和simple(包括nop)
- 都是slf4j门面时间线后面提供的日志实现,所以API完全遵循slf4j进行的设计
- 只需要导入想要使用的日志实现依赖,即可与slf4j无缝衔接
- 注意:nop虽然也划分到实现中了,但是它是指不实现日志记录
- 3、log4j和JUL
- 都是slf4j门面时间线前面的日志实现,所以API不遵循slf4j进行设计
- 通过适配桥接的技术,完成的与日志门面的衔接
- 1、在没有绑定任何日志实现的基础之上,日志是不能够绑定实现任何功能的
4.2.5、绑定logback
依赖
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency>
测试
public class SLF4JTest { @Test public void bingingLogTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { logger.info("具体错误是:",e); } }}
结果
10:50:15.391 [main] INFO com.zixieqing.SLF4JTest - 具体错误是:java.lang.ClassNotFoundException: aaa at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) .......
- 这种就看起来很清爽,如果加上
slf4j-simple
那输出结果又是另一回事,测试跳过 - 上面这种就是不用去管底层到底采用的是哪一种日志实现,可是上面的源代码完全没有改变,照常写,这就是日志门面的好处
4.2.6、slf4j-nop禁止日志打印
依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!-- 注意:有坑儿,和依赖导入顺序有关 如·:要是将这个依赖放到logback的下面,那么屁用都没有 但是:把这个依赖放在logback的前面就可以实现日志禁止打印了 原因:在slf4j环境下,要是同时出现了多个日志实现,默认使用先导入的日志实现 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>1.7.30</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency>
测试
@Test public void slf4jNopTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); try { Class.forName("aaa"); } catch (ClassNotFoundException e) { logger.info("具体错误是:",e); } }
结果
SLF4J: Class path contains multiple SLF4J bindings.SLF4J: Found binding in [jar:file:/D:/install/maven/apache-maven-3.6.1/maven-repo/org/slf4j/slf4j-nop/1.7.30/slf4j-nop-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class]SLF4J: Found binding in [jar:file:/D:/install/maven/apache-maven-3.6.1/maven-repo/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.SLF4J: Actual binding is of type [org.slf4j.helpers.NOPLoggerFactory]
- 日志内容的相关打印就没了
- 注意:如果想要让nop发挥效果,禁止所有日志的打印,那么就必须要将slf4j-nop的依赖放在所有日志实现依赖的上方
4.2.7、绑定log4j
- 玩这个就需要注意日志框架时间线的问题了
- 出现顺序 :log4j -->JUL-->JCL--> slf4j --> logback --> log4j2
- 也就是在slf4j之后出现的( 如:logback、log4j2 ),这些都准许slf4j的规范,所以直接导入对应的依赖之后就可以使用,但是在slf4j之前的,并没有预料到会出现后续这些规范嘛,而slf4j想要绑定log4j就需要一个
slf4j-log4j12
的适配器
玩玩绑定log4j
依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!--slf4j和log4j的适配器 - 想要slf4j能够绑定log4j就需要这个--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.30</version> </dependency>
测试
@Test public void slf4jBindingLog4jTest() { Logger logger = LoggerFactory.getLogger(SLF4JTest.class); logger.info("info信息"); }
结果
# 虽然没有输出消息,但是有如下这个话就足够了No appenders could be found for logger# 这表示没有appender嘛,加上log4j.properties配置文件就可以使用了log4j:WARN No appenders could be found for logger (com.zixieqing.SLF4JTest).log4j:WARN Please initialize the log4j system properly.log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
其他日志框架的绑定就看官网的那张图,在那张图中对绑定谁时导入哪一个依赖都有备注
4.2.8、slf4j源码分析流程
进入到getLogger 看到Logger logger = getLogger(clazz.getName()); 进入重载的getLogger ILoggerFactory iLoggerFactory = getILoggerFactory(); 用来取得Logger工厂实现的方法 进入getILoggerFactory() 看到以双重检查锁的方式去做判断 执行performInitialization(); 工厂的初始化方法 进入performInitialization() bind()就是用来绑定具体日志实现的方法 进入bind() 看到Set集合Set<URL> staticLoggerBinderPathSet = null; 因为当前有可能会有N多个日志框架的实现 看到staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); 进入findPossibleStaticLoggerBinderPathSet() 看到创建了一个有序不可重复的集合对象 LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet(); 声明了枚举类的路径,经过if else判断,以获取系统中都有哪些日志实现 看到Enumeration paths; if (loggerFactoryClassLoader == null) { paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH); } else { paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH); } 我们主要观察常量STATIC_LOGGER_BINDER_PATH 通过常量我们会找到类StaticLoggerBinder 这个类是以静态的方式绑定Logger实现的类 来自slf4j-JDK14的适配器 进入StaticLoggerBinder 看到new JDK14LoggerFactory(); 进入JDK14LoggerFactory类的无参构造方法 看到java.util.logging.Logger.getLogger(""); 使用的就是jul的Logger 接着观察findPossibleStaticLoggerBinderPathSet 看到以下代码,表示如果还有其他的日志实现 while(paths.hasMoreElements()) { URL path = (URL)paths.nextElement(); 将路径添加进入 staticLoggerBinderPathSet.add(path); } 回到bind方法 表示对于绑定多实现的处理 reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); 如果出现多日志实现的情况 则会打印 Util.report("Class path contains multiple SLF4J bindings.");
通过源码总结:
在真实生产环境中,slf4j只绑定一个日志实现框架就可以了
绑定多个,默认使用导入依赖的第一个,而且会产生没有必要的警告信息
4.2.9、slf4j日志重构
有这么一个情况:项目原本使用的是log4j日志,但是随着技术的迭代,需要使用另外的日志框架,如:slf4j+logback,因此:此时不可能说去该源码,把所有使用log4j的地方都改成slf4j
- 这种情况,在slf4j官网中就提供了对应的解决方式,就是加个依赖而已,加的东西就叫做桥接器
4.2.9.1、玩一下桥接器
演示bug:先保证项目没有其他任何的干扰,如:另外的依赖、另外日志的代码
依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
log4j.properties配置文件
log4j.rootLogger=debug,consolelog4j.appender.console=org.apache.log4j.ConsoleAppenderlog4j.appender.console.layout=org.apache.lo4j.SimpleLayout
测试代码
package com.zixieqing;import org.apache.log4j.LogManager;import org.apache.log4j.Logger;import org.junit.Test; @Test public void bridgingTest() { // 假设项目用的是log4j Logger logger = LogManager.getLogger(SLF4JTest.class); logger.info("inf信息"); }
此时,项目升级,改用slf4j+logback来当日志
-
1、去掉log4j的依赖
-
2、添加slf4j的桥接组件,所以现在的依赖就变成如下的样子
-
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!--这个就是log4j和slf4j的桥接组件--> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency>
-
要引入什么桥接组件,看要怎么重构,然后根据官网的说明来弄就行
-
测试
- 源代码直接不用动
package com.zixieqing;import org.apache.log4j.LogManager;import org.apache.log4j.Logger;import org.junit.Test; @Test public void bridgingTest() { // 假设项目用的是log4j Logger logger = LogManager.getLogger(SLF4JTest.class); logger.info("inf信息"); }
结果
14:45:09.901 [main] INFO com.zixieqing.SLF4JTest - inf信息
5、LOGBACK
- 这是log4j的作者跳槽之后开发的另一款日志框架,比log4j出色很多
- Logback当前分成三个模块:logback-core,logback- classic和logback-access
- logback-core是其它两个模块的基础模块
- logback-classic是log4j的一个改良版本。此外logback-classic完整实现SLF4J API。可以很方便地更换成其它日志系统如log4j或JDK14 Logging
- logback-access访问模块与Servlet容器集成提供通过Http来访问日志的功能
logback的组成
- Logger: 日志的记录器,主要用于存放日志对象,也可以定义日志类型、级别
- Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等
- Layout: 负责把事件转换成字符串,格式化的日志信息的输出
- 注意点:在Logback中Layout对象被封装在encoder中,即 未来使用的encoder其实就是Layout
logback配置文件的类型
- Logback提供了3种配置文件
- logback.groovy
- logback-test.xml
- logback.xml
- 我们用的一般是xml的格式
logback的日志输出格式
日志输出格式: %-10level 级别 案例为设置10个字符,左对齐 %d{yyyy-MM-dd HH:mm:ss.SSS} 日期 %c 当前类全限定名 %M 当前执行日志的方法 %L 行号 %thread 线程名称 %m或者%msg 信息 %n 换行
5.1、玩logback
5.1、logback的日志级别
error > warn > info > debug > trace 其中:logback的默认日志级别就是debug
5.2、快速上手
依赖
<!-- ogback-classic 中包含了基础模板 ogback-core--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency> <!--logback是日志实现,也需要通过日志门面来集成--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>
测试
public class LogBackTest { @Test public void quickStartTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }}
结果
15:44:27.032 [main] ERROR com.zixieqing.LogBackTest - error信息15:44:27.033 [main] WARN com.zixieqing.LogBackTest - warn信息15:44:27.033 [main] INFO com.zixieqing.LogBackTest - info信息15:44:27.033 [main] DEBUG com.zixieqing.LogBackTest - debug信息
- 得出信息,logback的默认日志级别就是debug
5.3、简单认识logback的配置文件
<?xml version="1.0" encoding="utf-8" ?><!--logback的所有配置都需要在configuration标签中机芯--><configuration> <!--通用属性配置 - 目的:在下面配置需要的地方使用${name}的形式,方便的取得value值 name 起的名字 见名知意即可 value 值 --> <!--配置日志输出格式--> <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L %thread %m%n"/> <!--配置日志输出类型 name 乱取,见名知意即可 class 类型全类路径 --> <appender name = "consoleAppender" class = "ch.qos.logback.core.ConsoleAppender"> <!--配置日志的颜色--> <target> <!--还可以用system.out就是常见的黑白色--> system.err </target> <!--配置日志输出格式,直接引用前面配置的property通用属性--> <encoder class = "ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${pattern}</pattern> </encoder> </appender> <!--配置root Logger--> <root level="DEBUG"> <appender-ref ref="consoleAppender"/> <oot></configuration>
- 其中:
<property name="" value = ""
中vlaue
的参数配置参考
日志输出格式: %-10level 级别 案例为设置10个字符,左对齐 %d{yyyy-MM-dd HH:mm:ss.SSS} 日期 %c 当前类全限定名 %M 当前执行日志的方法 %L 行号 %thread 线程名称 %m或者%msg 信息 %n 换行
测试
public class LogBackTest { @Test public void quickStartTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }}
5.4、将日志输出到文件中
在logback.xml
中加入如下的配置即可
<!--配置全局的文件输出路径--> <property name="filePath" value="D:test"/> <!--定义文件的appender输出类型--> <appender name="fileAppender" class="ch.qos.logback.core.FileAppender"> <!--文件路径 和 文件名 --> <file>${filePath}/logback.log</file> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--文件输出的格式--> <pattern>${pattern}</pattern> </encoder> </appender> <!--记得在root logger中引用一下fileAppender的配置--> <!--配置root Logger--> <root level="DEBUG"> <appender-ref ref="consoleAppender"/> <!--引用配置的文件appender输出配置--> <appender-ref ref="fileAppender"/> <oot>
测试代码
@Test public void fileAppenderTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
通过结果观察会发现:logback的日志文件输出默认是以追加的形式添加新日志内容
5.5、以HTML格式记录日志文件
- 当日志文件不是很多的时候,可以采用这种方式,因为这种HTML里面的样式和格式都可以让logback进行生成,而里面的内容使我们加进去的而已,因此:这样一看就知道,HTML文件占用的内存容量就蛮大了
logback.xml
配置文件编写
<!--定义HTML文件输出格式,本质还是文件,所以class是FileAppender--> <appender name="HTMLFileAppender" class="ch.qos.logback.core.FileAppender"> <file>${filePath}logback.log</file> <!--注意:这里采用的是LayoutWrappingEncoder--> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <!--注意:采用layout标签包裹,格式类型为HTMLLayout--> <layout class="ch.qos.logback.classic.html.HTMLLayout"> <pattern>${pattern}</pattern> </layout> </encoder> </appender> <!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <appender-ref ref="HTMLFileAppender"/> <oot>
测试
@Test public void htmlFileAppenderTest() { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
- 运行程序即可生成HTML文件
5.6、拆分日志 和 归档压缩
<!-- 配置文件的appender 可拆分归档的文件 --> <appender name="roll" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 输入格式 --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${pattern}</pattern> </encoder> <!-- 引入文件位置 --> <file>${logDir}oll_logback.log</file> <!-- 指定拆分规则 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!-- 按照时间和压缩格式声明文件名 压缩格式gz --> <fileNamePattern>${logDir}oll.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern> <!-- 按照文件大小来进行拆分 --> <maxFileSize>1KB</maxFileSize> <ollingPolicy> </appender> <!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <!--<appender-ref ref="HTMLFileAppender"/>--> <appender-ref ref="roll"/> <oot>
- 注意点:只要使用到拆分日志的事情,那么
<fileNamePattern></fileNamePattern>
标签必须有,源码中有说明
在TimeBasedRollingPolicy类中有一个常量 static final String FNP_NOT_SET ="The FileNamePattern option must be set before using TimeBasedRollingPolicy. ";这个常量值告知的结果就是上面的注意点
- 另外:其他有哪些属性,而每一个class对应对应下有哪些标签可以用,那就是class指定值中的属性名,也就是标签名,看源码即可
@Test public void rollFileAppenderTest() { for (int i = 0; i < 1000; i++) { // 这个log4j中的 Logger logger = LoggerFactory.getLogger(LogBackTest.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
5.7、过滤器
<!-- 配置控制台的appender 使用过滤器 --> <appender name="consoleFilterAppender" class="ch.qos.logback.core.ConsoleAppender"> <target> System.out </target> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${pattern}</pattern> </encoder> <!-- 配置过滤器 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <!-- 设置日志的输出级别 --> <level>ERROR</level> <!-- 高于level中设置的级别,则打印日志 --> <onMatch>ACCEPT</onMatch> <!-- 低于level中设置的级别,则屏蔽日志 --> <onMismatch>DENY</onMismatch> </filter> </appender> <!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <!--<appender-ref ref="HTMLFileAppender"/>--> <!--<appender-ref ref="roll"/>--> <!--解开console日志打印logger--> <appender-ref ref="consoleAppender"/> <oot>
- 测试代码照常用,然后在console控制台输出的内容会根据上述配置的filter过滤器罢对应的内容过滤掉,从而不打印出来
5.8、异步日志
<!-- 1、配置异步日志 --> <appender name="asyncAppender" class="ch.qos.logback.classic.AsyncAppender"> <!-- 引用appender配置,即:让什么输出类型的日志进行异步操作,可以选择consoleAppender、fileAppender.... --> <appender-ref ref="fileAppender"/> </appender> <!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <!--<appender-ref ref="HTMLFileAppender"/>--> <!--<appender-ref ref="roll"/>--> <!--解开console日志打印logger--> <!--<appender-ref ref="consoleAppender"/>--> <!--2、在root logger中引用一下配置的异步日志--> <appender-ref ref="asyncAppender"/> <oot>
另外:在上面的1、配置异步日志
里面还有两个配置属性
下面这个配置的是一个阈值,当队列的剩余容量小于这个阈值的时候,当前日志的级别 trace、debug、info这3个级别的日志将被丢弃 设置为0,说明永远都不会丢弃trace、debug、info这3个级别的日志 <discardingThreshold>0</discardingThreshold> 配置队列的深度,这个值会影响记录日志的性能,默认值就是256 <queueSize>256</queueSize> 关于这两个属性,一般情况下,使用默认值即可 这两个属性不要乱配置,会影响系统性能,了解其功能即可
@Test public void asyncAppenderTest(){ Logger logger = LoggerFactory.getLogger(LOGBACKTest01.class); // 日志打印操作 for (int i = 0; i < 100; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } // 系统本身业务相关的其他操作 System.out.println("系统本身业务相关的操作1"); System.out.println("系统本身业务相关的操作2"); System.out.println("系统本身业务相关的操作3"); System.out.println("系统本身业务相关的操作4"); }
5.9、自定义logger
- 还是老样子,自定义logger就是为了替换下面这一堆
<!--配置root Logger--> <root level="DEBUG"> <!--<appender-ref ref="consoleAppender"/>--> <!--<appender-ref ref="fileAppender"/>--> <!--<appender-ref ref="HTMLFileAppender"/>--> <!--<appender-ref ref="roll"/>--> <!--解开console日志打印logger--> <!--<appender-ref ref="consoleAppender"/>--> <!--2、在root logger中引用一下配置的异步日志--> <appender-ref ref="asyncAppender"/> <oot>
自定义logger配置
<!-- additivity="false" 表示不继承rootlogger --> <logger name="com.bjpowernode" level="info" additivity="false"> <!-- 在自定义logger中配置appender --> <appender-ref ref="consoleAppender"/> </logger>
- 测试代码和前面一样,跳过
6、LOG4J2
- 这玩意儿虽然叫log4j2,也就是对log4j做了增强,但是更多的其实是对logback不足做了优化
log4j2的特征
性能提升
- Log4j2包含基于LMAX Disruptor库的下一代异步记录器。在多线程场景中,异步记录器的吞吐量比Log4j 1.x和Logback高18倍,延迟低
自动重新加载配置
- 与Logback一样,Log4j2可以在修改时自动重新加载其配置。与Logback不同,它会在重新配置发生时不会丢失日志事件
高级过滤
- 与Logback一样,Log4j2支持基于Log事件中的上下文数据,标记,正则表达式和其他组件进行过滤
- 此外,过滤器还可以与记录器关联。与Logback不同,Log4j2可以在任何这些情况下使用通用的Filter类
插件架构
- Log4j使用插件模式配置组件。因此,无需编写代码来创建和配置Appender,Layout,Pattern Converter等。在配置了的情况下,Log4j自动识别插件并使用它们
无垃圾机制
- 在稳态日志记录期间,Log4j2 在独立应用程序中是无垃圾的,在Web应用程序中是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应性能
log4j2的最佳搭配
- 采用
log4j2 + slf4j
的方式
6.1、玩log4j2
6.1.1、快速上手
依赖
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.17.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>
测试
public class Log4j2Test { @Test public void quickStartTest() { // 是org.apache.logging.log4j包下的 Logger logger = LogManager.getLogger(Log4j2Test.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); }}
结果
21:18:43.909 [main] FATAL com.zixieqing.Log4j2Test - fatal信息21:18:43.911 [main] ERROR com.zixieqing.Log4j2Test - error信息
- 得出结论:log4j2的默认级别是error级别
6.1.2、简单了解log4j2的配置文件
- log4j2是参考logback创作出来的,所以配置文件也是使用xml
- log4j2同样是默认加载类路径(resources)下的log4j2.xml文件中的配置
- 但是:log4j2.xml和logback.xml有区别,第一点就是log4j2.xml中的标签名字是首字母大写;第二点就是多个单词采用的是驼峰命名法,还有其他的区别
简单的log4j2.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒 注意:这个标签不要加xmlns属性,加上之后就有表空间约束,指不定会搞出问题--><Configuration> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> </Root> </Loggers></Configuration>
测试代码
@Test public void log4j2XmlTest() { // 是org.apache.logging.log4j包下的 Logger logger = LogManager.getLogger(Log4j2Test.class); logger.fatal("fatal信息"); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); }
结果
直接输出如下内容: fatal信息 error信息 warn信息 info信息
6.1.3、SLF4J + LOG4J2 - 从此开始都是重点
- slf4j门面调用的是log4j2的门面,再由log4j2的门面调用log4j2的实现
- 步骤
- 1、导入slf4j的日志门面
- 2、导入log4j2的适配器
- 3、导入log4j2的日志门面
- 4、导入log4j2的日志实现
- 5、编写log4j2.xml
依赖
<!--slf4j门面--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!--log4j-slf4j适配器--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.17.1</version> </dependency> <!--log4j门面--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.17.1</version> </dependency> <!--logj日志实现--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.1</version> </dependency> <dependency>
log4j2.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒 注意:这个标签不要加xmlns属性,加上之后就有表空间约束,指不定会搞出问题--><Configuration> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> </Root> </Loggers></Configuration>
测试
- 保证没有其他的测试代码 或者说 保证
import
导入是org.slf4j
门面的
@Test public void log4j2AndSlf4jTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
结果
error信息warn信息info信息debug信息
6.1.4、将日志输出到文件
- 目前的代码都是基于前面配置好的log4j2 + slf4j的形式
log4j2.xml的配置文件
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> </Root> </Loggers></Configuration>
@Test public void fileAppenderTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); }
6.1.5、拆分日志
log4j2.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> <!-- 按照指定规则来拆分日志文件 fileName:日志文件的名字 filePattern:日志文件拆分后文件的命名规则 $${date:yyyy-MM-dd}:根据日期当天,创建一个文件夹 例如:2021-01-01这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) 2021-01-02这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) rollog-%d{yyyy-MM-dd-HH-mm}-%i.log 为文件命名的规则:%i表示序号,从0开始,目的是为了让每一份文件名字不会重复 --> <RollingFile name="rollingFile" fileName="${logPath}ollingLog.log" filePattern="${logPath}/$${date:yyyy-MM-dd}ollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log"> <!-- 日志消息格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> <Policies> <!-- 在系统启动时,触发拆分规则,产生一个日志文件 --> <OnStartupTriggeringPolicy/> <!-- 按照文件的大小进行拆分 注:这里虽然限制是10KB,但是真实生成出来的文件一般都是比这数值大1KB--> <SizeBasedTriggeringPolicy size="10KB"/> <!-- 按照时间节点进行拆分 拆分规则就是filePattern--> <TimeBasedTriggeringPolicy/> </Policies> <!-- 在同一目录下,文件的个数限制,如果超出了设置的数值,则根据时间以新的覆盖旧--> <DefaultRolloverStrategy max="30"/> </RollingFile> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> <AppenderRef ref="rollingFile"/> </Root> </Loggers></Configuration>
测试
@Test public void rollingLogTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } }
6.1.6、异步日志
- 这个技术就是log4j2最骚的一个点
- Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger,分别对应前面我们说的Appender组件和Logger组件
- 注意这是两种不同的实现方式,在设计和源码上都是不同的体现
6.1.6.1、AsyncAppender方式 - 了解
- 是通过引用别的Appender来实现的
- 这种方式使用的就是
<Async></Async>
标签
当有日志事件到达时,会开启另外一个线程来处理它们。需要注意的是,如果在Appender的时候出现异常,对应用来说是无法感知的。AsyncAppender应该在它引用的Appender之后配置,默认使用 java.util.concurrent.ArrayBlockingQueue实现而不需要其它外部的类库。 当使用此Appender的时候,在多线程的环境下需要注意,阻塞队列容易受到锁争用的影响,这可能会对性能产生影响。这时候,我们应该考虑使用无锁的异步记录器(AsyncLogger)
步骤
-
1、添加异步日志依赖
-
<!--asyncAppender异步日志依赖--> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.4.2</version> </dependency>
-
2、在Appenders标签中,对于异步进行配置
-
使用Async标签
-
<!-- 配置异步日志 --> <Async name="asyncAppender"> <!-- 引用在前面配置的appender --> <AppenderRef ref="fileAppender"/> </Async>
-
-
-
3、rootlogger引用Async
-
<!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="asyncAppender"/> </Root> </Loggers>
-
完整的log4j2.xml配置
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> <!-- 按照指定规则来拆分日志文件 fileName:日志文件的名字 filePattern:日志文件拆分后文件的命名规则 $${date:yyyy-MM-dd}:根据日期当天,创建一个文件夹 例如:2021-01-01这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) 2021-01-02这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) rollog-%d{yyyy-MM-dd-HH-mm}-%i.log 为文件命名的规则:%i表示序号,从0开始,目的是为了让每一份文件名字不会重复 --> <RollingFile name="rollingFile" fileName="${logPath}ollingLog.log" filePattern="${logPath}/$${date:yyyy-MM-dd}ollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log"> <!-- 日志消息格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> <Policies> <!-- 在系统启动时,触发拆分规则,产生一个日志文件 --> <OnStartupTriggeringPolicy/> <!-- 按照文件的大小进行拆分 注:这里虽然限制是10KB,但是真实生成出来的文件一般都是比这数值大1KB--> <SizeBasedTriggeringPolicy size="10KB"/> <!-- 按照时间节点进行拆分 拆分规则就是filePattern--> <TimeBasedTriggeringPolicy/> </Policies> <!-- 在同一目录下,文件的个数限制,如果超出了设置的数值,则根据时间以新的覆盖旧--> <DefaultRolloverStrategy max="30"/> </RollingFile> <!-- 配置异步日志 --> <Async name="asyncAppender"> <!-- 引用在前面配置的appender --> <AppenderRef ref="fileAppender"/> </Async> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> <AppenderRef ref="rollingFile"/> <AppenderRef ref="asyncAppender"/> </Root> </Loggers></Configuration>
测试
@Test public void asyncAppenderTest() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2、AsyncLogger方式 - 必须会
- AsyncLogger才是log4j2实现异步最重要的功能体现,也是官方推荐的异步方式
- 它可以使得调用Logger.log返回的更快。你可以有两种选择:全局异步和混合异步
6.1.6.2.1、全局异步 - 了解
-
所有的日志都异步的记录,在配置文件上不用做任何改动,只需要在jvm启动的时候增加一个参数即可实现
-
设置方式
-
只需要在类路径resources下添加一个properties属性文件,做一步配置即可,**文件名要求必须是:
log4j2.component.properties**
,这个properties配置文件内容如下:-
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
-
-
操作一波
在resources
目录中新建log4j2.component.properties
文件,并配置如下内容:
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
log4j2.xml
文件配置( 不配置任何的async异步设置 )
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> <!-- 按照指定规则来拆分日志文件 fileName:日志文件的名字 filePattern:日志文件拆分后文件的命名规则 $${date:yyyy-MM-dd}:根据日期当天,创建一个文件夹 例如:2021-01-01这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) 2021-01-02这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) rollog-%d{yyyy-MM-dd-HH-mm}-%i.log 为文件命名的规则:%i表示序号,从0开始,目的是为了让每一份文件名字不会重复 --> <RollingFile name="rollingFile" fileName="${logPath}ollingLog.log" filePattern="${logPath}/$${date:yyyy-MM-dd}ollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log"> <!-- 日志消息格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> <Policies> <!-- 在系统启动时,触发拆分规则,产生一个日志文件 --> <OnStartupTriggeringPolicy/> <!-- 按照文件的大小进行拆分 注:这里虽然限制是10KB,但是真实生成出来的文件一般都是比这数值大1KB--> <SizeBasedTriggeringPolicy size="10KB"/> <!-- 按照时间节点进行拆分 拆分规则就是filePattern--> <TimeBasedTriggeringPolicy/> </Policies> <!-- 在同一目录下,文件的个数限制,如果超出了设置的数值,则根据时间以新的覆盖旧--> <DefaultRolloverStrategy max="30"/> </RollingFile> <!-- 配置异步日志 --> <!--<Async name="asyncAppender">--> <!-- <!– 引用在前面配置的appender –>--> <!-- <AppenderRef ref="fileAppender"/>--> <!--</Async>--> </Appenders> <!--配置logger--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> <AppenderRef ref="rollingFile"/> <!--<AppenderRef ref="asyncAppender"/>--> </Root> </Loggers></Configuration>
测试
@Test public void globalAsync() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2.2、混合异步 - 必须会
- 可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活
- 混合异步的方式需要通过修改配置文件来实现,使用
AsyncLogger
标签来配置
log4j2.xml配置
- 注意点:记得把全局异步的properties内容注释掉,全局异步和混合异步不能同时出现
- 编写方式:在
appender
标签中石油``AsyncLogger`标签,然后在这里面引用要异步输出的appender即可 - 下面配置的需求是:
cn.zixieqing
包下的日志输出到文件进行异步操作,而root logger
日志是同步操作
<?xml version="1.0" encoding="UTF-8" ?><!--一样的,所有的日志配置都需要在Configuration标签中进行 这个标签中除了xmlns,还可以跟两个属性配置 status="级别" 日志框架本身的日志输出级别 一般都不需要配置,因为加了之后,输出信息会多一些其实没多大用的内容 如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/> monitorInterval="数值" 自动加载配置文件的间隔时间 如:monitorInterval="5" 就是5秒--><Configuration> <!--配置全局通用属性--> <properties> <property name="logPath">D:test</property> </properties> <!--配置Appender输出类型--> <Appenders> <Console name="consoleAppender" target="SYSTEM_OUT"/> <!--配置文件输出--> <File name="fileAppender" fileName="${logPath}/log4j2.log"> <!-- 配置文件输出格式 一样遵循logback中的格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> </File> <!-- 按照指定规则来拆分日志文件 fileName:日志文件的名字 filePattern:日志文件拆分后文件的命名规则 $${date:yyyy-MM-dd}:根据日期当天,创建一个文件夹 例如:2021-01-01这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) 2021-01-02这个文件夹中,记录当天的所有日志信息(拆分出来的日志放在这个文件夹中) rollog-%d{yyyy-MM-dd-HH-mm}-%i.log 为文件命名的规则:%i表示序号,从0开始,目的是为了让每一份文件名字不会重复 --> <RollingFile name="rollingFile" fileName="${logPath}ollingLog.log" filePattern="${logPath}/$${date:yyyy-MM-dd}ollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log"> <!-- 日志消息格式 --> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/> <Policies> <!-- 在系统启动时,触发拆分规则,产生一个日志文件 --> <OnStartupTriggeringPolicy/> <!-- 按照文件的大小进行拆分 注:这里虽然限制是10KB,但是真实生成出来的文件一般都是比这数值大1KB--> <SizeBasedTriggeringPolicy size="10KB"/> <!-- 按照时间节点进行拆分 拆分规则就是filePattern--> <TimeBasedTriggeringPolicy/> </Policies> <!-- 在同一目录下,文件的个数限制,如果超出了设置的数值,则根据时间以新的覆盖旧--> <DefaultRolloverStrategy max="30"/> </RollingFile> <!-- 配置异步日志 --> <!--<Async name="asyncAppender">--> <!-- <!– 引用在前面配置的appender –>--> <!-- <AppenderRef ref="fileAppender"/>--> <!--</Async>--> <!-- 自定义logger,让自定义的logger为异步logger --> <!-- name 就是包路径,注意点:这个异步配置只限于这个name指定的包下的日志有异步操作,要是另有一个包cn.xiegongzi 那么:cn.xiegongzi包下的日志还是同步的,并不会异步 includeLocation="false" 表示去除日志记录中的行号信息,这个行号信息非常的影响日志记录的效率(生产中都不加这个行号) 严重的时候可能记录的比同步的日志效率还有低 additivity="false" 表示不继承rootlogger --> <AsyncLogger name="cn.zixieqing" level="debug" includeLocation="false" additivity="false"> <!-- 文件输出fileAppender,设置为异步打印 --> <AppenderRef ref="fileAppender"/> </AsyncLogger> </Appenders> <!--配置logger--> <!--现在这个root logger中的日志是同步的--> <Loggers> <!--配置root logger--> <Root level="DEBUG"> <AppenderRef ref="consoleAppender"/> <AppenderRef ref="fileAppender"/> <AppenderRef ref="rollingFile"/> <!--<AppenderRef ref="asyncAppender"/>--> </Root> </Loggers></Configuration>
测试
@Test public void blendAsync() { Logger logger = LoggerFactory.getLogger(Log4j2Test.class); for (int i = 0; i < 1000; i++) { logger.error("error信息"); logger.warn("warn信息"); logger.info("info信息"); logger.debug("debug信息"); logger.trace("trace信息"); } System.out.println("其他要执行的异步任务1"); System.out.println("其他要执行的异步任务2"); System.out.println("其他要执行的异步任务3"); System.out.println("其他要执行的异步任务4"); System.out.println("其他要执行的异步任务5"); }
6.1.6.2.3、AsyncAppender、AsyncLogge两种方式的建议
- 如果使用异步日志,AsyncAppender、AsyncLogger不要同时出现,不会有这种奇葩需求,同时效果也不会叠加,如果同时出现,那么效率会以AsyncAppender为主
- 同样的,AsyncLogger的全局异步和混合异步也不要同时出现,也不会有这种奇葩,效果也不会叠加
7、后续的事情
- 下一篇为SpringBoot相关,链接地址:https://www.cnblogs.com/xiegongzi/p/16305432.html
-
这篇博客内容太多,加载量太大,这个垃圾博客,老衲真心怀疑:TMD,它是每次更新时在线更新博文全部内容,TNND,不会在点击确认时再收集全部数据啊,到了3000多行以上的博文内容之后,老衲只要一写内容,就发现在重新加载,然后我笔记本的风扇就开始快速转动,内存快速升到60%以上
-
然后就发现浏览器弹出艹了它先人坟的如下页面,害老衲又要重新把markdown中的内容重新复制过来,搞得老衲很想只弄我的知乎去了
Java中如何快捷的创建不可变集合
陈叔叔 发表了文章 • 0 个评论 • 62 次浏览 • 2022-05-24 15:42
在Java 9中又新增了一些API来帮助便捷的创建不可变集合,以减少代码复杂度。
本期配套视频:Java 9 新特性:快速定义不可变集合
常规写法
以往我们创建一些不可变集合的时候,通常是这样写的:
// 不可变的SetSet<String> set = new HashSet<>();set.add("a");set.add("b");set.add("c");set = Collections.unmodifiableSet(set);// 不可变的ListList<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list = Collections.unmodifiableList(list);
Java8的写法
在Java 8的时候,利用Stream API还能简化一下,可以演变成这样:
Set<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));List<Integer> list = Collections.unmodifiableList(Stream.of(1, 2, 3).collect(toList()));
Java9的写法
而现在到了Java 9,这一操作变的更为简单,只需要这样:
Set<String> set = Set.of("a", "b", "c");List<Integer> list = List.of(1, 2, 3);
同时,对于一下更复杂的集合也一样支持,比如Map类型也可以这样写:
Map<String, String> map = Map.of("a", "1", "b", "2", "c", "3");
就是要注意下,Map.of的参数是key和value成对出现的,所以参数数量一定是偶数:
Map.of()Map.of(k1, v1)Map.of(k1, v1, k2, v2)Map.of(k1, v1, k2, v2, k3, v3)...
与asList的区别
看到这里,可能有的人会问了,之前不是对于集合有asXxx这样的方便方法了么?他们有啥区别吗?
这里就以List.of
和Arrays.asList
为例,给大家列一下它们的异同:
-
Java 9中推出
List.of
创建的是不可变集合,而Arrays.asList
是可变集合 -
List.of
和Arrays.asList
都不允许add和remove元素,但Arrays.asList
可以调用set更改值,而List.of
不可以,会报java.lang.UnsupportedOperationException
异常 -
List.of
中不允许有null值,Arrays.asList
中可以有null值
好了,今天的分享就到这里!
如果您学习过程中如遇困难?可以加入我们超高质量的技术交流群,参与交流与讨论,更好的学习与进步!
本文收录在了我正在连载的《Java新特性专栏》,该系列该用电子书的方式编写,如果想要沉浸式阅读学习的话,可以访问Web版本:https://www.didispace.com/java-features/
欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源
画图带你彻底弄懂三级缓存和循环依赖的问题
陈叔叔 发表了文章 • 0 个评论 • 54 次浏览 • 2022-05-23 12:44

大家好。我们都知道,Spring可以通过三级缓存解决循环依赖的问题,这也是面试中很常见的一个面试题,本文就来着重讨论一下有关循环依赖和三级缓存的问题。
一、什么是循环依赖
大家平时在写业务的时候应该写过这样的代码。
其实这种类型就是循环依赖,就是AService 和BService两个类相互引用。
二、三级缓存可以解决的循环依赖场景
如上面所说,大家平时在写这种代码的时候,项目其实是可以起来的,也就是说其实三级缓存是可以解决这种循环依赖的。
当然除了这种字段注入,set注入也是可以解决的,代码如下。

接下来就来探究三级缓存是如何解决这种循环依赖的?
三、Spring的Bean是如何创建出来的
本文所说的Bean和对象可以理解为同一个意思。
先说如何解决循环依赖之前,先来了解一下一个Bean创建的大致流程。为什么要说Bean的创建过程,因为循环依赖主要是发生在Bean创建的过程中,知道Bean是如何创建的,才能更好的理解三级缓存的作用。
其实Spring Bean的生命周期源码剖析我也在微信公众号 三友的java日记 中发过,并且有简单的提到三级缓存,有兴趣的同学可以在关注公众号之后回复 Bean 即可获取文章链接,里面有Bean创建过程更详细的说明。这里我简单画一张图来说一下。

其实图里的每个阶段还可以分为一些小的阶段,我这里就没画出来了。
来说一下每个阶段干了什么事。
- BeanDefinition的读取阶段:我们在往Spring容器注入Bean的时候,一般会通过比如xml方式,@Bean注解的方式,@Component注解的方式,其实不论哪一种,容器启动的时候都会去解析这些配置,然后为每个Bean生成一个对应的BeanDefinition,这个BeanDefinition包含了这个Bean的创建的信息,Spring就是根据BeanDefinition去决定如何创建一个符合你要求的Bean
- Bean的实例化阶段:这个阶段主要是将你配置的Bean根据Class的类型创建一个对象出来
- Bean的属性赋值阶段:这个阶段主要是用来处理属性的赋值,比如@Autowired注解的生效就是在这个阶段的
- Bean的初始化阶段:这个阶段主要是回调一些方法,比如你的类实现了InitializingBean接口,那么就会回调afterPropertiesSet方法,同时动态代理其实也是在这个阶段完成的。
其实从这可以看出,一个Spring Bean的生成要分为很多的阶段,只有这些事都处理完了,这个Bean才是完完全全创建好的Bean,也就是我们可以使用的Bean。
四、三级缓存指的是哪三级缓存
这里直接上源码

第一级缓存:singletonObjects
存放已经完完全全创建好的Bean,什么叫完完全全创建好的?就是上面说的是,所有的步骤都处理完了,就是创建好的Bean。一个Bean在产的过程中是需要经历很多的步骤,在这些步骤中可能要处理@Autowired注解,又或是处理@Transcational注解,当需要处理的都处理完之后的Bean,就是完完全全创建好的Bean,这个Bean是可以用来使用的,我们平时在用的Bean其实就是创建好的。
第二级缓存:earlySingletonObjects
早期暴露出去的Bean,其实也就是解决循环依赖的Bean。早期的意思就是没有完完全全创建好,但是由于有循环依赖,就需要把这种Bean提前暴露出去。其实 早期暴露出去的Bean 跟 完完全全创建好的Bean 他们是同一个对象,只不过早期Bean里面的注解可能还没处理,完完全全的Bean已经处理了完了,但是他们指的还是同一个对象,只不过它们是在Bean创建过程中处于的不同状态,如果早期暴露出去的Bean跟完完全全创建好的Bean不是同一个对象是会报错的,项目也就起不来,这个不一样导致报错问题,这里我会结合一个案例再来写一篇文章,这里不用太care,就认为是一样的。
第三级缓存:singletonFactories
存的是每个Bean对应的ObjectFactory对象,通过调用这个对象的getObject方法,就可以获取到早期暴露出去的Bean。
注意:这里有个很重要的细节就是三级缓存只会对单例的Bean生效,像多例的是无法利用到三级缓存的,通过三级缓存所在的类名DefaultSingletonBeanRegistry就可以看出,仅仅是对SingletonBean也就是单例Bean有效果。

五、三级缓存在Bean生成的过程中是如何解决循环依赖的
这里我假设项目启动时先创建了AService的Bean,那么就会根据Spring Bean创建的过程来创建。
在Bean的实例化阶段,就会创建出AService的对象,此时里面的@Autowired注解是没有处理的,创建出AService的对象之后就会构建AService对应的一个ObjectFactory对象放到三级缓存中,通过这个ObjectFactory对象可以获取到AService的早期Bean。

然后AService继续往下走,到了某一个阶段,开始处理@Autowired注解,要注入BService对象,如图

要注入BService对象,肯定要去找BService对象,那么他就会从三级缓存中的第一级缓存开始依次查找有没有BService对应的Bean,肯定都没有啊,因为BService还没创建呢。没有该怎么办呢?其实很好办,没有就去创建一个么,这样不就有了么。于是AService的注入BService的过程就算暂停了,因为现在得去创建BService,创建之后才能注入给AService。
于是乎,BService就开始创建了,当然他也是Spring的Bean,所以也按照Bean的创建方式来创建,先实例化一个BService对象,然后缓存对应的一个ObjectFactory到第三级缓存中,然后就到了需要处理@Autowired注解的时候了,如图。

@Autowired注解需要注入AService对象。注入AService对象,就需要先去拿到AService对象,此时也会一次从三级缓存查有没有AService。
先从第一级查,有没有创建好的AService,肯定没有,因为AService此时正在在创建(因为AService在创建的过程中需要注入BService才去创建BService的,虽然此刻代码正在创建BService,但是AService也是在创建的过程中,只不过暂停了,只要BService创建完,AService会继续往下创建);第一级缓存没有,那么就去第二级看看,也没有,没有早期的AService;然后去第三级缓存看看有没有AService对应的ObjectFactory对象,惊天的发现,竟然有(上面提到过,创建出AService的对象之后,会构建AService对应的一个ObjectFactory对象放到三级缓存中),那么此时就会调用AService对应的ObjectFactory对象的getObject方法,拿到早期的AService对象,然后将早期的AService对象放到二级缓存,为什么需要放到二级缓存,主要是怕还有其他的循环依赖,如果还有的话,直接从二级缓存中就能拿到早期的AService对象。

虽然是早期的AService对象,但是我前面说过,仅仅只是早期的AService对象可能有些Bean创建的步骤还没完成,跟最后完完全全创建好的AService Bean是同一个对象。
于是接下来就把早期的AService对象注入给BService。

此时BService的@Autowired注解注入AService对象就完成了,之后再经过其他阶段的处理之后,BService对象就完完全全的创建完了。
BService对象创建完之后,就会将BService放入第一级缓存,然后清空BService对应的第三级缓存,当然也会去清空第二级缓存,只是没有而已,至于为什么清空,很简单,因为BService已经完全创建好了,如果需要BService那就在第一级缓存中就能查找到,不需要在从第二级或者第三级缓存中找到早期的BService对象。

BService对象就完完全全的创建完之后,那么接下来该干什么呢?此时当然继续创建AService对象了,你不要忘了为什么需要创建BService对象,因为AService对象需要注入一个BService对象,所以才去创建BService的,那么此时既然BService已经创建完了,那么是不是就应该注入给AService对象了?所以就会将BService注入给AService对象,这下就明白了,BService在构建的时候,已经注入了AService,虽然是早期的AService,但的确是AService对象,现在又把BService注入给了AService,那么是不是已经解决了循环依赖的问题了,AService和BService都各自注入了对方,如图。

然后AService就会跟BService一样,继续处理其它阶段的,完全创建好之后,也会清空二三级缓存,放入第一级缓存。

到这里,AService和BService就都创建好了,循环依赖也就解决了。
这下你应该明白了三级缓存的作用,主要是第二级和第三级用来存早期的对象,这样在有循环依赖的对象,就可以注入另一个对象的早期状态,从而达到解决循环依赖的问题,而早期状态的对象,在构建完成之后,也就会成为完完全全可用的对象。
六、三级缓存无法解决的循环依赖场景
1)构造器注入无法解决循环依赖
上面的例子是通过@Autowired注解直接注入依赖的对象,但是如果通过构造器注入循环依赖的对象,是无法解决的,如代码下

构造器注入就是指创建AService对象的时候,就传入BService对象,而不是用@Autowired注解注入BService对象。
运行结果

启动时就会报错,所以通过构造器注入对象就能避免产生循环依赖的问题,因为如果有循环依赖的话,那么就会报错。
至于三级缓存为什么不能解决构造器注入的问题呢?其实很好理解,因为上面说三级缓存解决循环依赖的时候主要讲到,在AService实例化之后,会创建对应的ObjectFactory放到第三级缓存,发生循环依赖的时候,可以通过ObjectFactory拿到早期的AService对象;而构造器注入,是发生在实例化的时候,此时还没有AService对象正在创建,还没完成,压根就还没执行到往第三级添加对应的ObjectFactory的步骤,那么BService在创建的时候,就无法通过三级缓存拿到早期的AService对象,拿不到怎么办,那就去创建AService对象,但是AService不是正在创建么,于是会报错。
2)注入多例的对象无法解决循环依赖


启动引导类

要获取AService对象,因为多例的Bean在容器启动的时候是不会去创建的,所以得去获取,这样就会创建了。
运行结果

为什么不能解决,上面在说三级缓存的时候已经说过了,三级缓存只能对单例Bean生效,那么多例是不会起作用的,并且在创建Bean的时候有这么一个判断,那就是如果出现循环依赖并且是依赖的是多例的Bean,那么直接抛异常,源码如下

注释其实说的很明白,推测出现了循环依赖,抛异常。
所以上面提到的两种循环依赖的场景,之所以无法通过三级缓存来解决,是因为压根这两种场景就无法使用三级缓存,所以三级缓存肯定解决不掉。
七、不用三级缓存,用二级缓存能不能解决循环依赖
遇到这种面试题,你就跟面试官说,如果行的话,Spring的作者为什么不这么写呢?
哈哈,开个玩笑,接下来说说到底为什么不行。
这里我先说一下前面没提到的细节,那就是通过ObjectFactory获取的Bean可能是两种类型,第一种就是实例化阶段创建出来的对象,还是一种就是实例化阶段创建出来的对象的代理对象。至于是不是代理对象,取决于你的配置,如果添加了事务注解又或是自定义aop切面,那就需要代理。这里你不用担心,如果这里获取的是代理对象,那么最后完全创建好的对象也是代理对象,ObjectFactory获取的对象和最终完全创建好的还是同一个,不是同一个肯定会报错,所以上面的理论依然符合,这里只是更加的细节化。
有了这个知识点之后,我们就来谈一下为什么要三级缓存。
第一级缓存,也就是缓存完全创建好的Bean的缓存,这个缓存肯定是需要的,因为单例的Bean只能创建一次,那么肯定需要第一级缓存存储这些对象,如果有需要,直接从第一级缓存返回。那么如果只能有二级缓存的话,就只能舍弃第二级或者第三级缓存。
假设舍弃第三级缓存
舍弃第三级缓存,也就是没有ObjectFactory,那么就需要往第二缓存放入早期的Bean,那么是放没有代理的Bean还是被代理的Bean呢?
1)如果直接往二级缓存添加没有被代理的Bean,那么可能注入给其它对象的Bean跟最后最后完全生成的Bean是不一样的,因为最后生成的是代理对象,这肯定是不允许的;
2)那么如果直接往二级缓存添加一个代理Bean呢?
- 假设没有循环依赖,提前暴露了代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错,
- 假设没有循环依赖,使用了ObjectFactory,那么就不会提前暴露了代理对象,到最后生成的对象是什么就是什么,就不会报错,
- 如果有循环依赖,不论怎样都会提前暴露代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错
通过上面分析,如果没有循环依赖,使用ObjectFactory,就减少了提前暴露代理对象的可能性,从而减少报错的可能。
假设舍弃第二级缓存
假设舍弃第二级缓存,也就是没有存放早期的Bean的缓存,其实肯定也不行。上面说过,ObjectFactory其实获取的对象可能是代理的对象,那么如果每次都通过ObjectFactory获取代理对象,那么每次都重新创建一个代理对象,这肯定也是不允许的。
从上面分析,知道为什么不能使用二级缓存了吧,第三级缓存就是为了避免过早地创建代理对象,从而避免没有循环依赖过早暴露代理对象产生的问题,而第二级缓存就是防止多次创建代理对象,导致对象不同。
本文完。
如果觉得这篇文章对你有所帮助,还请帮忙点赞、在看、转发给更多的人,码字不易,非常感谢!
欢迎关注公众号 三友的java日记,更多技术干货及时获得。
往期热门文章推荐
从零玩转人脸识别
陈叔叔 发表了文章 • 0 个评论 • 68 次浏览 • 2022-05-21 15:48
前言
在线demo
(前往享受人脸识别)
文章作者个人博客
(前往作者博客)
本期教程人脸识别第三方平台为虹软科技,本文章讲解的是人脸识别RGB活体追踪技术,免费的功能很多可以自行搭配,希望在你看完本章课程有所收获。
ArcFace 离线SDK,包含人脸检测、性别检测、年龄检测、人脸识别、图像质量检测、RGB活体检测、IR活体检测等能力,初次使用时需联网激活,激活后即可在本地无网络环境下工作,可根据具体的业务需求结合人脸识别SDK灵活地进行应用层开发。
功能介绍
1. 人脸检测
对传入的图像数据进行人脸检测,返回人脸的边框以及朝向信息,可用于后续的人脸识别、特征提取、活体检测等操作;
- 支持IMAGE模式和VIDEO模式人脸检测。
- 支持单人脸、多人脸检测,最多支持检测人脸数为50。
2.人脸追踪
对来自于视频流中的图像数据,进行人脸检测,并对检测到的人脸进行持续跟踪。(我们是实时的所以就只能使用第三方操作,先不使用这个)
3.人脸特征提取
提取人脸特征信息,用于人脸的特征比对。
4.人脸属性检测
人脸属性,支持检测年龄、性别以及3D角度。
人脸3D角度:俯仰角(pitch), 横滚角(roll), 偏航角(yaw)。
5.活体检测
离线活体检测,静默式识别,在人脸识别过程中判断操作用户是否为真人,有效防御照片、视频、纸张等不同类型的作弊攻击,提高业务安全性,让人脸识别更安全、更快捷,体验更佳。支持单目RGB活体检测、双目(IR/RGB)活体检测,可满足各类人脸识别终端产品活体检测应用。
开造
访问地址: https://ai.arcsoft.com.cn/technology/faceTracking.html
进入开发者中心进行注册以及认证个人信息
1. 点击我的应用 > 新建应用
2.填写信息立即创建 点击 添加SDK
3.选中免费版人脸识别
4. 填写授权码信息
选择平台先选择windows的根据你的电脑配置来 是64位还是32位的, 语言选择Java
5. 介绍sdk文件
构建项目工程
导入项目依赖
<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.7.RELEASE</spring-boot.version> <druid-spring-boot-starter.version>1.2.6</druid-spring-boot-starter.version> <mybatis-spring-boot.version>2.1.4</mybatis-spring-boot.version> <pagehelper.boot.version>1.3.0</pagehelper.boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 人脸识别 --> <dependency> <groupId>com.arcsoft.face</groupId> <artifactId>arcsoft-sdk-face</artifactId> <scope>system</scope> <systemPath>${project.basedir}/lib/arcsoft-sdk-face-3.0.0.0.jar</systemPath> <version>3.0.0.0</version> </dependency> <!-- pool 对象池 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.0.1-jre</version> </dependency> <!-- Mysql驱动包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- mybatis-plus 增强CRUD --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!-- pagehelper 分页插件 内置mybatis 依赖--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pagehelper.boot.version}</version> </dependency> <!-- 阿里数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid-spring-boot-starter.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <!-- Spring框架基本的核心工具 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <!-- JSON工具类 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.0</version> </dependency> <!-- SpringWeb模块 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency> <!-- servlet包 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency> <!-- aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.7.RELEASE</version> <configuration> <mainClass>top.yangbuyi.YangbuyiFaceDemoApplication</mainClass> <!-- 文件要配置includeSystemScope属性,否则可能会导致arcsoft-sdk-face-3.0.0.0.jar获取不到 --> <includeSystemScope>true</includeSystemScope> <fork>true</fork> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
使用代码生成器生成CURD
版本请对应图片当中的jebat全家桶群里获取3秒破解使用
生成完毕后在根目录创建lib目录将下载下来的人脸识别依赖导入->右击添加到库
application.yml 修改配置文件
# 开发环境配置server: # 服务器的HTTP端口,默认为8080 port: 8080 servlet: # 应用的访问路径 context-path: / tomcat: # tomcat的URI编码 uri-encoding: UTF-8 # tomcat最大线程数,默认为200 max-threads: 800 # Tomcat启动初始化的线程数,默认值25 min-spare-threads: 30# Spring配置spring: # 同时执行其它配置文件 profiles: active: druid mvc: # 把前端的接收到的时间格式 格式化为 yyyy-MM-dd HH:mm:ss date-format: yyyy-MM-dd HH:mm:ss jackson: # 把后台的时间格式 格式化为 yyyy-MM-dd HH:mm:ss date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 # 服务模块 devtools: restart: # 热部署开关 enabled: true# MyBatis配置mybatis-plus: # 搜索指定包别名 typeAliasesPackage: top.yangbuyi.domain # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapperLocations: classpath*:mapper/*Mapper.xml # 加载全局的配置文件 configLocation: classpath:mybatis/mybatis-config.xml# PageHelper分页插件pagehelper: helperDialect: mysql reasonable: true supportMethodsArguments: true params: count=countSql# 人脸识别配置# WIND64config: sdk-lib-path: M:yangbuyiya-RBAClibsWIN64 app-id: 4QKtmacvsKqaCsoXyyujcs21JTAr79pTczPdZpuaEjhH sdk-key: EgBjrmidnqstaL46msfHukeKanYXCujzeHokf2qcC3br thread-pool-size: 5
application-druid.yml 数据源配置
# 数据源配置spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: url: jdbc:mysql://127.0.0.1:3308/face?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /yangbuyi/druid/* filter: stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
resources 下创建mybatis文件夹创建mybatis-config.xml文件
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <settings> <setting name="cacheEnabled" value="true"/> <!-- 全局映射器启用缓存 --> <setting name="useGeneratedKeys" value="true"/> <!-- 允许 JDBC 支持自动生成主键 --> <setting name="defaultExecutorType" value="REUSE"/> <!-- 配置默认的执行器 --> <setting name="logImpl" value="SLF4J"/> <!-- 指定 MyBatis 所用日志的具体实现 --> <!-- <setting name="mapUnderscoreToCamelCase" value="true"/> 驼峰式命名 --> </settings></configuration>
项目基础文件配置
创建config文件
创建ApplicationConfig全局配置类
/** * 程序注解配置 * * @author yangbuyi */@Configuration// 表示通过aop框架暴露该代理对象,AopContext能够访问@EnableAspectJAutoProxy(exposeProxy = true)// 指定要扫描的Mapper类的包的路径@MapperScan("top.yangbuyi.mapper")public class ApplicationConfig { /** * 时区配置 */ @Bean public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization () { return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); } /** * 处理Long类型精度丢失 * * @return */ @Bean("jackson2ObjectMapperBuilderCustomizer") public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer () { return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); }}
创建全局MybatisPlusConfig配置
/** * @program: yangbuyi-rbac * @ClassName: MybatisPlusConfig * @create: 2022-04-25 15:48 * @author: yangbuyi.top * @since: JDK1.8 * @MybatisPlusConfig: $ **/import com.baomidou.mybatisplus.annotation.DbType;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.transaction.annotation.EnableTransactionManagement;@EnableTransactionManagement(proxyTargetClass = true)@Configurationpublic class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(paginationInnerInterceptor()); // 乐观锁插件 interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor()); // 阻断插件 interceptor.addInnerInterceptor(blockAttackInnerInterceptor()); return interceptor; } /** * 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html */ public PaginationInnerInterceptor paginationInnerInterceptor () { PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); // 设置数据库类型为mysql paginationInnerInterceptor.setDbType(DbType.MYSQL); // 设置最大单页限制数量,默认 500 条,-1 不受限制 paginationInnerInterceptor.setMaxLimit(-1L); return paginationInnerInterceptor; } /** * 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html */ public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor () { return new OptimisticLockerInnerInterceptor(); } /** * 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html */ public BlockAttackInnerInterceptor blockAttackInnerInterceptor () { return new BlockAttackInnerInterceptor(); }}
创建dto文件夹
创建人脸返回实体 FaceSearchResDto
/** * @author yangbuyi.top */@Datapublic class FaceSearchResDto { /** * 唯一人脸Id */ private String faceId; /** * 人脸名称 */ private String name; private Integer similarValue; /** * 年龄 */ private Integer age; /** * 性别 */ private String gender; /** * 图片 */ private String image;}
创建人脸映射 FaceUserInfo
/** * @author yangbuyi.top */@Datapublic class FaceUserInfo { private int id; private int groupId; private String faceId; private String name; private Integer similarValue; private byte[] faceFeature;}
创建年龄映射
/** * @author yangbuyi.top */public class ProcessInfo { private Integer age; private Integer gender; public Integer getAge () { return age; } public void setAge (Integer age) { this.age = age; } public Integer getGender () { return gender; } public void setGender (Integer gender) { this.gender = gender; }}
创建enums文件夹
创建ErrorCodeEnum枚举类
/** * @author yangbuyi.top */public enum ErrorCodeEnum { MOK(0, "成功"), UNKNOWN(1, "未知错误"), INVALID_PARAM(2, "无效参数"), UNSUPPORTED(3, "引擎不支持"), NO_MEMORY(4, "内存不足"), BAD_STATE(5, "状态错误"), USER_CANCEL(6, "用户取消相关操作"), EXPIRED(7, "操作时间过期"), USER_PAUSE(8, "用户暂停操作"), BUFFER_OVERFLOW(9, "缓冲上溢"), BUFFER_UNDERFLOW(10, "缓冲下溢"), NO_DISKSPACE(11, "存贮空间不足"), COMPONENT_NOT_EXIST(12, "组件不存在"), GLOBAL_DATA_NOT_EXIST(13, "全局数据不存在"), NO_FACE_DETECTED(14, "未检出到人脸"), FACE_DOES_NOT_MATCH(15, "人脸不匹配"), INVALID_APP_ID(28673, "无效的AppId"), INVALID_SDK_ID(28674, "无效的SdkKey"), INVALID_ID_PAIR(28675, "AppId和SdkKey不匹配"), MISMATCH_ID_AND_SDK(28676, "SdkKey 和使用的SDK 不匹配"), SYSTEM_VERSION_UNSUPPORTED(28677, "系统版本不被当前SDK所支持"), LICENCE_EXPIRED(28678, "SDK有效期过期,需要重新下载更新"), APS_ENGINE_HANDLE(69633, "引擎句柄非法"), APS_MEMMGR_HANDLE(69634, "内存句柄非法"), APS_DEVICEID_INVALID(69635, " Device ID 非法"), APS_DEVICEID_UNSUPPORTED(69636, "Device ID 不支持"), APS_MODEL_HANDLE(69637, "模板数据指针非法"), APS_MODEL_SIZE(69638, "模板数据长度非法"), APS_IMAGE_HANDLE(69639, "图像结构体指针非法"), APS_IMAGE_FORMAT_UNSUPPORTED(69640, "图像格式不支持"), APS_IMAGE_PARAM(69641, "图像参数非法"), APS_IMAGE_SIZE(69642, "图像尺寸大小超过支持范围"), APS_DEVICE_AVX2_UNSUPPORTED(69643, "处理器不支持AVX2指令"), FR_INVALID_MEMORY_INFO(73729, "无效的输入内存"), FR_INVALID_IMAGE_INFO(73730, "无效的输入图像参数"), FR_INVALID_FACE_INFO(73731, "无效的脸部信息"), FR_NO_GPU_AVAILABLE(73732, "当前设备无GPU可用"), FR_MISMATCHED_FEATURE_LEVEL(73733, "待比较的两个人脸特征的版本不一致"), FACEFEATURE_UNKNOWN(81921, "人脸特征检测错误未知"), FACEFEATURE_MEMORY(81922, "人脸特征检测内存错误"), FACEFEATURE_INVALID_FORMAT(81923, "人脸特征检测格式错误"), FACEFEATURE_INVALID_PARAM(81924, "人脸特征检测参数错误"), FACEFEATURE_LOW_CONFIDENCE_LEVEL(81925, "人脸特征检测结果置信度低"), ASF_EX_BASE_FEATURE_UNSUPPORTED_ON_INIT(86017, "Engine不支持的检测属性"), ASF_EX_BASE_FEATURE_UNINITED(86018, "需要检测的属性未初始化"), ASF_EX_BASE_FEATURE_UNPROCESSED(86019, "待获取的属性未在process中处理过"), ASF_EX_BASE_FEATURE_UNSUPPORTED_ON_PROCESS(86020, "PROCESS不支持的检测属性,例如FR,有自己独立的处理函数"), ASF_EX_BASE_INVALID_IMAGE_INFO(86021, "无效的输入图像"), ASF_EX_BASE_INVALID_FACE_INFO(86022, "无效的脸部信息"), ASF_BASE_ACTIVATION_FAIL(90113, "人脸比对SDK激活失败,请打开读写权限"), ASF_BASE_ALREADY_ACTIVATED(90114, "人脸比对SDK已激活"), ASF_BASE_NOT_ACTIVATED(90115, "人脸比对SDK未激活"), ASF_BASE_SCALE_NOT_SUPPORT(90116, "detectFaceScaleVal 不支持"), ASF_BASE_VERION_MISMATCH(90117, "SDK版本不匹配"), ASF_BASE_DEVICE_MISMATCH(90118, "设备不匹配"), ASF_BASE_UNIQUE_IDENTIFIER_MISMATCH(90119, "唯一标识不匹配"), ASF_BASE_PARAM_NULL(90120, "参数为空"), ASF_BASE_SDK_EXPIRED(90121, "SDK已过期"), ASF_BASE_VERSION_NOT_SUPPORT(90122, "版本不支持"), ASF_BASE_SIGN_ERROR(90123, "签名错误"), ASF_BASE_DATABASE_ERROR(90124, "数据库插入错误"), ASF_BASE_UNIQUE_CHECKOUT_FAIL(90125, "唯一标识符校验失败"), ASF_BASE_COLOR_SPACE_NOT_SUPPORT(90126, "输入的颜色空间不支持"), ASF_BASE_IMAGE_WIDTH_NOT_SUPPORT(90127, "输入图像的byte数据长度不正确"), ASF_NETWORK_BASE_COULDNT_RESOLVE_HOST(94209, "无法解析主机地址"), ASF_NETWORK_BASE_COULDNT_CONNECT_SERVER(94210, "无法连接服务器"), ASF_NETWORK_BASE_CONNECT_TIMEOUT(94211, "网络连接超时"), ASF_NETWORK_BASE_UNKNOWN_ERROR(94212, "未知错误"); private Integer code; private String description; ErrorCodeEnum (Integer code, String description) { this.code = code; this.description = description; } public Integer getCode () { return code; } public void setCode (Integer code) { this.code = code; } public String getDescription () { return description; } public void setDescription (String description) { this.description = description; } public static ErrorCodeEnum getDescriptionByCode (Integer code) { for (ErrorCodeEnum errorCodeEnum : ErrorCodeEnum.values()) { if (code.equals(errorCodeEnum.getCode())) { return errorCodeEnum; } } return ErrorCodeEnum.UNKNOWN; }}
创建utils文件夹
创建Base64DecodeMultipartFile
package top.yangbuyi.utils;import org.springframework.web.multipart.MultipartFile;import java.io.*;/** * @author yangbuyi.top * @program: yangbuyi-rbac * @ClassName: Base64DecodeMultipartFile * @create: 2022-04-25 16:25 * @since: JDK1.8 * @Base64DecodeMultipartFile: $ **/public class Base64DecodeMultipartFile implements MultipartFile { private final byte[] imgContent; private final String header; public Base64DecodeMultipartFile (byte[] imgContent, String header) { this.imgContent = imgContent; this.header = header.split(";")[0]; } @Override public String getName () { return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1]; } @Override public String getOriginalFilename () { return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1]; } @Override public String getContentType () { return header.split(":")[1]; } @Override public boolean isEmpty () { return imgContent == null || imgContent.length == 0; } @Override public long getSize () { return imgContent.length; } @Override public byte[] getBytes () throws IOException { return imgContent; } @Override public InputStream getInputStream () throws IOException { return new ByteArrayInputStream(imgContent); } @Override public void transferTo (File dest) throws IOException, IllegalStateException { new FileOutputStream(dest).write(imgContent); }}
创建ImageUtils
package top.yangbuyi.utils;import org.apache.tomcat.util.codec.binary.Base64;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.multipart.MultipartFile;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.InputStream;import java.net.URL;import java.net.URLConnection;import java.util.Arrays;/** * 图片处理工具类 * * @author yangbuyi.top */public class ImageUtils { private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); public static MultipartFile base64ToMultipartFile (String base64) { //base64编码后的图片有头信息所以要分离出来 [0]data:image/png;base64, 图片内容为索引[1] String[] baseStrs = base64.split(","); //取索引为1的元素进行处理 byte[] b = Base64.decodeBase64(baseStrs[1]); for (int i = 0; i < b.length; ++i) { if (b[i] < 0) { b[i] += 256; } } //处理过后的数据通过Base64DecodeMultipartFile转换为MultipartFile对象 return new Base64DecodeMultipartFile(b, baseStrs[0]); }}
项目基本需要的配置与工具已经创建完毕接下来我们开始人脸识别业务编写
实现BasePooledObjectFactory自定义引擎工厂
创建factory文件夹->创建FaceEngineFactory类
import com.arcsoft.face.EngineConfiguration;import com.arcsoft.face.FaceEngine;import com.arcsoft.face.enums.DetectMode;import com.arcsoft.face.enums.DetectOrient;import lombok.extern.slf4j.Slf4j;import org.apache.commons.pool2.BasePooledObjectFactory;import org.apache.commons.pool2.PooledObject;import org.apache.commons.pool2.impl.DefaultPooledObject;/** * 引擎工厂 * @author yangbuyi.top */@Slf4jpublic class FaceEngineFactory extends BasePooledObjectFactory<FaceEngine> { private final String appId; private final String sdkKey; private final String sdkLibPath; private final EngineConfiguration engineConfiguration; private final Integer detectFaceMaxNum = 10; private final Integer detectFaceScaleVal = 16; private final DetectMode detectMode = DetectMode.ASF_DETECT_MODE_IMAGE; private final DetectMode detectVideo = DetectMode.ASF_DETECT_MODE_VIDEO; private final DetectOrient detectFaceOrientPriority = DetectOrient.ASF_OP_0_ONLY; public FaceEngineFactory (String sdkLibPath, String appId, String sdkKey, EngineConfiguration engineConfiguration) { this.sdkLibPath = sdkLibPath; this.appId = appId; this.sdkKey = sdkKey; this.engineConfiguration = engineConfiguration; } @Override public FaceEngine create () throws Exception { FaceEngine faceEngine = new FaceEngine(sdkLibPath); // 用于在线激活SDK--- int activeCode = faceEngine.activeOnline(appId, sdkKey); log.info("在线激活SDK完毕!,{}", activeCode); int initCode = faceEngine.init(engineConfiguration); log.info("初始化功能引擎完毕!,{}", initCode); return faceEngine; } @Override public PooledObject<FaceEngine> wrap (FaceEngine faceEngine) { return new DefaultPooledObject<>(faceEngine); } @Override public void destroyObject (PooledObject<FaceEngine> p) throws Exception { FaceEngine faceEngine = p.getObject(); int unInitCode = faceEngine.unInit(); super.destroyObject(p); log.info("销毁对象完毕! faceEngineUnInitCode: {}", unInitCode); }}
一、编写人脸识别业务接口
在service目录下创建-> FaceEngineService
package top.yangbuyi.service;import com.arcsoft.face.FaceInfo;import com.arcsoft.face.toolkit.ImageInfo;import top.yangbuyi.dto.FaceUserInfo;import top.yangbuyi.dto.ProcessInfo;import java.util.List;import java.util.concurrent.ExecutionException;/** * 人脸识别接口 */public interface FaceEngineService { /** * 人脸检测 * @param imageInfo * @return */ List<FaceInfo> detectFaces (ImageInfo imageInfo); /** * 提取年龄-性别 * @param imageInfo * @return */ List<ProcessInfo> process (ImageInfo imageInfo); /** * 人脸特征 * @param imageInfo * @return */ byte[] extractFaceFeature (ImageInfo imageInfo) throws InterruptedException; /** * 人脸比对 * @param groupId * @param faceFeature * @return */ List<FaceUserInfo> compareFaceFeature (byte[] faceFeature, Integer groupId) throws InterruptedException, ExecutionException;}
service.impl 包下创建 FaceEngineServiceImpl 实现类
import cn.hutool.core.collection.CollectionUtil;import com.arcsoft.face.*;import com.arcsoft.face.enums.DetectMode;import com.arcsoft.face.enums.DetectOrient;import com.arcsoft.face.toolkit.ImageInfo;import com.google.common.collect.Lists;import org.apache.commons.pool2.impl.GenericObjectPool;import org.apache.commons.pool2.impl.GenericObjectPoolConfig;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import top.yangbuyi.dto.FaceUserInfo;import top.yangbuyi.dto.ProcessInfo;import top.yangbuyi.factory.FaceEngineFactory;import top.yangbuyi.mapper.SysUserFaceInfoMapper;import top.yangbuyi.service.FaceEngineService;import javax.annotation.PostConstruct;import java.math.BigDecimal;import java.util.ArrayList;import java.util.List;import java.util.concurrent.*;/** * @author yangbuyi.top */@Servicepublic class FaceEngineServiceImpl implements FaceEngineService { public final static Logger logger = LoggerFactory.getLogger(FaceEngineServiceImpl.class); @Value("${config.sdk-lib-path}") public String sdkLibPath; @Value("${config.app-id}") public String appId; @Value("${config.sdk-key}") public String sdkKey; @Value("${config.thread-pool-size}") public Integer threadPoolSize; /** * 人脸识别引擎 */ private static FaceEngine faceEngine; /** * 相似度 */ private final Integer passRate = 95; private ExecutorService executorService; @Autowired private SysUserFaceInfoMapper userFaceInfoMapper; /** * 线程池 */ private GenericObjectPool<FaceEngine> faceEngineObjectPool; /** * 项目启动时初始化线程池与人脸识别引擎配置 */ @PostConstruct public void init () { executorService = Executors.newFixedThreadPool(threadPoolSize); GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); poolConfig.setMaxIdle(threadPoolSize); poolConfig.setMaxTotal(threadPoolSize); poolConfig.setMinIdle(threadPoolSize); poolConfig.setLifo(false); //引擎配置 EngineConfiguration engineConfiguration = new EngineConfiguration(); engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE); engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY); //功能配置 对应的功能请查看文档 FunctionConfiguration functionConfiguration = new FunctionConfiguration(); functionConfiguration.setSupportAge(true); functionConfiguration.setSupportFace3dAngle(true); functionConfiguration.setSupportFaceDetect(true); functionConfiguration.setSupportFaceRecognition(true); functionConfiguration.setSupportGender(true); functionConfiguration.setSupportLiveness(true); functionConfiguration.setSupportIRLiveness(true); engineConfiguration.setFunctionConfiguration(functionConfiguration); // 底层库算法对象池 faceEngineObjectPool = new GenericObjectPool(new FaceEngineFactory(sdkLibPath, appId, sdkKey, engineConfiguration), poolConfig); try { faceEngine = faceEngineObjectPool.borrowObject(); } catch (Exception e) { e.printStackTrace(); } } /** * 确保精度 * @param value * @return */ private int plusHundred (Float value) { BigDecimal target = new BigDecimal(value); BigDecimal hundred = new BigDecimal(100f); return target.multiply(hundred).intValue(); }}
人脸识别逻辑
1、必须进行人脸特征获取 -> 特征获取成功 -> 进入人脸对比 -> 人脸检测 -> 返回人脸数据
2、必须进行人脸特征获取 -> 特征获取失败 -> 直接跳出返回 未检出到人脸
编写人脸特征获取逻辑
/** * 人脸特征 * * @param imageInfo * @return */@Overridepublic byte[]extractFaceFeature(ImageInfo imageInfo)throws InterruptedException{ FaceEngine faceEngine=null; try{ //获取引擎对象 faceEngine=faceEngineObjectPool.borrowObject(); //人脸检测得到人脸列表 List<FaceInfo> faceInfoList=new ArrayList<FaceInfo>(); //人脸检测 int i=faceEngine.detectFaces(imageInfo.getImageData(),imageInfo.getWidth(),imageInfo.getHeight(),imageInfo.getImageFormat(),faceInfoList); if(CollectionUtil.isNotEmpty(faceInfoList)){ FaceFeature faceFeature=new FaceFeature(); //提取人脸特征 faceEngine.extractFaceFeature(imageInfo.getImageData(),imageInfo.getWidth(),imageInfo.getHeight(),imageInfo.getImageFormat(),faceInfoList.get(0),faceFeature); return faceFeature.getFeatureData(); } }catch(Exception e){ logger.error("",e); }finally{ if(faceEngine!=null){ //释放引擎对象 faceEngineObjectPool.returnObject(faceEngine); } } return null; }
编写人脸对比逻辑
/** * 人脸比对 * @param groupId * @param faceFeature * @return 人脸组 */@Overridepublic List<FaceUserInfo> compareFaceFeature(byte[]faceFeature,Integer groupId)throws InterruptedException,ExecutionException{ // 识别到的人脸列表 List<FaceUserInfo> resultFaceInfoList=Lists.newLinkedList(); // 创建人脸特征对象 FaceFeature targetFaceFeature=new FaceFeature(); targetFaceFeature.setFeatureData(faceFeature); // 根据分组拿人脸库,从数据库中取出人脸库 List<FaceUserInfo> faceInfoList=userFaceInfoMapper.getUserFaceInfoByGroupId(groupId); // 分成50一组,多线程处理, 数据量大1000组 List<List<FaceUserInfo>>faceUserInfoPartList=Lists.partition(faceInfoList,50); // 多线程 CompletionService<List<FaceUserInfo>>completionService=new ExecutorCompletionService(executorService); for(List<FaceUserInfo> part:faceUserInfoPartList){ // 开始线程扫描人脸匹配度 completionService.submit(new CompareFaceTask(part,targetFaceFeature)); } // 获取线程任务参数 for(List<FaceUserInfo> faceUserInfos:faceUserInfoPartList){ List<FaceUserInfo> faceUserInfoList=completionService.take().get(); if(CollectionUtil.isNotEmpty(faceInfoList)){ resultFaceInfoList.addAll(faceUserInfoList); } } // 从大到小排序 resultFaceInfoList.sort((h1,h2)->h2.getSimilarValue().compareTo(h1.getSimilarValue())); return resultFaceInfoList; }/** * 多线程跑人脸对比逻辑 */private class CompareFaceTask implements Callable<List<FaceUserInfo>> { private final List<FaceUserInfo> faceUserInfoList; private final FaceFeature targetFaceFeature; public CompareFaceTask (List<FaceUserInfo> faceUserInfoList, FaceFeature targetFaceFeature) { this.faceUserInfoList = faceUserInfoList; this.targetFaceFeature = targetFaceFeature; } @Override public List<FaceUserInfo> call () throws Exception { FaceEngine faceEngine = null; //识别到的人脸列表 List<FaceUserInfo> resultFaceInfoList = Lists.newLinkedList(); try { faceEngine = faceEngineObjectPool.borrowObject(); for (FaceUserInfo faceUserInfo : faceUserInfoList) { FaceFeature sourceFaceFeature = new FaceFeature(); // 设置人脸特征 sourceFaceFeature.setFeatureData(faceUserInfo.getFaceFeature()); FaceSimilar faceSimilar = new FaceSimilar(); faceEngine.compareFaceFeature(targetFaceFeature, sourceFaceFeature, faceSimilar); //获取相似值 Integer similarValue = plusHundred(faceSimilar.getScore()); //相似值大于配置预期,加入到识别到人脸的列表 if (similarValue > passRate) { FaceUserInfo info = new FaceUserInfo(); info.setName(faceUserInfo.getName()); info.setFaceId(faceUserInfo.getFaceId()); info.setSimilarValue(similarValue); resultFaceInfoList.add(info); } } } catch (Exception e) { logger.error("", e); } finally { if (faceEngine != null) { faceEngineObjectPool.returnObject(faceEngine); } } return resultFaceInfoList; }}
编写根据根据分组ID获取人脸库数据
在mapper下SysUserFaceInfoMapper -> 创建接口 getUserFaceInfoByGroupId
/** * 根据分组Id * 从数据库中取出人脸库 * * @param groupId * @return 人脸库 */ List<FaceUserInfo> getUserFaceInfoByGroupId(Integer groupId);
sql实现
<resultMap id="userFace2" type="top.yangbuyi.dto.FaceUserInfo"><id column="id" property="id" javaType="int"/><result column="group_id" property="groupId" javaType="java.lang.Integer"/><result column="name" property="name" javaType="java.lang.String"/><result column="face_id" property="faceId" javaType="String"/><result column="face_feature" property="faceFeature"/><esultMap><select id = "getUserFaceInfoByGroupId" resultMap="userFace2" parameterType="java.lang.Integer" resultType="top.yangbuyi.dto.FaceUserInfo">select id, group_id, face_id, name, face_featurefrom `sys_user_face_info`where group_id = #{groupId} < /select>
二、编写人脸添加业务接口
userFaceInfoService 新增人脸业务接口
/** * 新增用户人脸识别 * * @param sysUserFaceInfo 用户人脸识别 * @return 结果 */public int insertSysUserFaceInfo(SysUserFaceInfo sysUserFaceInfo);
就是一共简简单单的新增插入数据 这个就不用我教了吧?????????
三、 编写Controller业务控制层
import top.yangbuyi.domain.AjaxResult;/** * (sys_user_face_info)表控制层 * * @author yangbuyi.top */@RestController@Slf4j@RequestMapping("face")@RequiredArgsConstructorpublic class SysUserFaceInfoController { public final static Logger logger = LoggerFactory.getLogger(SysUserFaceInfoController.class); private final FaceEngineService faceEngineService; private final SysUserFaceInfoService userFaceInfoService; /** * 人脸添加 * * @param file 人脸附件 * @param groupId 分组id * @param name 用户登录名称 */ @RequestMapping(value = "/faceAdd", method = RequestMethod.POST) public AjaxResult faceAdd (@RequestBody Map<String, Object> map) { // 业务.... yangbuyi.top 版权所有 return AjaxResult.success(); } /** * 人脸识别 * * @param file 人脸附件 * @param groupId 分组ID 方便快速识别 */ @RequestMapping(value = "/faceSearch", method = RequestMethod.POST) public AjaxResult faceSearch (@RequestBody Map<String, Object> map) throws Exception { // 业务... yangbuyi.top 版权所有 return AjaxResult.success(); }
faceAdd 具体业务编写
- 根据前端传递的
file -> 人脸图片
groupId -> 用户分组(用于缩小范围查询该人员的位置有效减少数据量大的问题)
name -> 当前用户名称(实际开发当中应该是存储id)
String file = String.valueOf(map.get("file")); String groupId = String.valueOf(map.get("groupId")); String name = String.valueOf(map.get("name")); try { if (file == null) { return AjaxResult.error("file is null"); } if (groupId == null) { return AjaxResult.error("file is null"); } if (name == null) { return AjaxResult.error("file is null"); } // 转换实体 byte[] decode = Base64.decode(base64Process(file)); ImageInfo imageInfo = ImageFactory.getRGBData(decode); //人脸特征获取 byte[] bytes = faceEngineService.extractFaceFeature(imageInfo); if (bytes == null) { return AjaxResult.error(ErrorCodeEnum.NO_FACE_DETECTED.getDescription()); } List<ProcessInfo> processInfoList = faceEngineService.process(imageInfo); // 开始将人脸识别Base64转换文件流->用于文件上传 // final MultipartFile multipartFile = ImageUtils.base64ToMultipartFile(file); final SysUserFaceInfo one = this.userFaceInfoService.lambdaQuery().eq(SysUserFaceInfo::getName, name).one(); // 如果存在则更新人脸和特征 if (null != one) { this.userFaceInfoService.lambdaUpdate().set(SysUserFaceInfo::getFaceFeature, bytes) .set(SysUserFaceInfo::getFpath, "存储头像地址") .eq(SysUserFaceInfo::getFaceId, name).update(); } else { // 组装人脸实体 SysUserFaceInfo userFaceInfo = new SysUserFaceInfo(); userFaceInfo.setName(name); userFaceInfo.setAge(processInfoList.get(0).getAge()); userFaceInfo.setGender(processInfoList.get(0).getGender().shortValue()); userFaceInfo.setGroupId(Integer.valueOf(groupId)); userFaceInfo.setFaceFeature(bytes); userFaceInfo.setFpath("存储头像地址"); // 存储用户ID -> 我这里先使用name代替 -> 假如是唯一性 userFaceInfo.setFaceId(name); //人脸特征插入到数据库 userFaceInfoService.insertSysUserFaceInfo(userFaceInfo); } logger.info("faceAdd:" + name); return AjaxResult.success("人脸绑定成功!"); } catch (Exception e) { logger.error("", e); } // 错误返回 return AjaxResult.error(ErrorCodeEnum.UNKNOWN.getDescription());
faceSearch 具体业务编写
- 根据前端传递的
file -> 人脸图片
groupId -> 用户分组(用于缩小范围查询该人员的位置有效减少数据量大的问题)
String file = String.valueOf(map.get("file")); String groupId = String.valueOf(map.get("groupId")); if (groupId == null) { return AjaxResult.error("groupId is null"); } byte[] decode = Base64.decode(base64Process(file)); BufferedImage bufImage = ImageIO.read(new ByteArrayInputStream(decode)); ImageInfo imageInfo = ImageFactory.bufferedImage2ImageInfo(bufImage); //人脸特征获取 byte[] bytes = faceEngineService.extractFaceFeature(imageInfo); // 校验是否显示出人脸 if (bytes == null) { return AjaxResult.error(ErrorCodeEnum.NO_FACE_DETECTED.getDescription()); } //人脸比对,获取比对结果 List<FaceUserInfo> userFaceInfoList = faceEngineService.compareFaceFeature(bytes, Integer.valueOf(groupId)); if (CollectionUtil.isNotEmpty(userFaceInfoList)) { FaceUserInfo faceUserInfo = userFaceInfoList.get(0); FaceSearchResDto faceSearchResDto = new FaceSearchResDto(); BeanUtil.copyProperties(faceUserInfo, faceSearchResDto); List<ProcessInfo> processInfoList = faceEngineService.process(imageInfo); if (CollectionUtil.isNotEmpty(processInfoList)) { //人脸检测 List<FaceInfo> faceInfoList = faceEngineService.detectFaces(imageInfo); int left = faceInfoList.get(0).getRect().getLeft(); int top = faceInfoList.get(0).getRect().getTop(); int width = faceInfoList.get(0).getRect().getRight() - left; int height = faceInfoList.get(0).getRect().getBottom() - top; Graphics2D graphics2D = bufImage.createGraphics(); // 红色 graphics2D.setColor(Color.RED); BasicStroke stroke = new BasicStroke(5f); graphics2D.setStroke(stroke); graphics2D.drawRect(left, top, width, height); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(bufImage, "jpg", outputStream); byte[] bytes1 = outputStream.toByteArray(); faceSearchResDto.setImage("data:image/jpeg;base64," + Base64Utils.encodeToString(bytes1)); faceSearchResDto.setAge(processInfoList.get(0).getAge()); faceSearchResDto.setGender(processInfoList.get(0).getGender().equals(1) ? "女" : "男"); } return AjaxResult.success(faceSearchResDto); } return AjaxResult.error(ErrorCodeEnum.FACE_DOES_NOT_MATCH.getDescription());
测试接口流程
准备两张图片
百度随便搜索个在线转换 Base64
1、人脸添加
新增有脸的
新增测试无脸的
2、人脸识别
有脸的
无脸的
结尾在线演示
(前往享受人脸识别)
Json 序列化框架导致 CPU 使用率过高
陈叔叔 发表了文章 • 0 个评论 • 60 次浏览 • 2022-05-20 23:48
问题现象:CPU 负载过高
我们线上的 jenkins 系统,时不时会发生 CPU 负载过高的现象。
CPU 负载过高后,SRE 同学会收到电话告警。
在我们的监控系统中,可以看到,某些时候,CPU 的负载确实会很高,如下图:
问题排查
Jenkins 系统本身是一个 Java 程序,应对 Java 程序导致的 CPU 使用率过高这一问题,GitHub 上有现成的解决方案:show-busy-java-threads。
下载链接如下:
- GitHub:show-busy-java-threads
- Gitee:show-busiest-java-threads
登录上机器,在 CPU 使用率高时候,执行 show-busy-java-threads 脚本:./show-busy-java-threads
。
摘选其中的一些输出如下:
The stack of busy(25.0%) thread(20239/0x4f0f) of java process(248927) of user(jenkins):"Handling GET /job/jenkins-test-job/api/json from 172.168.1.1 : qtp1641808846-3127" #3127 prio=5 os_prio=0 tid=0x00007f7380014000 nid=0x4f0f runnable [0x00007f722c392000] java.lang.Thread.State: RUNNABLE at java.util.Arrays.copyOfRange(Arrays.java:3664) at java.lang.String.<init>(String.java:207) at java.lang.String.substring(String.java:1933) at net.sf.json.util.JSONTokener.matches(JSONTokener.java:110) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:912) at net.sf.json.JSONObject.fromObject(JSONObject.java:156) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348) at net.sf.json.JSONArray._fromJSONTokener(JSONArray.java:1131) at net.sf.json.JSONArray.fromObject(JSONArray.java:125) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:351) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955) at net.sf.json.JSONObject.fromObject(JSONObject.java:156) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955) at net.sf.json.JSONObject.fromObject(JSONObject.java:156) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955) at net.sf.json.JSONObject.fromObject(JSONObject.java:156) at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348) at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955) at net.sf.json.JSONObject._fromString(JSONObject.java:1145) at net.sf.json.JSONObject.fromObject(JSONObject.java:162) at net.sf.json.JSONObject.fromObject(JSONObject.java:132) at sam.Sam.sendRequestReturnJson(Sam.java:517) at sam.Sam.getPermissionByUser(Sam.java:225) at sam.Sam.checkUserPermissionLocal(Sam.java:243) at com.michelin.cio.hudson.plugins.rolestrategy.PermissionCache.getPermissionSam(RoleMap.java:155) at com.michelin.cio.hudson.plugins.rolestrategy.PermissionCache.getPermission(RoleMap.java:106) at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap.hasPermission(RoleMap.java:220) at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap.access$000(RoleMap.java:166) at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap$AclImpl.hasPermission(RoleMap.java:569) at hudson.security.SidACL._hasPermission(SidACL.java:70)
从上面的输出可以看到,25.0% 的 CPU 资源在处理 Handling GET /job/jenkins-test-job/api/json from 172.168.1.1
这个请求。
运维同学根据这个 ip ,定位到发起请求的是某同学 A。这个同学在跑一些定时任务,定时拉取 job 的执行结果。
问题是当我直接访问这个接口:/job/jenkins-test-job/api/json 时,返回并不慢,几乎很快就可以返回。问题应该不是这个接口的问题。
我们接着从 ./show-busy-java-threads
输出往下看:看到其中有问题的调用栈:
at net.sf.json.JSONObject.fromObject(JSONObject.java:132)at sam.Sam.sendRequestReturnJson(Sam.java:517)at sam.Sam.getPermissionByUser(Sam.java:225)at sam.Sam.checkUserPermissionLocal(Sam.java:243)
看起来是这个 Sam 校验用户权限导致的 CPU 使用率过高,而接着看上面的代码 net.sf.json.JSONObject.fromObject
,这个是在做 json 的反序列化。
通常来说,json 的序列化、反序列化都是比较费 CPU 的,更糟糕的是,这里用到的 json 序列化框架是 net.sf.json,而不是 Java 常用的 jackson 和 gson 等。
直觉告诉我,肯定是这个 net.sf.json 反序列化引起的 CPU 使用率过高问题。
备注:
通过跟之前维护 jenkins 的同学了解到,他们基于 role-strategy 插件,重写了 jenkins 权限验证逻辑,用的就是 Sam 权限。翻看 sam 权限插件的代码,确实有用 net.sf.json 做 json 反序列化。
到这里,定位到大概率是 Sam 权限插件的 net.sf.json 反序列化引起的问题。
问题复现
为了验证这个问题,我们拿到 Sam 权限插件的代码。找到出问题的关键代码:
public void getPermissionByUser(String email) { JSONObject params = new JSONObject(); params.put("user_email", email); params.put("subsystem_id", SAM_JENKINS_SUM_SYSTEM_ID); JSONObject res = sendRequestReturnJson(URL, "GET", params); if (res.get("success").equals(true)) { cacheUserPermission(params.getString("user_email"), res.getJSONObject("permission").getJSONObject(email).getJSONObject("SERVICE")); }}public static JSONObject sendRequestReturnJson(String endpoint, String method, JSONObject params) { if (method.equals("POST")) { return JSONObject.fromObject(sendPostRequest(endpoint, params)); } else if (method.equals("GET")) { return JSONObject.fromObject(sendGetRequest(endpoint, params)); } return new JSONObject();}
可以看到,这段代码会根据用户邮箱,发送 http 请求调用 Sam 系统,获取用户的权限数据,然后将数据反序列化成 JSONObject,即:
JSONObject.fromObject(sendGetRequest(endpoint, params, token))
在本地,通过复现 A 同学的请求,发现这个请求确实比较慢,而且费 CPU。通过 debug 得知,这个用户返回的 json 数据有 1M 左右,json 反序列化 CPU 打满。
而通过其他用户请求,发现处理很快,返回的 json 数据也比较小。
到这里,确认就是 net.sf.json 框架的反序列化性能问题,引起的 CPU 使用率过高。我们需要替换成其他高性能的 json 序列化框架。
备选有:gson、jackson、fastjson等。fastjson 因为经常出安全漏洞,暂不考虑,我们考虑从 gson、jackson 选择一个。
在选定之前,先对 gson、jackson, 的性能做个基准测试,并与 net.sf.json 做对比。
JMH 基准测试 json 框架性能
Json 框架的性能测试,我们选用 JMH 框架。
JMH 框架是 JDK 官方提供的性能基准测试套件,参考:https://github.com/openjdk/jmh
代码如下:
import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.gson.Gson;import com.google.gson.JsonObject;import net.sf.json.JSONObject;import org.apache.commons.io.FileUtils;import org.openjdk.jmh.annotations.Benchmark;import org.openjdk.jmh.annotations.BenchmarkMode;import org.openjdk.jmh.annotations.Mode;import org.openjdk.jmh.annotations.OutputTimeUnit;import org.openjdk.jmh.annotations.Param;import org.openjdk.jmh.annotations.Scope;import org.openjdk.jmh.annotations.Setup;import org.openjdk.jmh.annotations.State;import org.openjdk.jmh.results.format.ResultFormatType;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.RunnerException;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;import org.springframework.util.ResourceUtils;import java.io.File;import java.io.IOException;import java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MILLISECONDS)@State(Scope.Benchmark)public class JsonBenchmark { @Param({"10", "100", "500"}) private int length; private String json; private String email = "a@123.com"; private String path = "classpath:sam.json"; @Benchmark public void testGson() throws IOException { Gson gson = new Gson(); JsonObject root = gson.fromJson(json, JsonObject.class); if (root.getAsJsonObject("success").getAsBoolean()) { JsonObject services = root.get("permission").getAsJsonObject() .get(email).getAsJsonObject() .get("SERVICE").getAsJsonObject(); System.out.println(services.size()); } } @Benchmark public void testJackson() throws IOException { ObjectMapper objectMapper = new ObjectMapper(); JsonNode root = objectMapper.readTree(json); if (root.get("success").asBoolean()) { JsonNode services = root.get("permission").get(email).get("SERVICE"); System.out.println(services.size()); } } @Benchmark public void testJsonObject() throws IOException { JSONObject root = JSONObject.fromObject(json); if (root.get("success").equals(true)) { JSONObject services = root.getJSONObject("permission").getJSONObject(email).getJSONObject("SERVICE"); System.out.println(services.size()); } } @Setup public void prepare() throws IOException { File file = ResourceUtils.getFile(path); json = FileUtils.readFileToString(file); } public static void main(String[] args) throws RunnerException { Options options = new OptionsBuilder() .include(JsonBenchmark.class.getSimpleName()) .forks(1) .warmupIterations(5) .measurementIterations(2)// .output("/Users/wxweven/Benchmark.log") .result("result.json") .resultFormat(ResultFormatType.JSON) .build(); new Runner(options).run(); }}
测试的结果如下:
Benchmark (length) Mode Cnt Score Error UnitsJsonBenchmark.testGson 10 avgt 2 7.979 ms/opJsonBenchmark.testGson100 avgt 2 8.958 ms/opJsonBenchmark.testGson500 avgt 2 9.975 ms/opJsonBenchmark.testJackson 10 avgt 2 10.393 ms/opJsonBenchmark.testJackson 100 avgt 2 12.214 ms/opJsonBenchmark.testJackson 500 avgt 2 10.548 ms/opJsonBenchmark.testJsonObject 10 avgt 2 1350.788 ms/opJsonBenchmark.testJsonObject 100 avgt 2 1350.583 ms/opJsonBenchmark.testJsonObject 500 avgt 2 1381.046 ms/op
可以看到,gson 和 jackson 性能接近,但是 jsonlib 性能就很差,比另外两个慢 100 多倍。
综合考虑性能、api 易用性等,选定 gson 作为替代方案。
替换成 gson
将之前的代码替换成 gson,代码如下:
public void getPermissionByUser(String email) { JSONObject params = new JSONObject(); params.put("user_email", email); params.put("subsystem_id", SAM_JENKINS_SUM_SYSTEM_ID); JsonObject res = sendRequestReturnJsonV2(URL, "GET", params); if (res.get("success").getAsBoolean()) { cacheUserPermission(params.getString("user_email"), res.getAsJsonObject("permission").getAsJsonObject(email).getAsJsonObject("SERVICE")); } }public static JsonObject sendRequestReturnJsonV2(String endpoint, String method, JSONObject params) throws IOException { if (method.equals("POST")) { return GSON.fromJson(sendPostRequest(endpoint, params, token), JsonObject.class); } else if (method.equals("GET")) { return GSON.fromJson(sendGetRequest(endpoint, params, token), JsonObject.class); } return new JsonObject();}
重新编译权限插件后上线,再次查看 CPU 负载监控,发现 CPU 负载确实降下来了(05/13晚上 0 点左右上线的)。
再次重新编译,问题得到解决。
结束语
这个问题,前前后后花费了不少时间,也困扰了 DevOps 团队比较久,经过大家的齐心协力,总算是把问题给解决了。
这篇文章也是对之前排查、解决问题的一个总结。
同时,也提醒大家,在使用第三方 jar 包的时候,一定要注意该 jar 包有没有性能、安全等问题。如果不确定的话,可以用 JMH 等手段自己测试以下。
我是梅小西,最近在某东南亚电商公司做 DevOps 的相关事情。从本期开始,将陆续分享基于 Jenkins 的 CI/CD 工作流,包括 Jenkins On k8s 等。
如果你对 Java 或者 Jenkins 等感兴趣,欢迎与我联系,微信:wxweven(备注 DevOps)
本文由博客群发一文多发等运营工具平台 OpenWrite 发布
Netty源码研究笔记(2)——Bootstrap系列
陈叔叔 发表了文章 • 0 个评论 • 51 次浏览 • 2022-05-20 19:30
1. Netty源码研究笔记(2)——Bootstrap系列
顾名思义,Bootstrap
是netty提供给使用者的脚手架,类似于Spring的ApplicationContext
,通过Bootstrap
我们使用一些自定义选项,将相关的组件打包起来,从而快速的启动服务器、客户端。
Bootstrap
分为:Bootstrap
、ServerBootstrap
两种,其中前者用于构建客户端、后者用于构建服务端。
Bootstrap
的实现使用的是典型的建造者模式,使用这种模式的好处是使得构造对象时可读性更好,更不容易出错(相比于使用构造函数而言),并且可以在构造的时候进行校验。
Bootstrap
系列还包含相关的BootstrapConfig
。
BootstrapConfig
的实现类似外观模式,只暴露其内部持有Bootstrap
的配置信息。(配置信息实际放在Bootstrap
中)。
1.1. 继承关系
1.2. AbstractBootstrap
注意:bind
方法定义在AbstractBoostrap
中。
在使用点对点的传输时,一般在Server端使用bind
,在Client端使用connect
。
如果bind
方法不是通过ServerBootstrap
使用,那么对应的是无连接的传输场景,比如:UDP。
AbstractBootstrap
是一个泛型类,它的泛型参数是其实现类,这样便可以使其链式调用方法返回的类型其具体的实现类。
忽略EMPTY_OPTION_ARRAY
、EMPTY_ATTRIBUTE_ARRAY
,这两者用于提供泛型方法的类型入参,从而指示其返回类型,没有实际意义。
我们可以看到在AbstractBootstrap
中需要配置:
- 关联的EventLoopGroup(必选)
- 监听的local socket address:对于端到端的server必选,对于无连接的也必选
- channel handler:必选,一般是channel initializer,负责将需要的channel handler添加到channel pipeline中。
- channel option(可选)
- channel attribute(可选)
- channel factory:一般使用默认的channel factory,它使用反射机制来创建channel
bind
、connect
方法在前文已经分析过了,这里我们看未谈到的validate
、init
方法。
validate:
AbstractBootstrap
只校验了EventLoopGroup
。
init:
子类中实现。
1.3. ServerBootstrap
在ServerBootstrap
需要配置:
- child channel option:可选,表示和客户端建立的传输信道的配置项。
- child channel attibute:可选,表示和客户端建立的传输信道的属性项。
- child channel handler:必填,一般是channel initializer,表示对客户端请求的处理逻辑
- child event loop group:可选,表示客户端请求处理器的执行线程组。
validate:
校验child channel handler
init:
ServerBootstrap::init
方法给channel pipeline 添加用户指定的channel handler,以及一个ServerBootstrapAcceptor
类型的channel handler,它使得服务端可以和客户端建立端到端的连接。
ServerBootstrapAcceptor
以后再谈。
1.4. Bootstrap
validate:
验证channel handler是否存在,即判断对server的response的处理器是否定义了。
init:
给channel pipeline添加用户指定的channel handler。
1.5. AbstractBootstrapConfig、ServerBootstrapConfig、BootstrapConfig
AbstractBootstrapConfig:
ServerBootstrapConfig:
BootstrapConfig:
吊炸天,Spring Security还有这种用法!
陈叔叔 发表了文章 • 0 个评论 • 44 次浏览 • 2022-05-20 10:59
在用Spring Security项目开发中,有时候需要放通某一个接口时,我们需要在配置中把接口地址配置上,这样做有时候显得麻烦,而且不够优雅。我们能不能通过一个注解的方式,在需要放通的接口上加上该注解,这样接口就能放通了。答案肯定是可以的啦,今天我们一起来看看实现过程吧。
SpringBoot版本
本文基于的Spring Boot的版本是2.6.7
实现思路
- 新建一个
AnonymousAccess
注解,该注解是应用于Controller
方法上的 - 新建一个存放所有请求方式的枚举类
- 通过判断
Controller
方法上是否存在该注解 - 在
SecurityConfig
上进行策略的配置
实现过程
新建注解
@Inherited@Documented@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface AnonymousAccess { }
新建请求枚举类
该类是存放所有的请求类型的,代码如下:
@Getter@AllArgsConstructorpublic enum RequestMethodEnum { /** * 搜寻 @AnonymousGetMapping */ GET("GET"), /** * 搜寻 @AnonymousPostMapping */ POST("POST"), /** * 搜寻 @AnonymousPutMapping */ PUT("PUT"), /** * 搜寻 @AnonymousPatchMapping */ PATCH("PATCH"), /** * 搜寻 @AnonymousDeleteMapping */ DELETE("DELETE"), /** * 否则就是所有 Request 接口都放行 */ ALL("All"); /** * Request 类型 */ private final String type; public static RequestMethodEnum find(String type) { for (RequestMethodEnum value : RequestMethodEnum.values()) { if (value.getType().equals(type)) { return value; } } return ALL; }}
判断Controller
方法上是否存在该注解
在SecurityConfig
类中定义一个私有方法getAnonymousUrl
,该方法主要作用是判断controller那些方法加上了AnonymousAccess
的注解
private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) { Map<String, Set<String>> anonymousUrls = new HashMap<>(8); Set<String> get = new HashSet<>(); Set<String> post = new HashSet<>(); Set<String> put = new HashSet<>(); Set<String> patch = new HashSet<>(); Set<String> delete = new HashSet<>(); Set<String> all = new HashSet<>(); for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) { HandlerMethod handlerMethod = infoEntry.getValue(); AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class); if (null != anonymousAccess) { List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods()); RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name()); switch (Objects.requireNonNull(request)) { case GET: get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case POST: post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case PUT: put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case PATCH: patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case DELETE: delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; default: all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; } } } anonymousUrls.put(RequestMethodEnum.GET.getType(), get); anonymousUrls.put(RequestMethodEnum.POST.getType(), post); anonymousUrls.put(RequestMethodEnum.PUT.getType(), put); anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch); anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete); anonymousUrls.put(RequestMethodEnum.ALL.getType(), all); return anonymousUrls; }
在SecurityConfig
上进行策略的配置
通过一个SpringUtil
工具类获取到requestMappingHandlerMapping
的Bean
,然后通过getAnonymousUrl
方法把标注AnonymousAccess
接口找出来。最后,通过antMatchers
细腻化到每个 Request 类型。
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { // 搜寻匿名标记 url: @AnonymousAccess RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) SpringUtil.getBean("requestMappingHandlerMapping"); Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); // 获取匿名标记 Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap); httpSecurity //禁用CSRF .csrf().disable() .authorizeRequests() // 自定义匿名访问所有url放行:细腻化到每个 Request 类型 // GET .antMatchers(HttpMethod.GET,anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll() // POST .antMatchers(HttpMethod.POST,anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll() // PUT .antMatchers(HttpMethod.PUT,anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll() // PATCH .antMatchers(HttpMethod.PATCH,anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll() // DELETE .antMatchers(HttpMethod.DELETE,anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll() // 所有类型的接口都放行 .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll() // 所有请求都需要认证 .anyRequest().authenticated(); }
在Controller方法上应用
在Controller上把需要的放通的接口上加上注解,即可不需要认证就可以访问了,是不是很方便呢。例如,验证码不需要认证访问的,代码如下:
@ApiOperation(value = "获取验证码", notes = "获取验证码") @AnonymousAccess @GetMapping("/code") public Object getCode(){ Captcha captcha = loginProperties.getCaptcha(); String uuid = ""+IdUtil.simpleUUID(); //当验证码类型为 arithmetic时且长度 >= 2 时,captcha.text()的结果有几率为浮点型 String captchaValue = captcha.text(); if(captcha.getCharType()-1 == LoginCodeEnum.ARITHMETIC.ordinal() && captchaValue.contains(".")){ captchaValue = captchaValue.split("\.")[0]; } // 保存 redisUtils.set(uuid,captchaValue,loginProperties.getLoginCode().getExpiration(), TimeUnit.MINUTES); // 验证码信息 Map<String,Object> imgResult = new HashMap<String,Object>(2){{ put("img",captcha.toBase64()); put("uuid",uuid); }}; return imgResult; }
效果展示
更多经常的内容请关注公众号"攻城狮成长日记"