面试题分享-附参考答案

FullGC多的原因,如何优化

原因:FullGC触发的原因是老年代满了,需要整堆回收

对象被放到老年代有原因:

  1. 大对象直接分配到老年代
  2. 对象经历了很多次GC,年龄超过15
  3. 在Survivor区中,超过某个年龄的对象占比超过50%,超过年龄的对象都转移到老年代
  4. 空间分配担保,导致老年代满了,或者需要担保的空间太大无法满足要求

优化:

  1. 增大JVM参数中堆内存的设置,看是否需要修改老年代和新生代的比例
  2. 减少不必要的对象创建,对象用完赋null值帮助回收
  3. 尽量不定义大对象
  4. 如果现在使用的是CMS,评估是否可以使用G1收集器

MQ怎么保证消息不丢失?

MQ保证消息不丢失从两个方面来看:客户端的使用和服务端的配置

客户端使用
  1. 发送消息时,使用同步发送或者异步发送+check的机制,保证客户端发送消息时成功被服务端接收了
  2. 处理消息时,先进行消息的业务处理,等业务逻辑处理完之后,再返回消息的消费ack,如果ack异常了,可以考虑对消息进行持久化处理
服务端配置
  1. 在接收到消息后,采用同步刷盘的机制来保存消息到磁盘
  2. 主从同步,保证即使遭遇系统宕机、磁盘损坏的情况下也保证消息的不丢失

MQ如何处理重复消费

  1. 看业务是否具有天然的幂等性,例如在订单类系统中,可以通过使用状态判断来看消息是否已经被处理过了
  2. 如果不具备幂等性,可以考虑使用setnx来做,当收到消息之后,获取消息的唯一标示,通过唯一标示来做一个分布式锁,消费结束时如果消费成功,执行ack,这个锁就放着不动;消费失败,释放锁,不执行ack

ID生成器的原理

ID生成器是为了全局生成唯一ID的

比较常见的有UUID、数据库自增主键、雪花算法

雪花算法的结构:时间戳 + 机器ID + 序列号

缓存雪崩、缓存击穿、缓存穿透

缓存雪崩

大量缓存在同一时间失效,导致大量请求打到MySQL,或者Redis服务不可用了,导致请求直接访问到MySQL

解决方案:

  1. 失效时间的设置尽量在原定时间上加上一个随机数,避免大量key同时失效
  2. 搭建Redis集群,提高可用性
缓存击穿

缓存击穿主要是因为某个热点key在Redis突然消失了,请求全都打到了MySQL

解决方案:

  1. 热点key不设置失效时间
  2. 避免误删操作(代码、人工)
  3. 布隆过滤器
缓存穿透

连续请求某一个不存在key,导致请求直接打到数据库

解决方案:

  1. 如果发现持续访问一个不存在的key,可以在Redis中放一个默认值,或者执行降级
  2. 在前端或者web层就校验key的合法性,避免不合法的key放进来

MySQL分库分表

分库分表的策略采用主键id和表个数取模的策略

如果数据量持续增加,可以考虑增加分表的张数,分表数量 * 2,减少数据迁移工作

或者最开始就使用一致性Hash

还可以考虑范围分片的方法,并对历史数据执行归档

CPU 100% 的问题

简要步骤:

  1. 找到最耗CPU的进程
  2. 找到最耗CPU的线程
  3. 查看堆栈,定位线程在干什么,定位代码
步骤一、找到进程
  • 执行top -c,显示进程运行信息列表
  • 键入P(大写),进程按照CPU使用率排序
  • 找出最耗CPU的进程id,例如10778
步骤二、找到线程
  • 执行top -Hp 10778,显示一个进程的线程运行信息列表
  • 键入P(大写),线程按照CPU使用率排序
  • 找出最耗CPU的线程id,例如10084
步骤三、看看线程在干什么
  • 首先将线程PID转化为16进制,使用printf “%x\n” 10084,得到对应16进制为: 2764
  • 通过jstack查看堆栈信息,jstack 10778 | grep ‘0x2764’ -C5 —color,得到线程堆栈
  • 找到线程对应的线程名称,找到对应代码

线程池的参数评估

首先要明确任务的类型

如果是CPU密集型任务,那么核心线程数设置为:CPU核数+1

