springboot使用slf4j+log4j2实现项目日志记录

后端Java是如何记录项目日志的

1.日志的概念

项目中日志文件作用的是什么
日志文件是用于记录系统操作事件的文件集合,可分为事件日志和消息日志。具有处理历史数据、诊断问题的追踪以及理解系统的活动等重要作用。
在计算机中,日志文件是记录在操作系统或其他软件运行中发生的事件或在通信软件的不同用户之间的消息的文件。记录是保持日志的行为。在最简单的情况下,消息被写入单个日志文件 。
许多操作系统,软件框架和程序包括日志系统。广泛使用的日志记录标准是在因特网 工程任务组(IETF )RFC 5424中定义的 syslog。 syslog标准使专用的标准化子系统能够生成,过滤,记录和分析日志消息。

什么是调试日志
软件开发中,我们经常需要去调试程序,做一些信息,状态的输出便于我们查询程序的运行状况。为了让我们能够更加灵活和方便的控制这些调试的信息,所有我们需要专业的日志技术。java中寻找bug会需要重现。调试也就是debug 可以在程序运行中暂停程序运行,可以查看程序在运行中的情况。日志主要是为了更方便的去重现问题。

什么是系统日志
系统日志是记录系统中硬件、软件和系统问题的信息,同时还可以监视系统中发生的事件。用户 可以通过它来检查错误发生的原因,或者寻找受到攻击时攻击者留下的痕迹。系统日志包括系统 日志、应用程序日志和安全日志。

系统日志的价值
系统日志策略可以在故障刚刚发生时就向你发送警告信息,系统日志帮助你在最短的时间内发现问题。
系统日志是一种非常关键的组件,因为系统日志可以让你充分了解自己的环境。这种系统日志信息对于决定故障的根本原因或者缩小系统攻击范围来说是非常关键的,因为系统日志可以让你了解故障或者袭击发生之前的所有事件。为虚拟化环境制定一套良好的系统日志策略也是至关重要的,因为系统日志需要和许多不同的外部组件进行关联。良好的系统日志可以防止你从错误的角度分析问题,避免浪费宝贵的排错时间。另外一种原因是借助于系统日志,管理员很有可能会发现一些之前从未意识到的问题,在几乎所有刚刚部署系统日志的环境当中。

2. 日志门面和日志实现

当我们的系统变的更加复杂的时候,我们的日志就容易发生混乱。随着系统开发的进行,可能会更新不同的日志框架,造成当前系统中存在不同的日志依赖,让我们难以统一的管理和控制。就算我们强制要求所有的模块使用相同的日志框架,系统中也难以避免使用其他类似
spring,mybatis等其他的第三方框架,它们依赖于我们规定不同的日志框架,而且他们自身的日志系统就有着不一致性,依然会出来日志体系的混乱。
所以我们需要借鉴JDBC的思想,为日志系统也提供一套门面,那么我们就可以面向这些接口规范来开发,避免了直接依赖具体的日志框架。这样我们的系统在日志中,就存在了日志的门面和日志的实现。

常见的日志门面 :
JCL:日志实现一套统一的api接口,apache推出的一个日志门面技术。JCL也是有缺陷的他当时只考虑了,主流的jcl和log4j。后面随着技术的的发展新的日志技术JCL是不支持的。后面jcl渐渐被淘汰了。

slf4j:slf4j站出来了,日志实现一套统一的api接口。日志门面技术比JCL更为强大,slf4j是目前主流的日志门面技术,可以操作所有的日志实现框架。

常见的日志实现:
JUL:java自带的日志实现框架,功能比较单一、
log4j:apache推出的实现框架,相对jul他的功能就比较强大了、
logback:日志实现框架性能比log4j好很多、比较火热的springboot2.0以后默认推荐使用了、
log4j2:Log4j 2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升log4j2是目前日志实现框架性能最好的,没有之一。

3. SpringBoot 中的日志使用

springboot框架在企业中的使用越来越普遍,springboot日志也是开发中常用的日志系统。springboot默认就是使用SLF4J作为日志门面,logback作为日志实现来记录日志。

4. SpringBoot中的日志设计

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

