在JVM的运行时数据区包括:方法区、虚拟机栈、本地方法栈、堆、程序计数器。而虚拟机栈描述的是JAVA方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

对于开头提到的信息相信每个对JVM有了解的人都明白,但是刚看到栈帧中的操作数栈,并不知道是做什么的?我不知道大家有没有这样的经历,知道有这么一个操作数栈,但是具体是做什么的,运行过程是什么样儿的,并不是很清楚

下面我们看两个表达式

波兰表达式法:标准四则运算表达式, 也称 中缀表达式
就是我们从小学习的四则运算的表达式。
例1:9+(3-1)*3+10/2 = ?

逆波兰表达式法 - 后缀表达式
上述例1,转为后缀表达式为:9 3 1 - 3 * + 10 2 / +

我们在计算例1的中缀表达式时,我们可以计算出结果是20,但是这样的描述,计算机无法实别,而计算机通过栈结构+后缀表达式,可以压栈出栈,得出表达式的结果。

操作数栈的执行过程

数字入栈,操作符,从栈中弹出操作数,计算结果,结果入栈

第一步: 将后缀表达式从头开始依次压入栈
图片.png
第二步:当遇到操作符“-”时,从栈中弹出两个操作数,第一个弹出的作为减数,第二个作为被减数,进行运算,计算出结果为2

图片.png
第三步:将计算出的结果入栈:

图片.png

第四步:将3压入栈中
图片.png
第五步:处理”*”
从栈中弹出3 作为乘数 弹出2作为被乘数,并将结果6压入栈中,结果如下图

图片.png
第六步:处理”+”
从栈中弹出6作为加数弹出9作为被加数,计算 9+6=15,将结果15入栈
图片.png

第七步:10入栈,2 入栈
图片.png
第八步:处理 “/“
弹出操作数2作为除数,10作为被除数,10/2=5,将结果5入栈
图片.png
第九步:处理”+”
弹出操作数5作为加数,操作数15作为被加数,15+5=20,20即为结果
上述是整个操作数栈的执行过程。

讲到这里,大家应该能明白JVM栈中的操作数栈的具体作用和执行过程了,但是有一个问题还没有解决,中缀表达式是怎么转换后后缀表达式?接下来我们看一下转换过程,其实在中缀转后缀的过程中,使用到的数据结构也是栈。

转换规则

- 1、数字输出
- 2、运算符进栈
- 3、括号匹配出栈
- 4、如栈顶优先级高,则输出后一位数字,再出栈操作符

转换过程