如果是IO密集性任务,那么就需要大致估算出IO时间和CPU时间的比例,最佳线程数就是

[(IO耗时/CPU耗时)+1]*CPU核数

上面说的是核心线程数,那么最大线程数则可以比核心线程数稍微多一些

然后就是阻塞队列了,阻塞队列不宜使用LinkedBlockingQueue(其实LinkedBlockingQueue也是有界的,为Integer的最大值,并且也可以设置容量),而应该使用有界的ArrayBlockingQueue,这个队列大小也需要根据访问量来估计,减去核心线程数在处理的任务,得出一个数值

面试题分享2-附参考答案

sleep方法和wait方法的区别

  • sleep方法属于Thread类,wait方法属于Object类。
  • sleep方法暂停执行指定的时间,让出CPU给其他线程,但其监控状态依然保持,在指定时间过后又会自动恢复运行状态。
  • 在调用sleep方法的过程中,线程不会释放对象锁。
  • 在调用wait方法时,线程会放弃对象锁,进入等待此对象的等待锁池,只有针对此对象调用notify方法后,该线程才能进入对象锁池准备获取
    对象锁,并进入运行状态。

MySQL优化

  1. 大表慢查
    • 查询慢查询日志,分析查询慢的原因
    • explain
    • 根据业务请求建立适当索引,可通过索引覆盖等手段
  2. B端后台的复杂查询
    • 如果数据量实在过大,则考虑Elastic Search
  3. 多表查询
    • 宽表

存储速度:快速设备 > 慢速设备

cpu 寄存器 > cache > 内存 > redis > Elastic Search > 本系统内部的MySQL > RPC调用的MySQL

Redis的原理及应用

Redis的原理及应用

Redis的数据类型

Redis支持String、Hash、List、Set、ZSet、Bitmap、HyperLogLog、Geospatial等8种数据类型。

  • String: String是Redis基本的数据类型,一个key对应一个value。String类型的值最大能存储512MB。
  • Hash: Redis Hash是一个键值(key -> value)对集合。
  • List: Redis List是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)。
    最多可存储2^31-1(4亿多)个元素。
  • Set: Set是String类型的无序集合。集合是通过散列表实现的,所以添加、删除、查找的复杂度都是O(1)。
  • ZSet: Redis ZSet和Set一样也是String类型元素的集合,且不允许有重复的成员,不同的是,每个元素都会关联一个double类型
    的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序的。
  • Bitmap: 通过操作二进制位记录数据。
  • HyperLogLog: 用于估计一个Set中元素数量的概率性的数据结构。
  • Geospatial: 用于地理空间关系计算。

Redis的发布和订阅

Redis发布、订阅是一种消息通信模式:发送者(pub)向频道(channel)发送消息,订阅者(sub)接收频道上的消息。Redis客户端可以
订阅任意数量的频道,发送者也可以向任意频道发送数据。

img.png

Redis的持久化

Redis的持久化分为RDB和AOF。

  1. RDB(Redis DataBase): RDB在指定的时间间隔内对数据进行快照存储。
    • RDB文件格式紧凑,方便数据传输和数据恢复。
    • 在保存.rdb快照文件时,父进程会fork出一个子进程,由子进程完成具体的持久化操作。
    • bgsave与save的区别:save会阻塞Redis服务器,直到RDB完成。bgsave是fork子线程,持久化由子线程完成,阻塞只发生在fork子线程的阶段。
    • 全量复制。
  2. AOF(Append Of File): AOF记录对服务器的每次写操作,在Redis重启会重放这些命令来恢复数据
    • AOF命令以Redis协议追加和保存每次写操作。
    • 支持对AOF文件进行重写,使得AOF文件的体积不至于过大。
    • 不同的fsync策略(无fsync,每秒fsync,每次写fsync)。
    • AOF文件是日志格式,容易被操作。
    • 实时复制。

Redis集群数据复制原理

Redis提供了复制功能,可以实现主数据库(Master)中的数据更新后,自动将更新的数据同步到从数据库(Slave)。

