门面模式

  • 说到日志框架不得不说门面模式。

  • 门面模式,其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。

  • 用一张图来表示门面模式的结构为:

  • 简单来说,该模式就是把一些复杂的流程封装成一个接口供给外部用户更简单的使用。这个模式中,设计到3个角色。

    • 门面角色:门面模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求预定了几种功能的组合(模块)。
    • 子系统(模块)角色:实现了子系统的功能。它对客户角色和 Facade 是未知的。它内部可以有系统内的相互交互,也可以由供外界调用的接口。
    • 客户角色:通过调用 Facede 来完成要实现的功能。

日志介绍

  • 项目开发中,日志是必不可少的组件,项目中也会构建我们需要的日志框架。

  • 通常日志是由一个抽象层+实现层组合来搭建的

  • 市面上的日志框架

    日志门面 日志实现
    JCL(Jakarta Commons Logging) Log4j,JUL(java.util.logging), SimpleLog
    SLF4j(Simple Logging Facade for Java) Log4j2 、 Logback
    jboss-logging
  • 简单说下,上表的日志门面对应了门面模式中的 Facede 对象,它们只是一个接口层,并不提供日志实现;

    • 而日志实现则对应着各个子系统或者模块,日志记录的具体逻辑实现,就写在这些右边的框架里面;
    • 那我们的应用程序就相当于客户端。

为什么要使用门面模式

  • 试想下我们开发系统的场景,需要用到很多包,而这些包又有自己的日志框架,于是就会出现这样的情况
    • 我们自己的系统中使用了 Logback 这个日志系统,我们的系统使用了 Hibernate,Hibernate 中使用的日志系统为 jboss-logging,我们的系统又使用了 Spring ,Spring 中使用的日志系统为 commons-logging。
    • 这样,我们的系统就不得不同时支持并维护 Logback、jboss-logging、commons-logging 三种日志框架,非常不便。
    • 解决这个问题的方式就是引入一个接口层,由接口层决定使用哪一种日志系统,而调用端只需要做的事情就是打印日志而不需要关心如何打印日志,而上表的日志门面就是这种接口层。
  • 鉴于此,我们选择日志时,就必须从上表左边的日志门面和右边的日志实现各选择一个框架,而 SpringBoot 底层默认选用的就是 SLF4j 和 Logback 来实现日志输出。

背景/发展史

那就要从Java Log的发展历程开始说起。

  1. log4j(作者Ceki Gülcü)出来时就等到了广泛的应用(注意这里是直接使用),是Java日志事实上的标准,并成为了Apache的项目
  2. Apache要求把log4j并入到JDK,SUN拒绝,并在jdk1.4版本后增加了JULjava.util.logging
  3. 毕竟是JDK自带的,JUL也有很多人用。同时还有其他日志组件,如SimpleLog等。这时如果有人想换成其他日志组件,如log4j换成JUL,因为api完全不同,就需要改动代码。
  4. Apache见此,开发了JCL(Jakarta Commons Logging),即commons-logging-xx.jar。它只提供一套通用的日志接口api,并不提供日志的实现。很好的设计原则嘛,依赖抽象而非实现。这样应用程序可以在运行时选择自己想要的日志实现组件。
  5. 这样看上去也挺美好的,但是log4j的作者觉得JCL不好用,自己开发出slf4j,它跟JCL类似,本身不替供日志具体实现,只对外提供接口或门面。目的就是为了替代JCL。同时,还开发出logback,一个比log4j拥有更高性能的组件,目的是为了替代log4j。
  6. Apache参考了logback,并做了一系列优化,推出了log4j2

SLF4j 使用

  • 官方文档给出这样一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @SpringBootApplication
    public class LogApplication {
    public static void main(String[] args) {
    // LogApplication.class 就是你要打印的指定类的日志,
    // 如果你想在其它类中打印,那就把 LogApplication.class 替换成目标类名.class 即可。
    Logger logger = LoggerFactory.getLogger(LogApplication.class);
    logger.info("Hello World");
    // SpringApplication.run(LogApplication.class, args);
    }
    }

用法

  • slf4j 有如下六种用法:

    1611824120877

  • 一共五种角色

    • application 不用说,就是我们的系统;
    • SLF4J API 就是日志接口层(门面);
    • 蓝色和最下面灰色的就是具体日志实现(子系统);
    • 而 Adaptation 就是适配层。
  • 其中第二种就是 SpringBoot 的默认用法;

  • 第三种/第四种同理:因为 Log4J 出现得比较早,它根本不知道后面会有 SLF4J 这东西。Log4J 不能直接作为 SLF4J 的日志实现,所以中间就出现了适配层。

  • 提醒:每一个日志的实现框架都有自己的配置文件。使用 slf4j 以后,配置文件还是日志实现框架自己本身的配置文件。

    • 比如,Logback 就使用 logback.xml、Log4j 就使用 Log4j.xml 文件。

统一日志到slf4j

  • 让系统中所有的日志都统一到 slf4j 的做法是:

    1. 将系统中其他日志框架先排除出去
    2. 用中间包来替换原有的日志框架
    3. 我们导入 slf4j 其他的实现

  • JCL

    • jcl-over-slf4j.jar 适配
  • log4j

    • log4j-over-slf4j.jar
  • JUL

    • jul-to-slf4j.jar

日志依赖图