spring-boot-starter-logging日志启动器默认就是完成日志传递的。该依赖用于开启springboot默认的slf4j日志门面和logback日志实现。由于spirngboot的依赖传递所以该依赖可以不用我们单独导入,导入springboot的相关依赖之后该依赖就存在了。
springboot日志依赖关系图:
springboot使用slf4j+log4j2实现项目日志记录_第1张图片

5. springboot依赖总结

spring-boot-starter-logging日志启动器默认就是完成日志传递的

  1. springboot 底层默认使用logback作为日志实现。
  2. 使用了SLF4J作为日志门面
  3. 将JUL也转换成slf4j
  4. 也可以使用log4j2作为日志门面,但是最终也是通过slf4j调用logback

6. springboot使用slf4j+log4j2实现项目日志记录

虽然springboot默认就是使用SLF4J作为日志门面,logback作为日志实现来记录日志,但是围绕java的日志技术也在不断迭代升级。其中apache组织也对原有的日志实现技术log4j做出了升级,就是现在的日志实现框架log4j2。log4j2作为日志的实现框架他的功能,性能各方面要高于logback很多。所以现在在java项目中也十分主流:slf4j(日志的门面)+log4j2(日志的实现框架),来支撑我们java软件项目中需要的日志技术。
下面就对springboot中如何使用slf4j+log4j2来实现日志。以及记录日志文件在项目中是如何分类的,我们到地应该如何去正确的打印日志。做一个相对全面的demo示例。
start…

1.创建项目
springboot使用slf4j+log4j2实现项目日志记录_第2张图片

2.pom文件导入日志相关依赖

<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <!-- 排除 logback 日志实现。排除日志启动器就表示排除logback了-->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
    <version>2.1.5.RELEASE</version>
</dependency>

<!--starter-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <!--排除springbootloggging起步日志, 也就排除了logback-->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
        <!--排除log4j的适配器,也就排除了log4j-->
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
    <version>2.1.5.RELEASE</version>
</dependency>

<!--导入log4j2依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

<!--导入log4j2 的异步日志依赖-->
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.3.4</version>
</dependency>

<!--spring拦截器 AOP-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.4</version>
</dependency>

<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
</dependency>

说明:如果你正在做某某项目正好需要使用到slf4j+logj2,导入上面的的日志是十分必要的。除过log4j2的相关依赖我还导入了其他依赖。AOP、fastjson、lombok后面有用处。

3.日志的配置文件log4j2.xml,该配置的作用:可以将log4j2日志按级别输出到不同文件中。




<Configuration monitorInterval="5">

    
    <Properties>
        <Property name="APP_NAME">duobao-log4j2Property>
        <Property name="LOG_FILE_PATH">D:/logs/${APP_NAME}Property>
        <Property name="PATTERN_FORMAT">%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%nProperty>
    Properties>

    
    <Appenders>

        
        <Console name="Console" target="SYSTEM_ERR">
            <PatternLayout pattern="${PATTERN_FORMAT}"/>
        Console>


        
        <RollingFile name="RollingInfoFile" fileName="${LOG_FILE_PATH}/info.log"
                     filePattern="${LOG_FILE_PATH}/$${date:yyyyMM}/info-%d{yyyyMMdd}-%i.log.gz">
            
            <Filters>
                <ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>  
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            Filters>

            <PatternLayout>
                <pattern>${PATTERN_FORMAT}pattern> 
            PatternLayout>

            
            <Policies>
                
                <OnStartupTriggeringPolicy/>
                
                <SizeBasedTriggeringPolicy size="30 MB"/>
                
                <TimeBasedTriggeringPolicy/>
            Policies>
            
            <DefaultRolloverStrategy max="30"/>
        RollingFile>


        
        <RollingFile name="RollingWarnFile" fileName="${LOG_FILE_PATH}/warn.log"
                     filePattern="${LOG_FILE_PATH}/$${date:yyyyMM}/warn-%d{yyyyMMdd}-%i.log.gz">
            
            <Filters>
                <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            Filters>

            <PatternLayout>
                <pattern>${PATTERN_FORMAT}pattern> 
            PatternLayout>

            
            <Policies>
                
                <OnStartupTriggeringPolicy/>
                
                <SizeBasedTriggeringPolicy size="30 MB"/>
                
                <TimeBasedTriggeringPolicy/>
            Policies>
            
            <DefaultRolloverStrategy max="30"/>
        RollingFile>

        
        <RollingFile name="RollingErrorFile" fileName="${LOG_FILE_PATH}/error.log"
                     filePattern="${LOG_FILE_PATH}/$${date:yyyyMM}/error-%d{yyyyMMdd}-%i.log.gz">
            
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>

            <PatternLayout>
                <pattern>${PATTERN_FORMAT}pattern>
            PatternLayout>

            <Policies>
                
                <OnStartupTriggeringPolicy/>
                
                <SizeBasedTriggeringPolicy size="30 MB"/>
                
                <TimeBasedTriggeringPolicy/>
            Policies>
            
            <DefaultRolloverStrategy max="30"/>
        RollingFile>

        
        <RollingFile name="RollingDebugFile" fileName="${LOG_FILE_PATH}/debug.log"
                     filePattern="${LOG_FILE_PATH}/$${date:yyyyMM}/debug-%d{yyyyMMdd}-%i.log.gz">
            
            <Filters>
                <ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>  
                <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
            Filters>

            <PatternLayout>
                <pattern>${PATTERN_FORMAT}pattern> 
            PatternLayout>

            
            <Policies>
                
                <OnStartupTriggeringPolicy/>
                
                <SizeBasedTriggeringPolicy size="30 MB"/>
                
                <TimeBasedTriggeringPolicy/>
            Policies>
            
            <DefaultRolloverStrategy max="30"/>
        RollingFile>

    Appenders>


    
    <Loggers>
        
        <Logger name="org.springframework" level="INFO"/>
        <Logger name="org.mybatis" level="INFO"/>

        
        <Logger name="com.luis" level="INFO"/>

        
        <Root level="ALL">
            
            <AppenderRef ref="Console"/>
            <AppenderRef ref="RollingErrorFile"/>
            <AppenderRef ref="RollingWarnFile"/>
            <AppenderRef ref="RollingInfoFile"/>
            <AppenderRef ref="RollingDebugFile"/>
        Root>
    Loggers>