img.png

  1. 一个从数据库在启动后,会向主数据库发送SYNC命令
  2. 主数据库在接收到SYNC命令后,会开始在后台保存快照(RDB持久化过程),并将保存快照期间接收到的命令缓存起来,生成.rdb快照文件。
  3. 在主数据库快照执行完成后,Redis会将快照文件和所有缓存的命令以.rdb快照文件的形式发送给从数据库。
  4. 从数据库收到主数据库的.rdb快照文件后,载入该快照文件到本地。
  5. 从数据库执行载入后的.rdb快照文件,将数据写入内存中。
  6. 1~5过程,称之为复制初始化。
  7. 在复制初始化结束后,主数据库在每次收到写命令时,都会将命令同步给从数据库,从而保证主从数据库的数据一致。

Redis集群模式及工作原理

Redis有三种集群模式:主从模式、哨兵模式、Redis Cluster。

主从模式

主从模式:所有的写请求都被发送到主数据库上,再由主数据库将数据同步到从数据库上。主数据库主要用于执行写操作和数据同步,从数据库主要用于读操作。

img.png

哨兵模式

哨兵模式:在主从模式上添加一个哨兵的角色来监控集群的运行状态。哨兵通过发送命令让Redis服务器返回其运行状态。哨兵是一个独立运行的进程,在检测到Master宕机时会自动
将Slave切换成Master,然后通过pub/sub模式通知其他服务修改配置文件,完成主备热切。

img.png

Redis-Cluster

Redis-Cluster:Redis-Cluster实现了在多个redis节点之间进行数据分片和数据复制。基于Redis集群的数据自动分片能力,可方便地对Redis集群进行横向扩展,以提高Redis集群的
吞吐量。基于redis集群的数据复制能力,在集群中的一部分节点失效或者无法进行通信时,Redis仍然可以基于副本数据对外提供服务,这提高了集群的可用性。

以下命令是redis 5.x及后续版本创建集群的命令。

1
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1 

–cluster-replicas 参数为数字,1表示每个主节点需要1个从节点

img.png

Redis-Cluster有以下特点:

  • 所有Redis节点彼此通过PING-PONG机制互联,内部使用二进制协议优化传输速度和带宽。
  • 在集群中超过半数的节点检测到某个节点Fail后,将该节点值为Fail状态。
  • 客户端连接集群中的任何一个节点即可对集群进行操作。
  • Redis-Cluster把所有的物理节点都映射到0~16383的slot上,cluster负责维护每个节点上数据槽的分配。
  • Redis的数据分配策略:Redis首先对Key使用CRC16算法算出一个结果,然后把结果对16384取余,这样每个key
    都会对应一个编号为0~16383的slot。
  • slot可以理解为是Redis节点的别名,比如key1和key2经过分配策略取值都是6000,那这连个key就在包含slot=6000的节点上,
    此时恰好是节点C,那么节点C就保存了key1和key2。
  • Redis会根据节点的数量大致均匀地将slot映射到不同的节点。

分布式缓存设计的核心问题

缓存淘汰策略

  • FIFO(先进先出):判断被存储的时间,离目前最远的数据优先淘汰。
  • LRU(最近最少使用):判断缓存最近被使用的时间,距离当前最远的数据优先被淘汰。
  • LFU(最不经常使用):在一段时间内,被使用次数最少的缓存优先被淘汰。
  • TTL(过期时间):设置过期时间,时间到期后,缓存淘汰。

缓存雪崩

缓存雪崩:在同一时刻由于大量缓存失效,导致大量查询缓存的请求都去查询数据库,对数据库造成巨大压力。

  • 设置不同的失效时间

缓存穿透

缓存穿透:缓存系统故障或者用户频繁查询系统中不存在的数据,这时请求会传过缓存不断被发送到数据库,导致数据库负载过重。

  • 布隆过滤器
  • cache null:在缓存中记录一个短暂的(数据过期时间内)数据在系统中是否存在的状态,如果不存在,直接返回null。

布隆过滤器

  1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数
  2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
  3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
  4. 判断某个key是否存在,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为存在。

优点:不需要存储key,节省空间

缺点:

  1. 算法判断key在集合中时,有一定的概率key其实不在集合中
  2. 无法删除

缓存更新

策略:延迟双删

  1. 删除redis缓存
  2. 更新数据库
  3. 更新成功后,通过延迟队列,进行二次删除

MySQL进阶知识点

MySQL进阶知识点

存储引擎

MyISAM

不支持事务、行级锁和表锁,在INSERT和UPDATE时会锁定整个表。