门面 适配包 日志实现
slf4j slf4j-jdk14 jul
slf4j slf4j-jcl jcl
slf4j logback-classic logback
slf4j log4j-slf4j-impl log4j2
slf4j-log4j12 log4j
jcl log4j
jcl jul
jcl simpleLog

SpringBoot日志框架

  • SpringBoot 使用以下依赖实现日志功能:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
    <scope>compile</scope>
    </dependency>
  • 关系图

  • 可见,

    1. SpringBoot2.x 底层也是使用 slf4j+logback 或 log4j 的方式进行日志记录;
    2. SpringBoot 引入中间替换包把其他的日志都替换成了 slf4j
    3. 如果我们要引入其他框架、可以把这个框架的默认日志依赖移除掉。
  • 比如 Spring 使用的是 commons-logging 框架,我们可以这样移除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <exclusions>
    <exclusion>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
  • SpringBoot 能自动适配所有的日志,而且底层使用 slf4j+logback 的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可。

Springboot配置日志

  • application.properties或 application.yml (系统层面)

    • 可做的事情也比较简单,比如:只能配置日志文件的输出路径、日志文件的格式、日志的级别等
  • logback-spring.xml(自定义文件方式)

    • 比较复杂,对日志的处理比较好,生产上推荐这种,运行维护好。
    • 如有以下需求:
      • 区分 debug、info、error 等类型的日志,并分别输出到不同的日志文件。
      • 对日志文件进行维护,如每天产生一个日志文件,并设置日志文件的大小和保留天数等。
  • 默认配置(以 Log4j 框架为例),SpringBoot 默认帮我们配置好了日志:

    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
    package tk.fulsun.demo;

    import org.junit.jupiter.api.Test;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.test.context.SpringBootTest;

    /**
    * @author fulsun
    * @description: 主测试类
    * @date 6/8/2021 4:26 PM
    */
    @SpringBootTest
    class LogApplicationTest {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Test
    public void contextLoads() {
    // 日志的级别;
    // 由低到高 trace<debug<info<warn<error
    // 可以调整输出的日志级别;日志就只会在这个级别以以后的高级别生效
    logger.trace("这是trace日志...");
    logger.debug("这是debug日志...");
    // SpringBoot 默认给我们使用的是 info 级别的,没有指定级别的就用SpringBoot 默认规定的级别;root 级别
    logger.info("这是info日志...");
    logger.warn("这是warn日志...");
    logger.error("这是error日志...");
    }
    }

    // 控制台结果
    2021-06-08 16:27:50.658 INFO 7272 --- [ main] tk.fulsun.demo.LogApplicationTest : 这是info日志...
    2021-06-08 16:27:50.658 WARN 7272 --- [ main] tk.fulsun.demo.LogApplicationTest : 这是warn日志...
    2021-06-08 16:27:50.658 ERROR 7272 --- [ main] tk.fulsun.demo.LogApplicationTest : 这是error日志...

yaml方式

  1. 日志级别 trace<debug<info<warn<error<fatal,默认级别为info,即默认打印info及其以上级别的日志

  2. logging.level设置日志级别,后面跟生效的区域

    • 比如root表示整个项目,默认的情况logging.level.root=info
    • 也可以设置为某个包下
    • 也可以具体到某个类名(日志级别的值不区分大小写)
  3. 日志输出格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #    日志输出格式:
    # %d表示日期时间,
    # %thread表示线程名,
    # %-5level:级别从左显示5个字符宽度
    # %logger{50} 表示logger名字最长50个字符,否则按照句点分割。
    # %msg:日志消息,
    # %n是换行符
    # %clr(对项){颜色名} 配置该项的颜色 #只在控制台有作用
    # ${PID:- } 进程号
    #系统默认配置
    %d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%-5level){green} %clr(${PID:- }){magenta} [ %clr(%thread){red}] --- %clr(%logger{36}){blue} : %msg%n
  4. 输出的文件,都可以指定目录和文件名,相对路径为项目目录下

    • 该方式,日志都写在一个文件里面,不方便维护

    • name: F:/ideaWorkSpace/spring-boot.log

    • path: F:/ideaWorkSpace/spring-boot.log

    • 这两个选一个配置就可以了,一起配置的话,name的生效。

    • 系统每次启动都会在原来的日志文件上追加数据

  • 配置文件 application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    logging:
    pattern: #配置日志输出格式
    file: "%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} === - %msg%n" #配置控制台的日志输出的格式
    file: #配置日志输出的文件
    #会在log目录下生成一个spring.log的日志文件
    path: F:/ideaWorkSpace/log/
    level: # 配置输出日志级别
    root: INFO #设置整个项目的日志输出级别默认info
    tk.fulsun.demo: trace # 设置该包下的日志输出级别为 DEBUG,输出执行的sql

