Spring Boot 统一日志处理

本文将介绍如何在 SpringBoot 中优雅地统一日志处理。

一、日志的重要性

日志对应用程序的重要性不言而喻,无论是记录运行情况还是追踪线上问题,都离不开对数据的分析。

二、日志框架

市面上的日志框架分为两类:日志门面(日志抽象层)和日志实现。

一般选用一个日志门面和一个日志实现组合为日志,并在代码中 “面向日志门面” 使用。

SpringBoot 默认使用 SLF4J 和 Logback 的组合。

三、简单使用

  • 实例化 logger:

    1
    2
    3
    4
    5
    6
    7
    public class 类名 {

    public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(类名.class);
    }

    }
  • 调用 logger:

    1
    2
    3
    4
    5
    6
    7
    8
    public class 类名 {

    public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(类名.class);
    logger.info("Hello");
    }

    }

结合 Lombok:

1
2
3
4
5
6
7
8
@Log
public class 类名 {

public static void main(String[] args) {
log.info("Hello");
}

}

四、日志的配置

  • 简单配置:日志的部分配置信息已经集成至 SpringBoot 中,可以通过修改 SpringBoot 的配置文件(porperties/yml)进行配置
  • 深度配置:如果希望更深度地配置日志,还可以创建和编写日志的专用 xml 配置文件。

具体请看:

Spring Boot日志配置及输出

五、日志的持久化

日志的持久化一共有两种方式:文件持久化、数据库持久化

1. 文件持久化

修改 SpringBoot 配置文件:

1
2
3
logging:
file:
path: 保存路径

2. 数据库持久化

创建日志专用配置文件,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
···

<appender name="db" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
<dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
<driverClass>com.mysql.jdbc.Driver</driverClass>
<url>···</url>
<user>···</user>
<password>···</password>
<sqlDialect class="ch.qos.logback.core.db.dialect.MySQLDialect" />
</dataSource>
</connectionSource>
</appender>
<logger name="包名.*" level="INFO" additivity="false">
<appender-ref ref="db" />
</logger>

···

在数据库中新建表,SQL 语句如下:

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
BEGIN;
DROP TABLE IF EXISTS logging_event_property;
DROP TABLE IF EXISTS logging_event_exception;
DROP TABLE IF EXISTS logging_event;
COMMIT;


BEGIN;
CREATE TABLE logging_event
(
timestmp BIGINT NOT NULL,
formatted_message TEXT NOT NULL,
logger_name VARCHAR(254) NOT NULL,
level_string VARCHAR(254) NOT NULL,
thread_name VARCHAR(254),
reference_flag SMALLINT,
arg0 VARCHAR(254),
arg1 VARCHAR(254),
arg2 VARCHAR(254),
arg3 VARCHAR(254),
caller_filename VARCHAR(254) NOT NULL,
caller_class VARCHAR(254) NOT NULL,
caller_method VARCHAR(254) NOT NULL,
caller_line CHAR(4) NOT NULL,
event_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
);
COMMIT;

BEGIN;
CREATE TABLE logging_event_property
(
event_id BIGINT NOT NULL,
mapped_key VARCHAR(254) NOT NULL,
mapped_value TEXT,
PRIMARY KEY(event_id, mapped_key),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);
COMMIT;

BEGIN;
CREATE TABLE logging_event_exception
(
event_id BIGINT NOT NULL,
i SMALLINT NOT NULL,
trace_line VARCHAR(254) NOT NULL,
PRIMARY KEY(event_id, i),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);
COMMIT;

六、AOP 统一处理

在实际开发中,我们可能会有大量的重复的日志记录需求,此时,我们便可以使用 AOP 来避免代码的重复书写。

假设我们希望记录每一次对接口的访问,具体做法如下:

  • 创建切面类,配置增强方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Aspect
    public class ControllerLogAspect {

    @Pointcut("execution(* pers.codewld.imall.*.controller.*.*(..))")
    public void controllerLog() {
    }

    @Around("controllerLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    // ···执行前增强···
    result = joinPoint.proceed();
    // ···执行后增强···
    return result;
    }

    }
  • 获取被切入方法的基本信息,记录至日志中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Aspect
    public class ControllerLogAspect {

    @Pointcut("execution(···)")
    public void controllerLog() {
    }

    @Around("controllerLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    result = joinPoint.proceed();

    // 获取基本信息
    String uri = request.getRequestURI();
    String username = request.getRemoteUser();

    // 记录至日志
    Logger logger = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
    logger.info("url:" + uri + " username:" + username)

    return result;
    }

    }

七、自定义数据库日志

日志框架自带的数据库持久化不够灵活,因此,我们实际上可以创建自己的数据库日志。

具体做法如下:

  • 选择数据库,这里选用 MongoDB,相比于 MySQL 它更加灵活、更适用于日志的存储,同时将 “数据数据库” 和 “日志数据库” 分离也可以减少增加日志引发的性能损失

  • 创建日志实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Log {

    @ApiModelProperty("URI")
    private String uri;

    @ApiModelProperty("操作用户")
    private String username;

    }
  • 创建日志 repository

  • 创建日志 Service

  • 在切面类中,将日志同时存储至 “日志” 和 “自定义数据库日志” 中

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

    @Pointcut("execution(···)")
    public void controllerLog() {
    }

    @Around("controllerLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    result = joinPoint.proceed();

    // 获取基本信息
    Log log = new Log();
    log.setUri(request.getRequestURI());
    log.setUsername(request.getRemoteUser());

    // 记录至日志
    Logger logger = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
    logger.info(log)

    // 记录至自定义数据库日志
    logService.add(log);

    return result;
    }

    }