MyISAM的特点是执行读取速度快,且占用的内存和存储资源较少。

InnoDB

InnoDB的底层存储是B+树,B+树的每个节点都对应InnoDB的一个Page,Page的大小固定,一般设为16KB。非叶子节点只有键值,叶子节点包含完整的数据。

img.png

主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。

非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。

基于非主键索引的查询需要多扫描一棵主键索引树,这个过程称为回表。

减少回表的方式:索引覆盖

隔离级别

MySQL 可重复读

InnoDB 的行数据有多个版本,每个数据版本有自己的row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id和一致性视图确定数据版本的可见性。

  • 可重复读,查询只承认在事务启动前就已经提交完成的数据
  • 读提交,查询只承认在查询语句启动前就已经提交完成的数据
  • 当前读,总是读取已经提交完成的最新版本

MVCC

在可重复读隔离级别下,事务在启动的时候就拍了快照。注意:这个快照是基于整库的。

InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向InnoDB 的事务系统申请的,是按申请顺序严格递增的。

而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。

也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

数据库锁

行级锁

行级锁指对某行数据加锁,是一种排它锁,防止其他事务修改此行。

  • SELECT … FOR UPDATE 语句允许用户一次针对多条记录执行更新。
  • COMMIT或者ROLLBACK释放锁

表级锁

表级锁是对当前操作的整张表加锁,表级锁分为表共享读锁与表独占写锁。
使用InnoDB引擎,如果筛选条件里面没有索引字段,就使用表锁,反之,则使用行锁。

页级锁

页级锁的锁定粒度介于行级锁和表级锁。表级锁的加锁速度快,但冲突多;行级锁冲突少,但加锁慢;页级锁在二者之间做了平衡,一次锁定相邻的一组记录。

间隙琐

在Innodb可重复读情况下,解决幻读引入的锁机制,前开后闭原则。

可重复读隔离级别下,数据库是通过行锁和间隙锁共同组成的。

synchronized解析

synchronized

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
public static final Object LOCK = new Object();

public void m1() {
synchronized (LOCK) {

}

}

public synchronized void m2() {

}

编译 -> 字节码

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
// access flags 0x
public final static Ljava/lang/Object; LOCK

// access flags 0x
public m1()V
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L2 L3 L2 null
L
LINENUMBER 13 L
GETSTATIC com/hh/skilljava/javabase/juc/SynchronizeTest.LOCK :
Ljava/lang/Object;
DUP
ASTORE 1
MONITORENTER // MONITORENTER
L
LINENUMBER 15 L
ALOAD 1
MONITOREXIT
L
GOTO L
L
FRAME FULL [com/hh/skilljava/javabase/juc/SynchronizeTest
java/lang/Object] [java/lang/Throwable]
ASTORE 2
ALOAD 1
MONITOREXIT // MONITOREXIT
L
ALOAD 2
ATHROW
L
LINENUMBER 16 L
FRAME CHOP 1
RETURN
L
LOCALVARIABLE this Lcom/hh/skilljava/javabase/juc/SynchronizeTest; L
L6 0
MAXSTACK = 2
MAXLOCALS = 3

// access flags 0x
public synchronized m2()V
L
LINENUMBER 20 L
RETURN
L
LOCALVARIABLE this Lcom/hh/skilljava/javabase/juc/SynchronizeTest; L
L1 0
MAXSTACK = 0
MAXLOCALS = 1

Synchronized在修饰同步代码块时,是由monitorenter和monitorexit指令来实现同步的。

进入monitorenter指令后,线程将持有Monitor对象,退出 monitorenter指令后,线程将释放该Monitor对象。(修饰方法,使用标识符,原理相同)

Monitor

JVM中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个Monitor,Monitor可以和对象一起创建、销毁。Monitor是由ObjectMonitor实现,而ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp文件实现,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于 wait 状态的线程,会被加入到 _WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

当多个线程同时访问一段同步代码时,多个线程会先被存放在EntryList集合中,处于block状态的线程,都会被加入到该列表。接下来当线程获取到对象的Monitor时,Monitor是依靠底层操作系统的Mutex Lock来实现互斥的,线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex。

因Monitor是依赖于底层的操作系统实现,存在用户态与内核态之间的切换,所以增加了性能开销。

Synchronized 优化