自定义配置文件

  • 在resources目录下添加logback-spring.xml配置文件

  • 配置文件内容如下:

    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
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    <?xml version="1.0" encoding="UTF-8"?>
    <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
    <!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
    <!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
    <!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
    <configuration debug="true">

    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />

    <!-- 定义属性 -->
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <property name="log.path" value="F:/ideaWorkSpace/log" />

    <!-- 彩色日志格式 -->
    <!-- 控制台日志输出-->
    <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex"/>
    <!-- <property name="CONSOLE_LOG_PATTERN" value="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%-4level][%line][%thread] reqLog:[%X{reqLog}] call:[%logger][%method] parameter:%msg%n"/>-->
    <!-- 日志文件日志输出-->
    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } --- [%t] %-40.40logger{39} : %m%n%wex"/>

    <!--多环境的日志输出-->
    <!--根据不同环境(prd:生产环境,test:测试环境,dev:开发环境)来定义不同的日志输出,-->
    <!--在 logback-spring.xml中使用 springProfile 节点来定义,方法如下:-->
    <springProfile name="prd">
    <property name="LOG_HOME" value="F:/ideaWorkSpace/log" />
    </springProfile>
    <springProfile name="test">
    <property name="LOG_HOME" value="F:/ideaWorkSpace/log" />
    </springProfile>
    <springProfile name="dev">
    <property name="LOG_HOME" value="F:/ideaWorkSpace/log" />
    <!-- <logger name="com.bs.agricultural_share_platform.dao" level="debug"/>-->
    </springProfile>

    <!--
    <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
    以及指定<appender>。<logger>仅有一个name属性,
    一个可选的level和一个可选的addtivity属性。
    name:用来指定受此logger约束的某一个包或者具体的某一个类。
    level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
    还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
    如果未设置此属性,那么当前logger将会继承上级的级别。
    addtivity:是否向上级logger传递打印信息。默认是true。
    -->
    <!--
    使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
    第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
    第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
    -->

    <!--
    root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
    level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
    不能设置为INHERITED或者同义词NULL。默认是DEBUG
    可以包含零个或多个元素,标识这个appender将会添加到这个logger。
    -->

    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <!-- 设置debug,在控制台输出执行的sql-->
    <level>debug</level>
    </filter>

    <encoder>
    <pattern>${CONSOLE_LOG_PATTERN}</pattern>
    <charset>utf8</charset>
    </encoder>
    </appender>

    <!--debug 级别的日志-->
    <!-- 按照每天生成日志文件 -->
    <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">

    <!-- 正在记录的日志文件的路径及文件名,正在的 -->
    <file>${log.path}/log_debug.log</file>

    <!-- 设置此日志文件只记录debug级别的 -->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>DEBUG</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>

    <!--日志文件输出格式-->
    <encoder>
    <pattern>${FILE_LOG_PATTERN}</pattern>
    <charset>UTF-8</charset>
    </encoder>

    <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

    <!--日志文件输出的文件名-->
    <fileNamePattern>${LOG_HOME}/info.%d{yyyy-MM-dd}.log</fileNamePattern>

    <!--日志文件保留天数-->
    <MaxHistory>30</MaxHistory>
    </rollingPolicy>

    <!--日志文件最大的大小-->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <MaxFileSize>10MB</MaxFileSize>
    </triggeringPolicy>
    </appender>

    <!--info 级别的日志-->
    <!-- 按照每天生成日志文件 -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">

    <!-- 正在记录的日志文件的路径及文件名,正在的 -->
    <file>${log.path}/log_info.log</file>

    <!-- 设置此日志文件只记录info级别的 -->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>

    <!--日志文件输出格式-->
    <encoder>
    <pattern>${FILE_LOG_PATTERN}</pattern>
    <charset>utf8</charset>
    </encoder>

    <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

    <!--日志文件输出的文件名-->
    <fileNamePattern>${LOG_HOME}/info.%d{yyyy-MM-dd}.log</fileNamePattern>

    <!--日志文件保留天数-->
    <MaxHistory>30</MaxHistory>
    </rollingPolicy>

    <!--日志文件最大的大小-->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <MaxFileSize>10MB</MaxFileSize>
    </triggeringPolicy>
    </appender>

    <!--WARN 级别的日志-->
    <appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">

    <!-- 正在记录的日志文件的路径及文件名,正在的 -->
    <file>${log.path}/log_warn.log</file>

    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>WARN</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
    <pattern>${FILE_LOG_PATTERN}</pattern>
    <charset>utf8</charset>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>${LOG_HOME}/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
    <MaxHistory>30</MaxHistory>
    </rollingPolicy>
    </appender>

    <!--ERROR 级别的日志-->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">

    <!-- 正在记录的日志文件的路径及文件名,正在的 -->
    <file>${log.path}/log_error.log</file>

    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>ERROR</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
    <pattern>${FILE_LOG_PATTERN}</pattern>
    <charset>utf8</charset>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
    <MaxHistory>30</MaxHistory>
    </rollingPolicy>
    </appender>

    <!-- 系统日志输出级别 ,ref 的名字对应上面appender标签的name名称-->
    <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="INFO"/>
    <appender-ref ref="WARN"/>
    <appender-ref ref="ERROR"/>
    <appender-ref ref="DEBUG"/>
    </root>
    </configuration>

指定配置文件

框架 命名方式
Logback logback-spring.xml, logback-spring.groovy, logback.xml or logback.groovy
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties
  • logback.xml:直接就被日志框架识别了。

  • logback-spring.xml:日志框架就不直接加载日志的配置项,由 SpringBoot 解析日志配置,可以使用 SpringBoot 的高级 Profile 功能,以 Logback 框架为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
    <!--
    日志输出格式:
    %d表示日期时间,
    %thread表示线程名,
    %-5level:级别从左显示5个字符宽度
    %logger{50} 表示logger名字最长50个字符,否则按照句点分割。
    %msg:日志消息,
    %n是换行符
    -->
    <layout class="ch.qos.logback.classic.PatternLayout">
    <!--指定在 dev 环境下,控制台使用该格式输出日志-->
    <springProfile name="dev">
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
    </springProfile>
    <!--指定在非 dev 环境下,控制台使用该格式输出日志-->
    <springProfile name="!dev">
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
    </springProfile>
    </layout>
    </appender>
  • 如果使用 logback.xml 作为日志配置文件,而不是 logback-spring.xml,还要使用profile 功能,会有以下错误:

    1
    no applicable action for [springProfile]