Configuration>

如图:根据上面的log4j2.xml,进行日志输出配置,最终输出的日志就是如下效果。
springboot使用slf4j+log4j2实现项目日志记录_第3张图片

4.全局异步配置:就是所有的日志都异步的记录,在log4j2.xml配置文件上不用做任何改动,只需要添加一个log4j2.component.properties 配置文件;

Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

说明:这里需要提一下什么是同步日志,什么是异步日志。同步日志是跟主线程进行执行的,也就是说日志全部记录完毕之后代码才会往下执行。会出现性能问题,就需要使用到异步日志。为日志重新开一个线程,专门执行日志操作。从而不会影响主线程操作。异步日志,为日志重新开一个线程,专门执行日志操作。从而不会影响主线程操作。

5.利用springAOP的技术,实现参数进入controller方法,返回统一加上日志。
说明:在前后端交互的过程中前端会给我们传递一些参数,我们后端接收到参数之后根据接口需求要返回给前端他所需要的数据。这个时候有一个非常重要的点,我们需要记录日志。记录的日志内容就是前端给我们传递了什么参数,我们要记录到日志文件中。我们给前端返回了什么数据,我们还要记录到日志文件中。还一个点需要我们记录日志,如果我某一个controller中方法出现异常了,出现程序错误这个时候需要记录出错的日志信息吧。那我应该如何拦截这个错误对象并且把他输出到日志文件中呢。

以下以下Aop切面类将解决上述问题。
以下Aop切面类,做的事情
(1) 在方法调用之前, 打印入参,记录日志。
(2) 方法返回之后,打印出参,记录日志
(3) 拦截方法中出现的异常对象。拿到对象之后打印到日志文件。
(value = “execution(public * com.duobao.web.controller...*(…))”):该配置表示对controller包下所有方法进行拦截,进行方法切入。

package com.duobao.web.aop;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 利用springAOP的技术,实现参数进入controller方法,返回统一加上日志
 */
@Aspect
@Component
@Slf4j
public class ControllerLogAop {

    /**
     * 在方法调用之前, 打印入参,记录日志
     */
    @Before(value = "execution(public * com.duobao.web.controller.*.*.*(..))")
    public void before(JoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        StringBuilder params = new StringBuilder();
        for (Object arg : args) {
            params.append(arg).append(" ");
        }
        log.debug(className + "#" + methodName +"【入参】为: " +params.toString());
    }