为了提升性能,JDK1.6 引入了偏向锁、轻量级锁、重量级锁概念,来减少锁竞争带来的上下文切换,而正是新增的 Java 对象头实现了锁升级功能

对象头

对象实例在堆内存中被分为了三个部分:对象头、实例数据和对⻬填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组⻓度三部分组成。
Mark Word记录了对象和锁有关的信息。Mark Word在64位JVM中的⻓度是64bit,我们可以一起看下Mark Word在64位JVM的存储结构是怎么样的。如下图所示

img.png

锁升级功能主要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,Synchronized同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。

偏向锁

当没有竞争出现时,默认会使用偏向锁。JVM会利用CAS操作,在对象头上的Mark Word部分设置线程 ID,以表示这个对象偏向于当前线程。偏向锁的作用就是,当一个线程再次
访问这个同步代码或方法时,该线程只需去对象头的Mark Word中去判断一下是否有偏向锁指向它的ID,无需再进入Monitor去竞争对象了。

偏向锁主要用来优化同一线程多次申请同一个锁的竞争(例如单线程操作线程安全的集合)。

在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加JVM参数关闭偏向锁来调优系统性能。

轻量级锁

当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象Mark Word中的线程ID不是自己的线程ID,就会进行CAS操作获取锁,如果获取成功,直接替换Mark Word中的线程ID为自己的ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前
锁有一定的竞争,偏向锁将升级为轻量级锁。

轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在⻓时间的竞争。

自旋锁和重量级锁

从JDK1.7开始,自旋锁默认启用,自旋次数由JVM设置决定。

在尝试升级轻量级锁CAS抢锁失败时,会进行自旋,自旋重试上限后依然没有抢到锁,线程将会被挂起进入阻塞状态。升级为重量级锁,这个状态下,未抢到锁的线程都会进入Monitor,被阻塞在_waitSet 队列中。

1
-XX:-UseBiasedLocking // 关闭偏向锁(默认打开)

动态编译实现锁消除 / 锁粗化

Java还使用了编译器对锁进行优化。JIT编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术,来判断同步代码块使用的锁对象是否只能够被一个线程访问,编译时直接不生成Synchronized对应的机器码。

锁粗化,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。

代码层优化

减小锁粒度,分段锁..

JVM知识点

JVM知识点

JVM的运行机制

JVM是用于运行Java字节码的虚拟机,包括一套字节码指令集、一组程序寄存器、一个虚拟机栈、一个虚拟机堆、一个方法区和一个垃圾回收器。JVM运行在操作系统
之上,不与硬件设备直接交互。

Java源文件在通过编译器之后被编译成.class文件,.class文件又被JVM中的解释器翻译成机器码在不同的操作系统(Windows、Linux、MacOs)上运行。每种
操作系统的解释器都是不同的,但基于解释实现的虚拟机是相同的,这也是Java跨平台的原因。在一个Java进程开始运行后,虚拟机就开始实例化,有多个进程启动就会
实例化多个虚拟机实例。进程退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间不能共享数据。

Java程序的具体运行过程如下:

  1. Java源文件被编译器编译成字节码文件。
  2. JVM将字节码文件翻译成相应操作系统的机器码。
  3. 机器码调用相应的操作系统的本地方法库执行相应的方法。

img.png

  • 类加载器子系统用于将编译好的.class文件加载到JVM中
  • 运行时数据区用于存储JVM运行过程中产生的数据
  • 执行引擎包括即时编译器和垃圾回收器,即时编译器用于将Java字节码翻译成具体的机器码,
    垃圾回收器用于回收在运行过程中不再使用的对象
  • 本地接口库用于调用操作系统的本地方法库完成具体的指令操作

多线程

在多核操作系统上,JVM允许在一个进程内同时并发执行多个线程。JVM中的线程与操作系统中的线程是相互对应的,在JVM线程的本地存储、缓冲区分配、同步对象、栈、
程序计数器等准备工作都完成时,JVM会调用操作系统的接口创建一个与之对应的原生线程;在JVM线程中运行结束时,原生线程随之会被回收。操作系统负责调度所有线程,
并为其分配CPU时间片,在原生线程初始化完毕后,就会调用Java线程的run()方法,执行该线程;在线程结束时,会释放原生线程和Java线程所对应的资源。