为了方便读者观看,我们把要转换的中缀表达式,在这里再显示一次,中缀表达式:9+(3-1)3+10/2
第1步:根据上面的规则,数字9、3、1、输出,操作符+(-依次进栈,可以得到,栈中的内容如下图:
图片.png
第2步:这里需要注意一下,因为规则中,扩号匹配出栈,怎么理解呢,就是把左号到当前栈顶的符号依次弹出栈,加到输出结果中,
图片.png
第3步:处理”
“,这里要注意一个,这个符合规则 的第4条,“”优先级要比现栈顶符号优先级高,所以先输出后面的数字3,然后将*入栈,再依次将操作符出栈。

图片.png

第4步: “+ “入栈,输出10
图片.png
第5步:这时候和第三步情况一下,”/“优先级高于栈顶操作符“+”,所以先输出“/”后面的数字“2”,将”/“入栈, 依次将栈中操作符出栈,并输出
图片.png

最终结果就是我们所需要的后缀表达式:图片.png

使用java代码模拟jvm操作数栈的计算过程

相关代码传送门:
https://github.com/18921569386/datastructure/blob/master/datastructure-base/src/main/java/com/albk/datastructure/base/statck/ext/OperateStack.java

上篇疑问

JVM篇 之 垃圾收集器中最后留一了一个问题
为什么CSM不直接使用标记压缩算法?
主要原因是,因为CMS垃圾回收是和用户线程一起运行的,如果使用标记压缩算法的话,就会导致大量在使用中的对象在堆中寻找不到,所以无法使用此算法。

class装载验证流程

自底向上检查,自上向下尝试加载

加载

此阶段是装载类的第一个阶段 ,负责通过各种方式获取类的二进制流,转为方法区数据结构,并在JAVA堆中生成对应的java.lang.Class对象

连接

连接过程又分为三个步骤,验证、准备、解析

验证

验证目的是为了保证Class流的格式是正确的,一定类通过了字节码检查,并不代表它一定没有问题,但是如果一个类没有通过字节码查检,那它一定是有问题,不能运行的。

  • 文件格式的验证

    主要是保证输入的字符流能正确地解析并存储于方法区之内,格式上符合一个Java类型信息的要求

    ** 是否以0xCAFEBABE开头
    ** 版本号是否合理

    ** 检查常量TAG标志是否是被支持的常量类型

    …..

  • 元数据验证

    目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息,如果文件格式校验阶段是对文件结构的校验,那么该了阶段就是对文件内容在使用上是否满足约定的检查,有些像IDEA编译器的类一部分检测内容

    ** 是否有父类,Object之外所有的类都应该有父类
    ** 这个类的父类是否被final修饰了?
    ** 如果当前类是非抽象类是否实现了所有的抽象方法?

    ** 类中的字符、方法是否与父类产生了矛盾?

    ….

  • 字节码验证

    在前面两个阶段,对类的的结构和使用规范上进行了校验,那么该阶段主要是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害JVM安全的行为,

    ** 运行检查
    ** 栈数据类型和操作码数据参数吻合
    ** 跳转指令指定到合理的位置

    ** 方法体中的类型转换是否有效

  • 符号引用验证

前3步已经对类的内容进行了校验,该阶段的主要是对当前类自身以外的信息进行匹配性校验,目的是保证解析 动作能正常执行

** 符号引用中通过字符串描述的全限定名是否能找到对应的类

** 符号引用的类、字段 、方法的访问性,是否可以被当前类访问

** 符号引用的类、字段、方法是否存在

准备

分配内存,并为类设置初始值 (方法区中)

  • public static int v=1;
    在准备阶段中,v会被设置为0
    在初始化的中才会被设置为1

  • 对于static final类型,在准备阶段就会被赋上正确的值,因为static final类型被认为是常量,要保证在以后用到的过程中已经被赋于正确的值
    public static final int v=1;

    解析

符号引用替换为直接引用

  • 符号引用:以一组符号情迷描述所引用的目标,符号可以是任何形式的字面量,只要使用时,能无歧义地定位到目标即可。符号引用与JVM实现的内存布局无关,引用的目标并不一定已经加载到内存中。各JVM实现的内在布局可以是不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。比如 java.lang.Object, 存放在常量池中。
  • 直接引用:是指直接指向常量池中目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。

    为什么要将符号引用为直接引用?
    因为符号引用只是一种表示方式,无法直接使用,所以在解析阶段会被替换为直接引用,在使用时候需要知道,该字符串具体在内在中的哪个地址。
    解析动作的解析范围如下:

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

初始化

在准备阶段,已经给变量赋于默认值,在初始化阶段主要是给类变量进行赋正确的值

执行类构造器

  • 对static变量赋于正确的值
  • static{} 静态代码块语句
    此阶段的一些约定:
  • 子类的调用前保证父类的被调用
  • 是线程安全的

思考:

NoSuchMethodError, NoSuchFieldError,IllegalAccessError等错误发生在哪个阶段?

验证阶段的符号引用验证

##前言

本篇讲一下各种GC之间的细微差异讲的清楚的,希望通过本篇文章,读者可以对GC的种类有更深刻的了解。
对于GC算法的可参数文章JVM篇之 GC算法

一、串行收集器-Serial

Serial收集器最古老的,最稳定的,历经考验,内部的BUG比较少的一个收集器,单个GC线程进行垃圾回收。

会作为CMS收集器降级处理时会使用此收集器。

JVM GC参数

增加JVM参数  -XX:+UseSerialGC 使用串行收集器,启用此参数后
- 新生代、老年代使用串行回收
- 新生代使用复制算法
- 老年代使用标记-压缩

回收过程如下图:

串行收集器.png
通过上图,可以看出在应用程序线程到达安全点后,会全部暂停,然后运行GC线程(单线程),GC线程线程执行完成之后,再开始执行应用线程。

缺点:因为是串行的,只使用一个线程进行回收,所以会产生较长的停顿时间,在多核的CPU上,是无法发挥CPU的性能

GC日志

Serial收集器GC日志 .png

当看GC日志有红色加粗显示的标识时,说明当前使用的是Serial收集器GC

二、并行收集器-ParNew

Serial收集器新生代并行版本,与Serial收集器相比只影响新生代的回收,此收集器需要多核cpu的支持,GC性能会更好。

JVM GC参数

增加JVM参数  -XX:+UseParNewGC ,启用ParNew收集器,启用之后
-新生代并行
-新生代使用复制算法
-老年代串行
-老年代使用标记-压缩算法

由于新生代是并行回收,可以通过参数来调整并行回收的线程数量

  -XX:ParallelGCThreads 限制线程数量

ParNew收集器与Serial收集器主要区别在于,新生代的回收,前者是多线程回收,后者是单线程回收

回收过程如下图:

 ParNew收集器.png
通过上图,可以看出在应用程序线程到达安全点后,会全部暂停,然后运行多个GC线程去做垃圾回收,GC线程线程执行完成之后,再开始执行应用线程。

GC日志

在GC日志中出现上图中红色加粗显示的标识时,说明当前GC使用的是ParNew收集器,如下图显示ParNew GC日志.png

三、Parallel收集器

  • Serial收集器在新生代和老年代的多线程版收集器
  • ParNew收集器,在老年代的并行版收集器

- 新生代复制算法
- 老年代 标记-压缩
- 更加关注吞吐量

JVM GC参数

-XX:+UseParallelGC 
  - 新生代使用Parallel收集器 
  - 老年代串行
-XX:+UseParallelOldGC
  - 新生代使用Parallel收集器
  - 并行老年代

回收过程如下图

图片.png

GC日志

在GC日志中出现上图中红色加粗显示的标识时,说明当前GC使用的是Parallel收集器,如下图显示!Parallel GC日志.png

其它JVM参数

由于Parallel收集器比较关注吞吐量,所以还提供了一些配套的参数来控制GC的时间和占比

-XX:MaxGCPauseMills(停顿时间)
    设置GC的最大停顿时间,单位毫秒
    GC尽力保证回收时间不超过设定值
    作为一个GC的目标值
-XX:GCTimeRatio(可理解为吞吐量)
    GC所使用的CPU时间占用总时间的百分比
    在0-100的取值范围
    默认99,即最大允许1%时间做GC

通常情况下,我们希望GC停顿时间短 ,同时又希望吞吐量高,但事实上这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优。在GC工作负载不变的情况下

  • 如果提高GC的频率,因为频率高了,所以每次GC要处理的垃圾就少了,GC速度自然速度就会加快,但是频繁的GC会对系统的整体性能有损伤的,就会出现GC停顿时间短了,但是系统整体性能并不会很好。
  • 如果降低GC的频率,自然而然每次要处理的垃圾就会增加,所以会导致每次GC的时间相对就会增加,但是对系统的整体性能是有所上升的。
对吞吐量的理解
  • cpu分到应用程序上的时间越多,吞吐量自然就会增加
  • cpu分到GC线程上的时间越多,处理应用线程就会越少。
    所以吞吐量,可以用应用线程占用CPU时间的长短来衡量。

在停顿时间和吞吐量两者上,在GC工作负载不变的情况下,不可能两者同时提高,个人认为除非优化GC算法,从根本上降低GC的工作负载。

所以在调试停顿时间 和吞吐量这两个参数时,要根据实际情况来调整。

四、CMS收集器

CMS(Concurrent Mark Sweep) 并发标记清除,并发的意思是与用户线程一起运行,从名字上可以理解,此算法是使用标记-清除算法,比较关注停顿时间 。

JVM GC参数

增加JVM参数  -XX:+UseConcMarkSweepGC,启用CMS收集器,启用之后
- 新生代使用ParNew收集器
- 新生代使用复制算法
- 老年代使用CMS收集器
- 老年代使用并发标记-清除

回收过程

因为CMS收集器要与用户线程一起运行,所以它的算法和实现机制比较复杂,主要工作可以分为五步:

  • 初始标记

    1
    2
    3
    - 标识根节点直可达的对象
    - 速度快
    - 会产生STW
  • 并发标记(和用户线程一起)

    1
    2
    -  并发标记存活对象
    - 占用时间相对较长
  • 重新标记

    1
    2
    - 由于并发标记时,用户线程依然运行因以在正式清理前,对有变化的对象,所以需要修正,对对象重新标记
    - 会产生STW
  • 并发清除(和用户线程一起)

    1
    -  基于标记结果,直接清理不可达的对象
  • 并发重置

    1
    - 为下一次CMS GC做准备
回收过程如下

cms回收过程.png

GC日志

 CMS-initial-mark  初始标记
 CMS-concurrent-mark  并发标记
 CMS-remark   重新标记
 CMS-concurrent-sweep  并发清除
 CMS-concurrent-reset  并发重置

具体的日志内容如下:图片.png

cms收集器的特点
  • 尽可能降低停顿
  • 会影响系统整体吞吐量和性能
    比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
  • 清理不彻底
    因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
  • 与标记-压缩相比会产生内存碎片
  • 并发阶段会降低吞吐量

因为CMS收集器和用户线程一起运行,所以不能在空间快满时再清理,因为与用户线程同时进入,如果在快满的时候进行清理的话,很容易出两用户线程申请内存,出现concurrent mode failure的情况,所以JVM考虑到这点,提供相应的参数进行设置触发GC的阈值

    -XX:CMSInitiatingOccupancyFraction设置触发GC的阈值
    -XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
    -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发

即使设置了阈值也不能保证不会出现并发收集的错误 ,如果不幸内存预留空间不够,就会引起concurrent mode failure,当发生这种情况之后,CMS收集器会降级到Serial收集器进行垃圾回收,这时候会暂停用户线程,会产生一个长时间的停顿来回收垃圾。
下图是发生concurrent mode failure时的GC日志concurrent mode failure.png

有关碎片

因为CMS采用标记清除算法,所以会产生大量的内存碎片,所以JVM提供了两个参数来对碎片进行整理
#
-XX:+ UseCMSCompactAtFullCollection
Full GC后,进行一次整理
整理过程是独占的,会引起停顿时间变长(因为要移动大量的存活对象)
-XX:+CMSFullGCsBeforeCompaction
设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads
设定CMS的线程数量

五、各GC收集器对比

收集器 JVM参数 新生代 老年代
Serial -XX:+UseSerialGC 串行、复制算法 串行、标记压缩算法
ParNew -XX:+UseParNewGC 并行、复制算法 串行、标记压缩算法
Parallel -XX:+UseParallelGC -XX:+UseParallelOldGC 串行或并行、复制算法 串行或者并行、标记压缩算法
CMS -XX:+UseConcMarkSweepGC 并行、复制算法 并发、标记清除算法

六、疑问

  • 为什么CMS不直接使用标记-压缩算法呢?

作者:BK
https://www.jianshu.com/u/a5230c4f0b7a
鉴于本人才疏学浅,不足之处还望斧正,也欢迎关注我,无特殊说明的都是自己一字一句码出来的,尊重原创,如果转载请说明出处!

–引用计数法

是一个老牌垃圾回收算法,通过引用计算来标记,判断一个对象是不是垃圾,是不是要回收。

基本思想 : 为每一个对象都标记一个引用数量,引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

引用计数

对于上图,根对象和ABC三个对象,有互相引用的关系,ABC引用计数分别为1

当A对B的引用失去之后,B的引用变成了0,根据引用计数的规则,引用为0 的B对象就会当作垃圾回收掉,回收之后堆中只剩下根对象和AC。

引用计数法带来的问题

1、引用和去引用伴随加法和减法,,因为要实时的计算当前对象有多少个引用,对象的引用和消是无时无刻存在的,所以对影响有一定性能

2、很难处理循环引用的问题,看下面的问题,
循环引用
上图中,根对象引用A,引用C,C引用B,B又引用A,所以引用计算A(2),C(1),B(1),当,根对象失去对A的引用时,由于A,B,C三者互相引用,所以三个对象的引用计数都是1,所以A,B,C相当于一个独立体,无法被回收,如果堆中存在大量这样的无用对象,就会导致不停的GC,而GC后又无效果,最终内存溢出

–标记清除

    标记-清除算法是现代垃圾回收算法的思想基础。    

    基本思想:标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点做搜索,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

标记清除
在标记清除算法中,大致总结思路如下:从根节点开始做两件事情

1、标活(标记可达对象),标记A,B,C,D,E是可达对象

2、去死 (去除不可达对象),去除黑色区域不可达对象

通过上面图可以反应出,从堆中清空垃圾对象后,堆中的可用空间比较分散不连续,这时候如果有一个大的对象需要申请6个连续的空间,是无法分配,会造成大量的空间碎片。标记压缩算法可以避免这种情况的发生。

–标记压缩

基本思路:在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

执行过程如下图:
标记压缩
标记压缩
大致总结思路如下:从根节点开始做两件事情

1、标活

2、移动存活对象到一端

3、清除边界外的所有空间

标记压缩对标记清除而言,有什么优势呢?

相比与标记清除算法,清理之后,会有大量的连续空间,不会有空间碎片,标记-压缩算法适合用于存活对象较多的场合,如老年代。

–复制算法

基本思想 :将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

复制算法两空一样大小的空间,下面是复制算法的执行过程:
标记压缩
复制算法
1、标活

2、复制存活对象从FROM区域到TO区域

3、交换FROM与TO的角色

与标记-清除算法相比,复制算法是一种相对高效的回收方法,在现在的JVM中,复制算法一般会用于年轻代,但是复制算法的最大问题是:空间浪费,每次只使用一半的空间。

可以想想,比如为复制算法分配8G的空间,但是在使用过程中会浪费4G的空间,当然8G的复制空间也只是举个例子,为了更能突出空间的浪费 ,因为复制算法本身的特性决定 ,是需要将存活对象进行一次复制 ,所以复制空间一般都不会很大,因为大的空间,如果在极端情况下,会涉及大量存活对象的移动,这种大量的移动对于系统本身,无疑问是灾难性的。

在现在的JVM中也不会很大,每一块复制区域默认占用1/10的空间。比如,有10M的空间默认的,FROM和TO区域分别占用1M。

所以一般整合标记清理思想,对复制算法会做一些改进,如下图:
标记压缩
复制算法改进
因为复制空间越大,浪费的空间越大,复制算法空间一般不会很大,一般在复制过程 中会了现两种情况

第一种:对象太大,放不到复制空间

第二种:即使大对象放得下,也会导致大量的小对象无法存放,会导致大量的不符合老年代特性的小对象进行老年代

所以复制算法一般需要一个担保空间,如上图中蓝色区域。

比如:C、D、F是大对象,在一执行复制算法后,会直接进入到担保空间。

对于对JVM堆内存划分有了解的人,可能对上面的图比较熟悉,这其实已经很接近JVM堆空间的结构,蓝色区域复制算法的担保空间就我们经常说的老年代(Old Generation)。顶部的区域就是年轻代(Eden),而中间两块复制算法的空间就是幸存区(Surivor),对比上面和下面的图,发现是完全可以吻合的。

JVM堆内存划分

标记压缩

分代思想

依据对象的存活周期进行分类

第一类:有些对象创建后,很快就会回收,我们把短命对象归为新生代,

第二类:有些对象创建后会长期存在,有可能和JVM生命周期相同,我们把长命对象归为老年代。

根据不同代的特点,选取合适的收集算法

–少量对象存活,适合复制算法

–老年代中的对象有两种:

     老年代中的对象:多次GC没有回收掉,年龄校大的对象,生命周期较长

     复制算法空间担保 ,直接进入 老年代的大对象

所以会有大量对象存活,和大对象,适合标记清理或者标记压缩

所有的算法,需要能够识别一个垃圾对象,因此需要给出一个可触及性的定义

什么是可触及的:从根节点可以到达的对象,此对象称为可触及的

可复活的: 一旦所有引用被释放,就是可复活状态(因为在finalize()中可能复活该对象)

什么是不可触及的:在finalize()后,可能会进入不可触及状态,不可触及的对象不可能复活,不可触及的对象,是可以回收的

上面的算法中,我们一直说从根节点查找,都提到了根节点,所以我们要搞清楚什么是根节点?

–栈中引用的对象(局部变量表)

–方法区中静态成员或者常量引用的对象(全局对象)

–JNI方法栈中引用对象

根是一系列对象的集合,是一砣根节点,文章中为了简单所以只有一个根对象,希望不要给大家带来误解!

GC中的一个重要 的现象Stop-The-Word

java中一种全局暂停的现象,所有的JAVA代码停止 ,native代码可能执行,但不能和JVM交互

引起STW的原因 :GC 、DUMP线程、堆 DUMP

多半是因为GC引起的。

为什么会产生全局停顿?

GC需要一个安静的状态来完成垃圾的回收,所以需要将用户线程停止,完成垃圾回收后,再继续用户线程。

STW危害?

1、影响系统吞吐量,系统响应时间 增长

2、长时间的服务停止,没有响应,遇到HA系统,可能引起主备切换,最终导致主备同时启动。在一些业务场景下,可能会导致数据不致,甚至无法正常工作。

大家好我是BK,第一次写文章 ,写和不是很好,有错误的地方,请大家指正,谢谢大家!

后续会更新GC的种类和各类的区别。希望大家可以关注我。

介绍

本文是一次数据泄漏之后的一点儿思考,系统日志对于后端系统而言是非常重要的,但是大多数开发人员在打印日志时,是非常随意的,不会去想太多,觉得日志打印的越多,排查总是越是方便。但是一些关键信息在打印日志时一定要注意,如果这些信息被人利用,可能会存在很大的风险。
下面我举例几个场景,想想这样的场景中存在的风险

1、用户名、密码、交易密码等敏感信息,如果这些信息明文打印在日志,这些信息被人拿到之后,就存在很大的安全隐患,这种隐患可能会直接导致用户的损失,影响公司的形象。

2、薪资系统中,姓名、薪资等敏感信息,在每次列表、详情查询的时候不加考虑全部打印日志,只要有日志查看权限的人员中就可能查看到大部分人的薪资情况,导致公司薪资信息泄漏,可能会导致同一团队的人员的薪资泄漏,造成人员流失等

其实对于这些场景,我们只要在日志打印时,进行脱敏处理就可以,最近公司存在一起数据泄漏的事件,在这个事件之后,公司也比较重视这一块,BOSS也因此很不开心,在此事之后,下班之余看了自己扩写了一个日志脱敏的工具,因为公司内部打印日志统一用的logback,本次扩展也是针对logback进行扩展

主要思路

每个团队甚至每个人打印日志的习惯和风格都不一样,基于日志格式输出的多样性,
所以本人觉得最好的办法是通过配置脱敏规则,然后在日志输出之前进行脱敏处理

整理了一下我们系统中日志打印的风格,大数数是以下三种

  • log.info(“xxxxx: {} “ , JSONObject.toJsonString(result));
  • log.info(“xxxxx: {} “ , result );
  • log.info(“xxxxxx: “ + result );

实现过程

第一步:针对上述日志整理,我们先定义一下自己的脱敏规则,增加配置文件logback-desensitization-rule.properties,配置如下
这里需要一些正则的知识,大家请自行补一下

1
2
3
4
5
6
7
8
9
10
#JSON字段中的mobile , telphone关键字对应的内容进行脱敏
RULE_REG_1=(\"mobile\"|\"telphone\")(:\")(\\w{3})(\\w{4})(\\w{4})*(\")&$1$2$3****$5$6
#JSON字段中的"证件号码"关键字对应的内容进行脱敏
RULE_REG_2=(\"idcard\")(:\")(\\w{2})(\\w{1,})(\\w{2})(\")&$1$2$3*********$5$6
#JSON字段中的"密码"对应的内容进行全部*显示
RULE_REG_3=(\"password\")(:\")(\\w+)(\")&$1$2*****$4
#JSON字段中的"用户名"对应的内容除第一位,其它位脱敏显示
RULE_REG_4=(\"customerName\"|\"userName\"|\"name\")(:\")([\u4E00-\u9FA5]{1})[\u4E00-\u9FA5]{1,}(\")&$1$2$3**$4
#log.info("password:{}",password);类似这样的日志,关键字后的8位中,后五位脱敏显示
RULE_REG_5=(password|mobile)([:|=|,| ]+)(\\w{3})(\\w{5})&$1$2$3*****

第二步:继承MessageConverter实现日志脱敏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.bk.framework.extension.logback;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.google.common.collect.Lists;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
* @author BK
* @version V2.0
* @description: logback扩展消息转换器 支持正则脱敏
* @date 2019/6/1 21:23
*/
@Slf4j
public class DesensitizationMessageConverter extends MessageConverter {
private static volatile List<RuleConfig> configList;

@Override
public String convert(ILoggingEvent event) {
initRuleConfig();
return doConvert(event);
}

/**
* 初始化规则配置
*/
private void initRuleConfig() {
if (configList == null) {
synchronized (DesensitizationMessageConverter.class) {
if (configList == null) {
configList = Lists.newArrayList();
Map<String, String> propertyMap = ((LoggerContext) LoggerFactory.getILoggerFactory()).getCopyOfPropertyMap();
for (String s : propertyMap.keySet()) {
if (s.startsWith("RULE_REG_")) {
String[] array = propertyMap.get(s).split("&");
if (ArrayUtils.isNotEmpty(array) && array.length == 2) {
configList.add(new RuleConfig(array[0], array[1]));
}
}
}
log.info("desensitization rule config init end ! ");
}
}
}
}

/**
* 日志内容转换
*
* @param event
* @return
*/
private String doConvert(ILoggingEvent event) {
String result = event.getFormattedMessage();
if (configList != null) {
for (RuleConfig ruleConfig : configList) {
result = ruleConfig.apply(result);
}
} else {
result = super.convert(event);
}
return result;
}

@Data
private class RuleConfig {
private String reg;
private String replacement;

RuleConfig(String reg, String replacement) {
this.reg = reg;
this.replacement = replacement;
}

String apply(String message) {
return Pattern.compile(reg).matcher(message).replaceAll(replacement);
}
}
}

第三步:增加logback.xml文件,整合规则与转换逻辑

  • 1、引入我们第一步配置的脱敏规则属性文件(这里要注意,属性的scope,默认是local,详细配置可以参考logback官网)
  • 2、配置我们第二步增加的转换器,同时配置conversionWord=”xxx”,当日志输出的pattern中引用%xxx时,会通过此转换顺进日志输出的转换
    配置如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    <property scope="context" resource="./logback-desensitization-rule.properties"/>

    <conversionRule conversionWord="msg"
    converterClass="com.bk.framework.extension.logback.DesensitizationMessageConverter"/>
    <conversionRule conversionWord="rid" converterClass="com.bk.framework.extension.logback.ClassicConverterExt"/>
    <property name="CONSOLE_LOG_PATTERN"
    value="%date{yyyy-MM-dd HH:mm:ss} | %boldYellow(%rid) | %highlight(%-5level) | %boldYellow(%thread) | %boldGreen(%logger) | %msg%n"/>
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
    <pattern>${CONSOLE_LOG_PATTERN}</pattern>
    </encoder>
    </appender>

    <root level="INFO">
    <appender-ref ref="stdout"/>
    </root>
    </configuration>

测试效果

  1. 编辑测试类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package com.bk.framework.extension;

    import lombok.extern.slf4j.Slf4j;

    /**
    * @author BK
    * @version V2.0
    * @date 2019-06-03 23:43
    */
    @Slf4j
    public class ExtensionApplication {
    public static void main(String[] args) {
    log.info("mobile:{}", "13588889999");
    log.info("userInfo:{}", "{\n" + " \"userName\":\"罗志祥\",\n" + " \"idcard\":\"321183197701017846\",\n" + " \"password\":\"luozhixiang1234\",\n" + " \"mobile\":\"18888888888\"\n" + " }");
    }
    }
  2. 运行main方法
    运行结果

    可以看到,只需要增加一个转换器,配置几个正则,就可以实现日志的脱敏,希望可以帮到大家,相关的代码参考logback日志脱敏V1版本,传送门项目地址.

关于优化的一点儿思路

其实到现在,我们可以实现日志脱敏,但是做法有些丑陋,不是很美观,可以推荐一个思路

通过DTD或得schema文件 ,定义自己的XML格式文件

  • 设置对于json,xml,等不同式日志的脱敏关键字的replacement等信息,主要目的是为了简化配置,封装正则的配置难度,后台根据配置自动生成正则和替换规则
  • 在logback start时,解析配置文件,读取配置生成相应的正则,进行脱敏

另一种优化的思路

增加注解,对需要脱敏的字段进行标记关键字 @Desensitization(type=Desensitization.NAME) //或者 MOBILE 等其它,可以自己定义,只是需要在和配置文件中的统一

  • 增加SpringBoot注解 EnableLogBackDesensitization(pageckage = “com.bk.test, com.bk.xxx”)
  • 参考Spring的component-scan,进行 包路径扫描,把相关的字段,根据分类收集起来,对正则中关键字部分替换
  • 在收集完成之后打印日志时,进行脱敏

相关代码会传到我的github上,相关的代码参考logback日志脱敏V2版本,传送门项目地址

一点儿建议

  1. 过大对象一般不建议打日志
    对于一些过大的对象,比如查询列表的结果建议不要打印,如果需要打印的话,建议在DEBUG级别打印
    一般这种日志,对于问题排查意义不大,而且这种日志导致日志文件巨增,同时如果日志被窃取会存在信息泄漏的风险
  2. 打印日志时一定要有危机意识
    哪些日志可以打印,如些日志打印会有风险,一定要做到心里有数
  3. 打印error级别日志时,建议使用log.error(“xxxxxx” ,e );
    不要使用log.error(“xxxxxx” ,e.getMessage() );

参考资料

https://logback.qos.ch/manual/configuration.html

写在最后

这点儿东西写了三遍,简书是不是最近有问题,图片展示不了,没有发布的内容总是丢失,这已经是第三遍了。

欢迎大家讨论,本人才疏学浅,有不正确的地方还请斧正。

0%