    /**
     * 监测方法,方法出现异常,catch到异常之后返回包装后的错误信息,并打印日志信息
     * @param joinPoint
     * @return
     */
    @Around(value = "execution(public * com.duobao.web.controller.*.*.*(..))")
    public Object catchException(ProceedingJoinPoint joinPoint) {
        try {
            Object proceed = joinPoint.proceed();//方法执行完返回值
            return proceed;//返回出去
        } catch (Throwable e) {
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            log.error("在"+className+"的"+methodName+",发生了异常:{}",e);
            return "异常"; // return RestData.error("异常")
        }
    }

    /**
     * 返回之后,打印出参,记录日志
     */
    @AfterReturning(value = "execution(public * com.duobao.web.controller.*.*.*(..))",returning = "returnVal")
    public void afterReturn(JoinPoint joinPoint,Object returnVal){
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        log.debug(className +"#" + methodName + "【结果】为: " + JSON.toJSONString(returnVal));
    }
}

6.如下类中介绍了,什么时候打日志,日志输出格式。

package com.duobao.web.logTest;


import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.sql.Connection;


@Slf4j //@Slf4j:注解在类上; 为类提供一个属性名为log的slf4j日志对象
@RestController
@RequestMapping("/logs")
public class LogTest {

    //1.使用slf4j 日志门面框架, 来打印日志
    private static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

    @GetMapping("/test")
    public void test() {
        // LOGGER.info("from log");
        log.info("from log");
        // log.error("error");  // 错误信息, 不会影响系统运行 ***
        // log.warn("warn");    // 警告信息, 可能会发生问题 ***
        // log.info("info");    // 运行信息, 一些普通信息。数据库连接、网络连接、IO 操作等等 *** //默认日志级别
        // log.debug("debug");  // 调试信息, 开发和测试阶段的调试信息, 记录程序变量传递信息等等 ***

        // log.trace("trace");  // 追踪信息, 记录程序所有的流程信息

        // 2.日志打印格式不一样,这个可以自己配 log4j2.xml.xml
        // %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n

        // 大型互联网项目的日志格式
        // 2019-12-01 00:00:00.000
        // |pid
        // |log-level
        // |[svc-name,trace-id,span-id,user-id,biz-id]
        // |thread-name
        // |package-name.class-name :
        // |log message

        // 时间
        // pid, pid
        // log-level, 日志级别
        // svc-name, 应用名称
        // trace-id, 调对链标识
        // span-id, 调用层级标识
        // user-id, 用户标识
        // biz-id, 业务标识
        // thread-name, 线程名称
        // package-name.class-name, 日志记录器名称
        // log message, 日志消息体

        // 3.方法日志
        checkUser("admin", "1234");

        // 4.外部调用 (外部http请求调用的时候)
        bizwithRpc("bywind");

        // 5.程序运行过程中 状态变更 或 条件分支
        stateChange();

        // 6.多个异常情况需要捕获
        tryCatch();
        // 7.能预测到的非正常情况
        someThingMayNotHappenButIncase(10);

        // 8.代码块执行效率记录(运行时间),获取关键数据记录等
        howLongTime();

        // =================日志虽好,也不要乱打哦!=================
        // 9.频繁打日志
        // 当日志生产的速度大于日志写磁盘的速度,会导致日志内容堆积在内存中,导致内存泄漏
        iLoveLog();

        // 10.无意义日志,容易混淆的日志
        useless();

        // 11.如果基础已抛出,请不要打日志, 交给调用者处理
        throwToYou();
    }


    // 3.方法日志
    public Boolean checkUser(String name, String password) {
        log.info("params,name:{},password:{}", name, password);
        System.out.println("do something");
        Boolean result = true;
        log.info("retValue:{} ", result);
        return result;
    }

    // 4.调用外部请求
    public Boolean bizwithRpc(String parameters) {
        log.debug("Calling external system:" + parameters);
        Object result = null;
        try {
            result = callRemotesSystem(parameters);
            log.debug("Called successfully. result is" + result);
        } catch (Exception e) {
            log.warn("Failed at calling xxx system . exception : " + e);
        }
        return true;
    }
    public Object callRemotesSystem(String parameters) {
        System.out.println(parameters);
        return "100";
    }