在JVM后台运行的线程主要有以下几个:

  • 虚拟机线程:JVM执行任务的线程,执行run()方法
  • 周期性任务线程:通过定时器调度线程,实现周期性操作
  • GC线程:垃圾回收
  • 编译器线程:编译器线程在运行时将字节码动态编译成本地平台的机器码,是JVM跨平台的体现
  • 信号分发线程:接收发送到JVM信号并调用JVM方法

自定义注解+AOP切面实现记录操作日志到数据库

需求

● 每当有增、删、改、上传等编辑操作时,需要记录日志到数据库,用于溯源。
● 记录字段比如:操作人id、名称、ip地址、请求方式、调用方法、参数、操作类型、操作时间、操作模块、返回结果。
使用AOP切面编程方式,对增删改操作进行增强,在操作完成后切入,获取所需要的信息,存入数据库。

创建自定义注解

● 使用@interface创建自定义注解OptLog
● 1个参数:optType,用于记录操作类型。

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OptLog {

/**
* @return 操作类型
*/
String optType() default "";

}

创建常量,定义操作类型

定义常量做为optType的值,统一规范,方便后期维护。

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
public class OptTypeConst {

/**
* 新增操作
*/
public static final String SAVE_OR_UPDATE = "新增或修改";

/**
* 新增
*/
public static final String SAVE = "新增";

/**
* 修改操作
*/
public static final String UPDATE = "修改";

/**
* 删除操作
*/
public static final String REMOVE = "删除";

/**
* 上传操作
*/
public static final String UPLOAD = "上传";

}

编写切面方法,实现记录日志

@Aspect:声明为切面类。
@Pointcut("@annotation(com.minzheng.blog.annotation.OptLog)"):在OptLog注解的位置切入代码。
@AfterReturning(value = "optLogPointCut()", returning = "keys"):表示对optLogPointCut()方法定义的切入点进行增强的实现方法,returning = “keys”表示返回的参数。
● 通过RequestContextHolder获取到request,获取请求中的信息。
joinPoint通过反射,获取到切入点所在的方法,以及注解中的内容。
● 将信息存入operationLog实体类,最终通过operationLogDao存入数据库。

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
@Aspect
@Component
public class OptLogAspect {

@Autowired
private OperationLogDao operationLogDao;

/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
*/
@Pointcut("@annotation(com.minzheng.blog.annotation.OptLog)")
public void optLogPointCut() {}


/**
* 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
*
* @param joinPoint 切入点
* @param keys 返回结果
*/
@AfterReturning(value = "optLogPointCut()", returning = "keys")
@SuppressWarnings("unchecked")
public void saveOptLog(JoinPoint joinPoint, Object keys) {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) Objects.requireNonNull(requestAttributes).resolveReference(RequestAttributes.REFERENCE_REQUEST);
OperationLog operationLog = new OperationLog();
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取操作
Api api = (Api) signature.getDeclaringType().getAnnotation(Api.class);
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
OptLog optLog = method.getAnnotation(OptLog.class);
// 操作模块
operationLog.setOptModule(api.tags()[0]);
// 操作类型
operationLog.setOptType(optLog.optType());
// 操作描述
operationLog.setOptDesc(apiOperation.value());
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + "." + methodName;
// 请求方式
operationLog.setRequestMethod(Objects.requireNonNull(request).getMethod());
// 请求方法
operationLog.setOptMethod(methodName);
// 请求参数
operationLog.setRequestParam(JSON.toJSONString(joinPoint.getArgs()));
// 返回结果
operationLog.setResponseData(JSON.toJSONString(keys));
// 请求用户ID
operationLog.setUserId(UserUtils.getLoginUser().getId());
// 请求用户
operationLog.setNickname(UserUtils.getLoginUser().getNickname());
// 请求IP
String ipAddress = IpUtils.getIpAddress(request);
operationLog.setIpAddress(ipAddress);
operationLog.setIpSource(IpUtils.getIpSource(ipAddress));
// 请求URL
operationLog.setOptUrl(request.getRequestURI());
operationLogDao.insert(operationLog);
}

}

段落引用知识点
@Around @Before @After @AfterReturning 执行顺序为:
● Around
● AroundBefore
● before
● method.invoke()
● AroundAfter
● After
● AfterReturning