注意

  • 为什么Spring Boot推荐使用logback-spring.xml来替代logback.xml来配置logback日志的问题分析?
    • 原因是:logback.xml加载早于application.properties,所以如果你在logback.xml使用了变量时,而恰好这个变量是写在application.properties时,那么就会获取不到,只要改成logback-spring.xml就可以解决。

lombok- @Slf4j

  1. idea中添加lombok插件

  2. 添加依赖到项目中

    1
    2
    3
    4
    5
    6
    <!-- Slf4j日志-->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.10</version>
    </dependency>
  3. 在类上使用注解,可以使用log属性打印日志

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package tk.fulsun.demo;

    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    /**
    * @author fulsun
    * @description: 主测试类
    * @date 6/8/2021 4:26 PM
    */
    // @SpringBootTest
    @Slf4j
    class LogApplicationTest {
    @Test
    public void contextLoads() {
    log.info("message by @slf4j");
    }
    }

切换日志框架

  • SpringBoot 的底层日志依赖关系,我们就可以按照 slf4j 的日志适配图,进行相关的切换。

切换log4j

  • 修改依赖如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
    <exclusion>
    <artifactId>logback-classic</artifactId>
    <groupId>ch.qos.logback</groupId>
    </exclusion>
    </exclusions>
    </dependency>

    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    </dependency>

切换成 log4j2

  • 修改依赖如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
    <exclusion>
    <artifactId>spring-boot-starter-logging</artifactId>
    <groupId>org.springframework.boot</groupId>
    </exclusion>
    </exclusions>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>

logback-spring.xml

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
89
<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
<!-- 定义日志的根目录 -->
<property name="LOG_HOME" value="/app/log" />
<!-- 定义日志文件名称 -->
<property name="appName" value="nasus-springboot"></property>
<!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!--
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
</springProfile>
</layout>
</appender>

<!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 指定日志文件的名称 -->
<file>${LOG_HOME}/${appName}.log</file>
<!--
当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--
滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
%i:当文件大小超过maxFileSize时,按照i进行文件滚动
-->
<fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
那些为了归档而创建的目录也会被删除。
-->
<MaxHistory>365</MaxHistory>
<!--
当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
</layout>
</appender>

<!--
logger主要用于存放日志对象,也可以定义日志类型、级别
name:表示匹配的logger类型前缀,也就是包的前半部分
level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,
false:表示只用当前logger的appender-ref,true:
表示当前logger的appender-ref和rootLogger的appender-ref都有效
-->
<!-- hibernate logger -->
<logger name="com.nasus" level="debug" />
<!-- Spring framework logger -->
<logger name="org.springframework" level="debug" additivity="false"></logger>



<!--
root 与 logger 是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
-->
<root level="info">
<appender-ref ref="stdout" />
<appender-ref ref="appLogAppender" />
</root>
</configuration>

logback节点配置详解

根节点

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<configuration debug="true" scan="true" scanPeriod="2">
<!--TODO : 子节点信息-->
</configuration>

属性

  • debug : 默认为false ,设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。

  • scan : 配置文件如果发生改变,将会重新加载,默认值为true;

  • scanPeriod : 检测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位时毫秒,当scan为true时,这个属性生效,默认时间间隔为1min。

  • 可以这样描述配置文件的基本结构:以<configuration>开头,后面有零
    个或多个<appender>元素,有零个或多个<logger>元素,有最多一个<root>元素

子节点

  1. <appender></appender>
  2. <logger></logger>
  3. <root></root>

appender

  • <appender><configuration> 的子节点,是负责写日志的组件。

  • appender 有两个必要属性 name ,class

  • name指定appender 的名称,

  • class 指定appender的全限定名

  • class 包括 :

    • ch.qos.logback.core.ConsoleAppender
    • ch.qos.logback.core.FileAppender
    • ch.qos.logback.core.RollingFileAppender
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?xml version="1.0" encoding="utf-8"?>
    <configuration debug="true" scan="true" scanPeriod="2">
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    </appender>

    <!-- conf file out -->
    <appender name="file_out" class="ch.qos.logback.core.FileAppender">
    </appender>

    <!-- conf file out -->
    <appender name="file_out" class="ch.qos.logback.core.RollingFileAppender">
    </appender>

    <root></root>
    <loger></loger>
    </configuration>
ConsoleAppender

把日志添加到控制台,有如下节点:

  • <encoder> : 对日志进行格式化。

  • <target> : 字符串System.out 或者 System.err, 默认 System.out;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
    <pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
    </encoder>
    </appender>

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