    // 5.程序运行过程中 状态变更 或 条件分支
    public void stateChange() {
        Boolean isRunning = true;
        log.info("System is running");
        //...
        isRunning = false;
        log.info("System was interrupted by" + Thread.currentThread().getName());
    }

    // 6.多个异常情况需要捕获
    public Boolean tryCatch() {
        try {
            //业务逻辑代码
            return true;
        } catch (RuntimeException e) {
            log.warn("Let me know anything", e);
        } catch (Exception e) {
            log.error("Description xxx", e);
        }
        return false;
    }

    // 7.能预测到的非正常情况
    public Boolean someThingMayNotHappenButIncase(Integer a) {

        int absResult = Math.abs(a);
        if (absResult < 0) {
            log.info("Original int " + a + " has nagetive abs " + absResult);
        }
        return true;
    }

    // 8.代码块执行效率记录(运行时间),获取关键数据记录等
    public void howLongTime() {
        long start = System.currentTimeMillis();
        System.out.println("some amazing code");
        long end = System.currentTimeMillis();

        log.info("time spent :{}", (end - start));
    }

    // 9.当日志生产的速度大于日志写磁盘的速度,会导致日志内容堆积在内存中,导致内存泄漏
    private Boolean iLoveLog() {
        log.debug("method enter");
        System.out.println("call rpc method some code");
        log.info("the method seems good");
        System.out.println("some amazing code of mine");
        log.info("I will return the value");
        return true;
    }

    // 10.无意义日志,容易混淆的日志
    private Boolean useless() {
        File file = new File("xxx");
        if (!file.exists()) {
            log.warn("File does not exist");//Useless message
        }
        Connection connection = getConnection();
        if (connection ==null ){
            log.warn("System initialized unsuccessfully");//不要笼统的说系统异常,应该给出具体错误信息。
        }
        return false;
    }
    private Connection getConnection() { return null; }

    // 11.如果基础已抛出,请不要打日志, 交给调用者处理
    private void throwToYou() {
        try{
            //业务代码
        }catch (RuntimeException e) {
            //log.error("RuntimeException",e);
            throw new RuntimeException("he he");
        }

        
    }

}

7.测试:
定义类,测试日志记录。在该类中我进行了日志记录,主要目的是请求过来之后,测试日志文件中是否可以正常输出我这里打印的日志信息。对于日志信息,我既输出到了控制台同时会输出到文件中。

package com.duobao.web.controller.v1;

import com.duobao.common.dtos.ResponseResult;
import com.duobao.web.service.AdLoginService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 * adUser用户登录
 **/
@Slf4j
@CrossOrigin
@RestController
@RequestMapping("/api/v1/login")
public class AdLoginController {

    @Autowired
    private AdLoginService adLoginService;

    /**
     * adUser用户登录
     *
     * @param account  账号
     * @param password 密码
     * @param request  http请求
     * @return token令牌
     */
    @PostMapping("/in")
    public ResponseResult login(@RequestParam("account") String account,
                                @RequestParam("password") String password, HttpServletRequest request) {
        log.error("error");  // 错误信息, 不会影响系统运行 ***
        log.warn("warn");    // 警告信息, 可能会发生问题 ***
        log.info("info");    // 运行信息, 一些普通信息。数据库连接、网络连接、IO 操作等等 *** //默认日志级别
        log.debug("debug");  // 调试信息, 开发和测试阶段的调试信息, 记录程序变量传递信息等等 ***
        return adLoginService.login(account, password, request);

    }

}

发送请求
在这里插入图片描述

查看日志控制台输出日志效果
springboot使用slf4j+log4j2实现项目日志记录_第4张图片

查看输出的日志文件
springboot使用slf4j+log4j2实现项目日志记录_第5张图片

8.特别说明:

  1. 在配置文件中我们一般会对rootlogger根日志对象进行配置当然也可以自定义logger,需要注意的是,如果rootlogger可以满足需要,直接用即可满足不了,就需要自定义logger。
  2. 项目中日志文件的分类,一般可以放在一个日志文件,也可以根据不同日志级别创建不同的文件,来存储日志。
  3. 项目发布后一般用的是Info,在开发过程中需要做Log的追踪,一般设置为Debug。

该示例基本可以满足开发项目时的日常日志记录,是本人查阅了很多资料视频总结所得~

你可能感兴趣的:(Java,SpringBoot)