把日志添加到文件,有如下节点:

  • <file> : 被写入的文件名,可以是相对目录 , 也可以是绝对目录 , 如果目录不存在则会自动创建

  • <append> : 如果是true , 日志被追加到文件结尾 , 如果是false,清空现存文件 , 默认是true

  • <encoder> : 对日志进行格式化 [具体的转换符说明请参见官网.

  • <prodent> : 如果是true,日志会被安全的写入文件 , 即使其他的FileAppender也会向此文件做写入操作 , 默认是false

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
    <pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
    </encoder>
    </appender>

    <!-- conf file out -->
    <appender name="file_out" class="ch.qos.logback.core.FileAppender">
    <file>logs/debug.log</file>
    <encoder>
    <pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
    </encoder>
    </appender>
    </configuration>
RollingFileAppender [常用]

滚动纪录文件,先将日志记录到指定文件,当符合某种条件时,将日志记录到其他文件,有如下节点:

  • <file> : 被写入的文件名,可以是相对目录,也可以解决目录,如果目录不存在则自动创建。

  • <append> : 如果是true,日志被追加到文件结尾,如果是false,清空现存文件,默认是true;

  • <encoder> : 对日志进行格式化 [具体的转换符说明请参见官网.]

  • <rollingPolicy> : 当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。

滚动策略

TimeBaseRollingPolicy
  • 最常用的滚动策略,根据时间来制定滚动策略,即负责滚动也负责触发滚动。有如下节点;

    • <fileNamePattern>: 必要节点,包含文件及“%d” 转换符,“%d”可以包含一个java.text.SimpleDateFormat 制定的时间格式,如:%d{yyyy-MM},如果直接使用 %d ,默认格式是 yyyy-MM-dd。
  • RollingFileAppender 的file 子节点可有可无,通过设置file,可以为活动文件和归档文件制定不同位置,当前日志总是纪录到file指定的文件,活动文件的名称不会改变,如果没有设置file,活动文件 的名称会根据fileNamePattern的值,每隔一段时间改变一次,“/”或者“\” 会被当作目录分隔符。

  • <maxHistory>: 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件,假设设置每个月滚动,且 是 6,则只保存最近6个月的文件,删除之前的旧文件,注意:删除旧文件是哪些为了归档而创建的目录也会被删除。

  • <filenamePattern>: 必须包含“%i” 例如:设置最小值,和最大值分别为1和2,命名模式为 log%i.log,会产生归档文件log1.log和log2.log,还可以指定文件压缩选项,例如:log%i.log.gz 或者 log%i.log.zip

  • <triggeringPolicy> :告知RollingFileAppender 激活RollingFileAppender滚动

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- 03:conf errorAppender out -->
    <appender name="errorAppender" class="ch.qos.logback.core.RollingFileAppender">
    <file>logs/error.log</file>
    <!-- 设置滚动策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--设置日志命名模式-->
    <fileNamePattern>errorFile.%d{yyyy-MM-dd}.log</fileNamePattern>
    <!--最多保留30天log-->
    <maxHistory>30</maxHistory>
    </rollingPolicy>
    <!-- 超过150MB时,触发滚动策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <maxFileSize>150</maxFileSize>
    </triggeringPolicy>
    <encoder>
    <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
    </encoder>
    </appender>
SizeBasedTriggeringPolicy
  • 查看当前活动文件的大小 , 如果超过指定大小会告知 RollingFileAppender , 触发当前活动滚动 , 只有一个节点 , 用来规定文件大小

  • <maxFileSize> : 活动文件的大小 , 默认10MB

  • <prudent>:当为true时 , 不支持FixedWindowRollingPolicy , 支持TimeBasedRollingPolicy , 但是有两个限制

    1. 不支持也不允许文件压缩
    2. 不能设置file属性 . 必须留空

过滤节点

  • Logback 的过滤器基于三值逻辑(ternary logic),允许把它们组装或成链,从而组成任 意的复合过滤策略。

  • 这里的所谓三值逻辑是说,过滤器的返回值只能是 ACCEPT、DENY 和 NEUTRAL 的其中一个。

  • 过滤器一般分为如下几类

级别过滤器(LevelFilter)
  • LevelFilter 根据记录级别对记录事件进行过滤。

  • 如果事件的级别等于配置的级别,过滤 器会根据 onMatch 和 onMismatch 属性接受或拒绝事件。

  • 下面是个配置文件例子:

    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>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <!-- 过滤掉非INFO级别 -->
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>

    <encoder>
    <pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
    </encoder>
    </appender>
    <root level="DEBUG">
    <appender-ref ref="console_out" />
    </root>
    </configuration>
临界值过滤器(ThresholdFilter)
  • ThresholdFilter 过滤掉低于指定临界值的事件 . 当记录的级别等于或高于临界值时 , ThresholdFilter 的decide()方法会返回NEUTRAL ;

  • 当记录级别低于临界值时 , 事件会被拒绝

  • 下面是个配置文件例子 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <!-- 过滤掉TRACE和DEBUG级别的日志 -->
    <level>INFO</level>
    </filter>

    <encoder>
    <pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
    </encoder>
    </appender>
    <root level="DEBUG">
    <appender-ref ref="console_out" />
    </root>
    </configuration>
求值过滤器(EvaluatorFilter)
  • EvaluatorFilter 封装了 EventEvaluator(ch.qos.logback.core.boolex.EventEvaluator) , 评估 是否符合指定的条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.EvaluatorFilter">
<evaluator>
<!--过滤掉所有日志中不包含hello字符的日志-->
<expression>
message.contains("hello")
</expression>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</evaluator>
</filter>

<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
匹配器(Matchers)
  • 尽管能通过调用 String 类的 matches()方法进行模式匹配,但这会导致每次调用过滤器时都会创建一个全新的 Pattern 对象。

  • 为消除这种开销,你可以预先定义一个或多个 Matcher 对象。一旦定义 matcher 后,就可以在求值表达式里重复引用它。

    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
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.EvaluatorFilter">
    <evaluator>
    <matcher>
    <Name>odd</Name>
    <!-- 过滤掉序号为奇数的语句-->
    <regex>statement [13579]</regex>
    </matcher>
    <expression>odd.matches(formattedMessage)</expression>
    <onMatch>NEUTRAL</onMatch>
    <onMismatch>DENY</onMismatch>
    </evaluator>
    </filter>

    <encoder>
    <pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
    </encoder>
    </appender>
    <root level="DEBUG">
    <appender-ref ref="console_out" />
    </root>
    </configuration>

至此 , Appender 节点已经介绍完毕.

logger 节点

  • logger 是 <configuration> 的子节点

  • 来设置某一个包或者具体的某一个类的日志打印级别,以及指定<appender>,

  • logger 仅有一个name属性,两个可选属性 leveladdtivity

    • name : 用来指定受此loger约束的某一个包或者具体的某一个类
level
  • 用来设置打印级别,大小写无关,TRACE,DEBUG,INFO,WARE,ERROR,ALL和OFF,

  • 还有一个特俗值INHERITED 或者 同义词NULL,代表强制执行上级的级别。

  • 如果未设置此属性,那么当前logger将会继承上级的级别。

  • level 大小 : ERROR > WARN > INFO > DEBUG > TRACE

  • 程序会打印高于或等于所设置级别的日志

addtivity
  • 是否向上级loger传递打印信息,默认为true;

  • <loger> 可以包含零个或多个<appender-ref>元素,表示这个appender将会添加到loger

    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
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <!-- 过滤掉非INFO级别 -->
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    </appender>

    <!-- conf infoAppender out -->
    <appender name="infoAppender" class="ch.qos.logback.core.RollingFileAppender">

    <file>logs/info.log</file>
    <!-- 设置滚动策略 -->
    <rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--设置日志命名模式-->
    <fileNamePattern>infoFile.%d{yyyy-MM-dd}.log</fileNamePattern>
    <!--最多保留30天log-->
    <maxHistory>30</maxHistory>
    </rollingPoliy>
    <!-- 超过150MB时,触发滚动策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <maxFileSize>150</maxFileSize>
    </triggeringPolicy>
    <encoder>
    <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
    </encoder>
    </appender>

    <!-- 指定在logback.olf.log包中的log -->
    <logger name="logback.olf.log" level="info">
    <appender-ref ref = "console_out"/>
    <appender-ref ref = "infoAppender"/>
    </logger>
    </configuration>

root 节点

  • 元素配置根 logger。该元素有一个 level 属性。

  • 没有 name 属性,因为已经被命名 为“root”

  • Level 属性的值大小写无关,其值为下面其中一个字符串:TRACE、DEBUG、INFO、 WARN、ERROR、ALL 和 OFF。

  • 意不能设置为“INHERITED” 或“NULL”

  • 元素可以包含零个或多个元素。声明元素后,会先关闭然后移除全部当前 appender,只引用声明了的 appender。

  • 如果 root 元素没有引用任何 appender,就会失去所有 appender。

  • 如下完整案例配置 :

    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
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <!-- 过滤掉非INFO级别 -->
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    </appender>

    <!-- 01:conf infoAppender out -->
    <appender name="infoAppender" class="ch.qos.logback.core.RollingFileAppender">

    <file>logs/info.log</file>
    <!-- 设置滚动策略 -->
    <rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--设置日志命名模式-->
    <fileNamePattern>infoFile.%d{yyyy-MM-dd}.log</fileNamePattern>
    <!--最多保留30天log-->
    <maxHistory>30</maxHistory>
    </rollingPoliy>
    <!-- 超过150MB时,触发滚动策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <maxFileSize>150</maxFileSize>
    </triggeringPolicy>
    <encoder>
    <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
    </encoder>
    </appender>

    <!-- 02:conf debugAppender out -->
    <appender name="debugAppender" class="ch.qos.logback.core.RollingFileAppender">
    <file>logs/debug.log</file>
    <!-- 设置滚动策略 -->
    <rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--设置日志命名模式-->
    <fileNamePattern>debugFile.%d{yyyy-MM-dd}.log</fileNamePattern>
    <!--最多保留30天log-->
    <maxHistory>30</maxHistory>
    </rollingPoliy>
    <!-- 超过150MB时,触发滚动策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <maxFileSize>150</maxFileSize>
    </triggeringPolicy>
    <encoder>
    <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
    </encoder>
    </appender>

    <!-- 03:conf errorAppender out -->
    <appender name="errorAppender" class="ch.qos.logback.core.RollingFileAppender">
    <file>logs/error.log</file>
    <!-- 设置滚动策略 -->
    <rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--设置日志命名模式-->
    <fileNamePattern>errorFile.%d{yyyy-MM-dd}.log</fileNamePattern>
    <!--最多保留30天log-->
    <maxHistory>30</maxHistory>
    </rollingPoliy>
    <!-- 超过150MB时,触发滚动策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <maxFileSize>150</maxFileSize>
    </triggeringPolicy>
    <encoder>
    <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
    </encoder>
    </appender>

    <root level="ALL">
    <appender-ref ref="infoAppender"/>
    <appender-ref ref="debugAppender"/>
    <appender-ref ref="errorAppender"/>
    </root>
    </configuration>

logback 执行流程

  1. 获得过滤链的策略

    依据过滤器链返回的结果做出不同的响应。共有三个响应结果:
    FilterReply.DENY, 直接退出,不执行后续流程
    FilterReply.NEUTRA,继续向下执行
    FilterReply.ACCEPT,不进行步骤二,即类型输出类型检查

  2. 执行基本的选择规则

    主要是比较下level,如果级别低直接退出后续执行

  3. 创建LoggingEvent对象

    这个对象包裹一些基本信息,包括日志界别,信息本身,可能的异常信息,执行时间,执行线程,其实一些随日志请求一起发出的数据和MDC。其中MDC是用来装一些额外的上下文信息的。

  4. 调用appenders

    此时logback会调用appender的doAppender,如果appender里有一些filer的话,此时也会调用

  5. 格式化输出结果

    通常情况下都是由layout层将event格式化成String型。当然也有意外比如说SocketAppender就是将event格式化成流。

  6. 输出LoggingEvent

    将格式化好的结果,输出到appender中记录的地址

使用logback

  • 官方文档

  • 默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台。 一般情况下,我们不会切换掉 Logback,因为 Logback 相对于其他的日志框架速度更快,占用内存更少。

    • 如何使用 logback 记录程序运行过程中的日志,
    • 如何配置 logback,可以同时生成控制台日志和文件日志记录,
    • 文件日志以日期和大小进行拆分生成。
  • 启动日志

  • 从上图可以看到,日志输出内容元素具体如下:

    1
    2
    3
    4
    5
    6
    7
    时间日期:精确到毫秒
    日志级别:ERROR, WARN, INFO, DEBUG or TRACE
    进程ID
    分隔符:— 标识实际日志的开始
    线程名:方括号括起来(可能会截断控制台输出)
    Logger名:通常使用源代码的类名
    日志内容

添加依赖

  • spring-boot-starter 其中包含了 spring-boot-starter-logging ,该依赖内容就是 Spring Boot 默认的日志框架 logback,所以实际开发中我们不需要直接添加该依赖

  • Logback使用到的jar包主要是下面三个:

    1. logback-classic-1.2.3.jar
    2. slf4j-api-1.7.29.jar
    3. logback-core-1.2.3.jar
  • dependencies如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>

    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
    </dependencies>

注解@Slf4j的使用

  • 如果不想每次都写private final Logger logger = LoggerFactory.getLogger(当前类名.class); 可以用注解@Slf4j;
  1. 使用idea首先需要安装Lombok插件

  2. 在pom文件加入lombok的依赖

  3. 类上面添加@Sl4j注解,然后使用log打印日志

    Sl4j 属于 import lombok.extern.slf4j.Slf4j;

  4. 类上面添加@Data,可以省去setter与getter、toString方法的书写,在编译时,会自动添加到class中

启动类

  • 代码如下:

    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
    package pers.sfl.log;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    @Slf4j
    @SpringBootApplication
    public class LogbackApplication {
    public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(LogbackApplication.class, args);
    int length = context.getBeanDefinitionNames().length;
    log.trace("Spring boot启动初始化了 {} 个 Bean", length);
    log.debug("Spring boot启动初始化了 {} 个 Bean", length);
    log.info("Spring boot启动初始化了 {} 个 Bean", length);
    log.warn("Spring boot启动初始化了 {} 个 Bean", length);
    log.error("Spring boot启动初始化了 {} 个 Bean", length);
    try {
    int i = 0;
    int j = 1 / i;
    } catch (Exception e) {
    log.error("【SpringBootDemoLogbackApplication】启动异常:", e);
    }
    }
    }

  • 控制台输出

日志输出文件

  • 默认情况下,Spring Boot将日志输出到控制台,不会写到日志文件。

  • 使用 Spring Boot在 application.properties 或 application.yml 配置,这样只能配置简单的场景,保存路径、日志格式等

  • 对于复杂的场景(区分 info 和 error 的日志、每天产生一个日志文件等)满足不了,只能自定义配置,下面演示。

简单的场景:application.yml

  • You can also set the location of a file to which to write the log (in addition to the console) by using “logging.file.name”. 您还可以使用“ logging.file.name”来设置要写入日志的文件的位置(除了控制台)。

  • logging.file.name可以设置文件路径和名字

  • logging.file.path:只能设置文件路径,名字默认为spring.log

  • logging.file.namelogging.file.path同时出现,使用logging.file.name,和位置顺序无关(经过测试的结果)

  • 配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server:
    port: 8080
    servlet:
    context-path: /demo
    logging:
    pattern:
    #格式化,只能输出日期和内容
    console: "%d - %msg%n"
    file:
    # 文件名为aaa.log
    name: "D:/bbb.log"
    # 文件名为spring.log
    path: "D:/"

复杂的场景: Spring Boot Logback扩展

  • 官方文档

  • logback-spring.xml加载早于application.properties

  • 常用配置

    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
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    <property name="FILE_ERROR_PATTERN"
    value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} %file:%line: %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <!-- 输出控制台日志记录并且仅将输出写入文件-->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <!-- 禁用输出控制台日志记录并且仅将输出写入文件-->
    <!-- <include resource="org/springframework/boot/logging/logback/file-appender.xml" />-->
    <!-- 输出控制台日志记录并且将禁用输出写入文件-->
    <!-- <include resource="org/springframework/boot/logging/logback/console-appender.xml" />-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>INFO</level>
    </filter>
    <encoder>
    <pattern>${CONSOLE_LOG_PATTERN}</pattern>
    <charset>UTF-8</charset>
    </encoder>
    </appender>

    <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!--如果只是想要 Info 级别的日志,只是过滤 info 还是会输出 Error 日志,因为 Error 的级别高, 所以我们使用下面的策略,可以避免输出 Error 的日志-->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <!--过滤 Error-->
    <level>ERROR</level>
    <!--匹配到就禁止-->
    <onMatch>DENY</onMatch>
    <!--没有匹配到就允许-->
    <onMismatch>ACCEPT</onMismatch>
    </filter>
    <!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天的日志改名为今天的日期。即,<File> 的日志都是当天的。-->
    <!--<File>logs/info.spring-boot-demo-logback.log</File>-->
    <!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
    <FileNamePattern>logs/spring-boot-demo-logback/info.created_on_%d{yyyy-MM-dd}.part_%i.log</FileNamePattern>
    <!--只保留最近90天的日志-->
    <maxHistory>90</maxHistory>
    <!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
    <!--<totalSizeCap>1GB</totalSizeCap>-->
    <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
    <!-- maxFileSize:这是活动文件的大小,默认值是10MB,本篇设置为1KB,只是为了演示 -->
    <maxFileSize>2MB</maxFileSize>
    </timeBasedFileNamingAndTriggeringPolicy>
    </rollingPolicy>
    <!--<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
    <!--<maxFileSize>1KB</maxFileSize>-->
    <!--</triggeringPolicy>-->
    <encoder>
    <pattern>${FILE_LOG_PATTERN}</pattern>
    <charset>UTF-8</charset> <!-- 此处设置字符集 -->
    </encoder>
    </appender>

    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的,ThresholdFilter-->
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>Error</level>
    </filter>
    <!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天的日志改名为今天的日期。即,<File> 的日志都是当天的。-->
    <!--<File>logs/error.spring-boot-demo-logback.log</File>-->
    <!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
    <FileNamePattern>logs/spring-boot-demo-logback/error.created_on_%d{yyyy-MM-dd}.part_%i.log</FileNamePattern>
    <!--只保留最近90天的日志-->
    <maxHistory>90</maxHistory>
    <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
    <!-- maxFileSize:这是活动文件的大小,默认值是10MB,本篇设置为1KB,只是为了演示 -->
    <maxFileSize>2MB</maxFileSize>
    </timeBasedFileNamingAndTriggeringPolicy>
    </rollingPolicy>
    <encoder>
    <pattern>${FILE_ERROR_PATTERN}</pattern>
    <charset>UTF-8</charset> <!-- 此处设置字符集 -->
    </encoder>
    </appender>

    <root level="info">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE_INFO"/>
    <appender-ref ref="FILE_ERROR"/>
    </root>
    </configuration>

AOP切面的方式记录日志

依赖文件

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- 解析 UserAgent 信息 -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
</dependency>
</dependencies>

切面通知

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
package pers.sfl.log.aop.aspectj;

import cn.hutool.json.JSONUtil;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;

/**
* 使用 aop 切面记录请求日志信息
* @description: 使用 aop 切面记录请求日志信息
*/
@Aspect
@Component
@Slf4j
public class AopLog {
private static final String START_TIME = "request-start";

/**
* 切入点
*/
@Pointcut("execution(public * pers.sfl.log.aop.controller.*Controller.*(..))")
public void log() {

}

/**
* 前置操作
*
* @param point 切入点
*/
@Before("log()")
public void beforeLog(JoinPoint point) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

log.info("【请求 URL】:{}", request.getRequestURL());
log.info("【请求 IP】:{}", request.getRemoteAddr());
log.info("【请求类名】:{},【请求方法名】:{}", point.getSignature().getDeclaringTypeName(), point.getSignature().getName());

Map<String, String[]> parameterMap = request.getParameterMap();
log.info("【请求参数】:{},", JSONUtil.toJsonStr(parameterMap));
Long start = System.currentTimeMillis();
request.setAttribute(START_TIME, start);
}

/**
* 环绕操作
*
* @param point 切入点
* @return 原方法返回值
* @throws Throwable 异常信息
*/
@Around("log()")
public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
Object result = point.proceed();
log.info("【返回值】:{}", JSONUtil.toJsonStr(result));
return result;
}

/**
* 后置操作
*/
@AfterReturning("log()")
public void afterReturning() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

Long start = (Long) request.getAttribute(START_TIME);
Long end = System.currentTimeMillis();
log.info("【请求耗时】:{}毫秒", end - start);

String header = request.getHeader("User-Agent");
UserAgent userAgent = UserAgent.parseUserAgentString(header);
log.info("【浏览器类型】:{},【操作系统】:{},【原始User-Agent】:{}", userAgent.getBrowser().toString(), userAgent.getOperatingSystem().toString(), header);
}
}

控制层

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
package pers.sfl.log.aop.controller;

import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
*
* @description: 测试 Controller
*/
@RestController
public class TestController {

/**
* 测试方法
*
* @param who 测试参数
* @return {@link Dict}
*/
@GetMapping("/test")
public Dict test(String who) {
return Dict.create().set("who", StrUtil.isBlank(who) ? "me" : who);
}

}