SpringBoot使用Slf4j+Log4j2完成项目的日志记录

x33g5p2x  于2022-02-07 转载在 Spring  
字(12.7k)|赞(0)|评价(0)|浏览(436)

SpringBoot使用Slf4j+Log4j完成项目的日志记录

前言

本示例采用SpringBoot项目使用SpringAOP记录日志,Slf4j作为日志门面,Log4j2作为日志实现实,实现开发中的日志记录.

部分效果展示 :

日志文件 :

日志信息 :

代码具体实现如下 :

  1. 因为SpringBoot自动集成了Slf4j日志门面并且同样集成了logback等日志实现,Log4j2和Logback并不能共存,所以我们要先排除依赖,并添加Log4j2的依赖与SpringAOP的依赖。
    避坑 : 在网上有很多人说是在 spring-boot-starter-web 这个启动器里面进行依赖排除,但是经过我的测试这种方法有时候并不是有效的,所以复制上面的依赖就好。
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <!-- 排除springboot自带的logback框架 -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <!-- 引入log4j2依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <!-- SpringAOP启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.4.5</version>
        </dependency>

二、编写log4j2-spring.xml : log4j2的配置文件

代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<!--status = "warn" 日志框架本身的输入日志级别
    monitorInterval = "5" 自动加载配置文件的时间间隔不低于5s
 -->
<Configuration status="warn" monitorInterval="5">

    <!-- 日志级别以及优先级排序 :
        在log4j2中, 共有8个级别,按照从低到高为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF。
        All:最低等级的,用于打开所有日志记录.
                                 Trace:是追踪,就是程序推进一下.

                                 Debug:指出细粒度信息事件对调试应用程序是非常有帮助的.

                                 Info:消息在粗粒度级别上突出强调应用程序的运行过程.

                                 Warn:输出警告及warn以下级别的日志.

                                 Error:输出错误信息日志.

                                 Fatal:输出每个严重的错误事件将会导致应用程序的退出的日志.

                                 OFF:最高等级的,用于关闭所有日志记录.

        程序会打印高于或等于所设置级别的日志,设置的日志等级越高,打印出来的日志就越少。
    -->

    <!--
    集中配置属性进行管理
    -->
    <Properties>
        <!--
            定义格式化输出:
            %d表示日期,
            %thread表示线程名,
            %-5level:级别从左显示5个字符宽度
            %msg:日志消息,%n是换行符
            %logger{36} 表示 Logger 名字最长36个字符
        -->
        <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level}[%thread] %style{%logger{36}}{cyan} : %msg%n" />

        <!-- 定义日志存储的路径,绝对路径 -->
        <property name="FILE_PATH" value="存储的绝对路径" />
        <property name="FILE_NAME" value="项目名称" />
    </Properties>

    <!--日志处理-->
    <Appenders>
        <!--*********************控制台日志***********************-->
        <!--
            target: SYSTEM_OUT 或 SYSTEM_ERR,一般只设置默认:SYSTEM_OUT.
        -->
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式和颜色-->
            <PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false" noConsoleNoAnsi="false"/>
            <!--控制台只输出level及其以上级别的信息(onMatch)放行,其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
        </console>

        <!--*********************文件日志***********************-->

        <!--按照一定规则查分日志文件的appender /logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log
                                              /logs:放在logs这个目录下,/$${date:yyyy-MM-dd}:以天为单位生成文件夹
                                              myrollog-%d{yyyy-MM-dd-HH-mm}-%d: 以分钟为单位到达了指定大小在进行拆分
                                              .gz 进行压缩归档
        -->

        <!--
        error 运行时异常日志信息
        -->
        <RollingFile name = "errorRollingFile" fileName = "${FILE_NAME}/error日志.log"
                     filePattern = "${FILE_PATH}/$${date:yyyy-MM-dd}/error-%d{yyyy-MM-dd-HH-mm}-%i.log.gz">
            <!--日志级别过滤器,文件只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--日志的消息格式-->
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n"/>
            <!--在系统每次启动时,触发拆分规则,生产一个新的日志文件-->
            <OnStartupTriggeringPolicy/>
            <!--按照文件大小进行拆分-->
            <SizeBasedTriggeringPolicy size = "10 MB"/>
            <!--按照时间节点进行拆分-->
            <TimeBasedTriggeringPolicy/>
            <!--在同一个目录下,文件的个数限定为30个,超过按照实际进行覆盖-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!--
        fatal 正常运行时日志
        -->
        <RollingFile name = "fatalRollingFile" fileName = "${FILE_NAME}/fatal日志.log"
                     filePattern = "${FILE_PATH}/$${date:yyyy-MM-dd}/fatal-%d{yyyy-MM-dd-HH-mm}-%i.log.gz">
            <!--日志级别过滤器,文件只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="fatal" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--日志的消息格式-->
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <!--在系统每次启动时,触发拆分规则,生产一个新的日志文件-->
            <OnStartupTriggeringPolicy/>
            <!--按照文件大小进行拆分-->
            <SizeBasedTriggeringPolicy size = "10 MB"/>
            <!--按照时间节点进行拆分-->
            <TimeBasedTriggeringPolicy/>
            <!--在同一个目录下,文件的个数限定为30个,超过按照实际进行覆盖-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!--
       info 操作日志
       -->
        <RollingFile name = "infoRollingFile" fileName = "${FILE_NAME}/info日志.log"
                     filePattern = "${FILE_PATH}/$${date:yyyy-MM-dd}/info-%d{yyyy-MM-dd-HH-mm}-%i.log.gz">
            <!--日志级别过滤器,文件只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--日志的消息格式-->
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <!--在系统每次启动时,触发拆分规则,生产一个新的日志文件-->
            <OnStartupTriggeringPolicy/>
            <!--按照文件大小进行拆分-->
            <SizeBasedTriggeringPolicy size = "10 MB"/>
            <!--按照时间节点进行拆分-->
            <TimeBasedTriggeringPolicy/>
            <!--在同一个目录下,文件的个数限定为30个,超过按照实际进行覆盖-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

    </Appenders>

    <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
    <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--过滤掉spring和mybatis的一些无用的信息-->
        <logger name="org.mybatis" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </logger>
        <!--监控系统信息-->
        <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
        <Logger name="org.springframework" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>

        <root level="info">
            <!--控制台-->
            <appender-ref ref="Console"/>
            <!--用户操作文件-->
            <appender-ref ref="infoRollingFile"/>
            <!--调试错误文件-->
            <appender-ref ref="errorRollingFile"/>
            <!--正常运行文件-->
            <appender-ref ref="fatalRollingFile"/>
        </root>

    </loggers>

</Configuration>

三、添加全局异步日志 : log4j2.component.properties

Log4j2的最大优点就是它的异步Logger,主要就是性能更好,这个这里不做过多解释。

#全局异步日志开启,提高日志性能
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

四、声明log4j2的配置文件路径 :

在application.properties的SpringBoot主配置文件中添加如下配置 :

我这里是给配置文件添加了个config的包目录,你要改成你自己的路径。

五、自定义注解类 :

自定义注解 : 主要是作用就是为了自定义方法操作,用于向AOP中添加方法操作的日志信息,既然已经到项目日志阶段那么我相信你也已经不是一个小白了,注解就是一个标注这里也不做过多解释。

//作用在方法上
@Target(ElementType.METHOD)
//运行时
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotationMethod {
    //模块名称
    String module() default "";
    //操作名称
    String operator() default "";
    //扩展属性
    String value() default "";
}

六、创建HttpContextUtil工具类用于在IP工具类中获取IP使用 :

public class HttpContextUtil {

    public static HttpServletRequest getHttpServletRequest(){
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

}

七、创建IP获取工具类 :

用于获取访问的用户IP,并记录到日志中。

@Slf4j
public class IpUtils {

    /**
     * 获取IP地址
     * <p>
     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = null, unknown = "unknown", seperator = ",";
        int maxLength = 15;
        try {
            ip = request.getHeader("x-forwarded-for");
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        } catch (Exception e) {
            log.error("IpUtils ERROR ", e);
        }

        // 使用代理,则获取第一个IP地址
        if (StringUtils.isEmpty(ip) && ip.length() > maxLength) {
            int idx = ip.indexOf(seperator);
            if (idx > 0) {
                ip = ip.substring(0, idx);
            }
        }

        return ip;
    }

    /**
     * 获取ip地址
     *
     * @return
     */
    public static String getIpAddr() {
        HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
        return getIpAddr(request);
    }
}

八、 编写AOP日志切面类

此类用于保存和执行具体的日志信息。

/**
 * @author 码不多
 * @version 1.0
 * @description: 此类采用SpringAOP,用于记录日志
 */

//声明AOP类
@Aspect
//声明组件
@Component
//使用Slf4j日志门面
@Slf4j
public class AopLogUtil {

    //定义切入点方法 标注这个自定义注解
    @Pointcut("@annotation(com.shouzhong.epidemicprevention.annotation.LogAnnotationMethod)")
    public void pt(){}

    /**
     * 功能描述: 执行通知日志的方法
     * @author 码不多
     * @date 2021/8/16
     * @param point
     * @return java.lang.Object
     */
            //拦截所有的Controller或者RestController或者自定义注解标识的方法
    @Around("@within(org.springframework.stereotype.Controller) ||"+
            "@within(org.springframework.web.bind.annotation.RestController) ||"+
    "pt()"
    )
    private Object runAndSaveLog(ProceedingJoinPoint point){
        //获取获取当前时间
        long beginTime = System.currentTimeMillis();
        //执行原始方法
        Object result = null;
        try {
            result = point.proceed();
        } catch (Throwable throwable) {
            //出现异常打印error日志
            log.error("Aop中方法出现异常",throwable);
        }

        //获取方法执行时间
        long runtime = System.currentTimeMillis() - beginTime;
        //调用保存日志的方法
        recordLog(point,runtime);
        //将结果返回
        return  result;
    }

    /**
     * 功能描述: 保存日志的方法
     * @author 码不多
     * @date 2021/8/16
     * @param joinPoint time
     * @return void
     */
    private void recordLog(ProceedingJoinPoint joinPoint,long time){
    
        //获取类名
        Object target = joinPoint.getTarget();
        String canonicalName = target.getClass().getCanonicalName();

        //获取模块名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        //获取方法名
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();

        //获取请求参数
        Object[] args = joinPoint.getArgs();
        String params = JSON.toJSONString(args[0]);

        //获取request,设置ip地址
        HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
        //获取IP地址
        String ipAddr = IpUtils.getIpAddr(request);

        //获取注解对象
        LogAnnotationMethod logAnnotation = method.getAnnotation(LogAnnotationMethod.class);

        //添加类名到日志,采用占位符赋值
        log.info("类名: {}",canonicalName);
        //添加模块日志信息,采用占位符赋值,通过注解对象获取注解中的值
        log.info("模块名: {}",logAnnotation.module());
        //添加方法名到日志,采用占位符赋值
        log.info("方法名: {}",methodName);
        //添加操作到日志,采用占位符赋值,通过注解对象获取注解中的值
        log.info("操作: {}",logAnnotation.operator());
        //添加请求参数信息到日志,采用占位符赋值
        log.info("请求参数: {}:",params);
        //添加ip地址到日志,采用占位符赋值
        log.info("ip地址: {}",ipAddr);
        //添加执行时间到日志,采用占位符赋值
        log.info("执行时间: {} ms",time);

        //日志结束
        log.info("#####################log End####################");

		

	补充001 :
			//不使用IPUtils获取ip和一些其他请求头日志信息的方法: logger.info不在这里定义。还是在上面的代码中
		/*     // 接收请求,记录请求中的内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 记录下请求内容
        //请求的url
        logger.info("URL : " + request.getRequestURL().toString());
        //请求的方法类型 : GET、POST...
        logger.info("HTTP_METHOD : " + request.getMethod());
        //IP地址
        logger.info("IP : " + request.getRemoteAddr());
        //类方法
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        //参数数组
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));

        //获取所有参数方法 :
        //获取请求参数们
        Enumeration<String> enu=request.getParameterNames();
        //遍历
        while(enu.hasMoreElements()){
            //取出请求参数名
            String paraName=(String)enu.nextElement();
            //请求的具体参数值
            System.out.println(paraName+": "+request.getParameter(paraName));
        }
    }*/
    
  补充002:
	/*
	        //如果有文件上传的参数MultipartFile类型,为了避免冲突可以给它们添加进集合展示
	        // 接收请求,记录请求中的内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
         	//创建集合存储请求参数
	        ArrayList paramsList = new ArrayList();
	        //获取所有参数方法 :
	        Enumeration<String> enu=request.getParameterNames();
	        while(enu.hasMoreElements()){
	            String paraName=(String)enu.nextElement();
	            //将参数添加到集合
	            paramsList.add(paraName+": "+request.getParameter(paraName));
	        }
		log.info("请求参数 :{}",paramsList)
	*/

		
    }
}

九、控制器日志添加

在你需要记录操作日志的Controller层的请求映射的方法上添加你的自定义注解,并将你的方法日志信息添加到你自定义的属性值中。

//自定义注解,声明方法日志
    @LogAnnotationMethod(module = "获得信息列表",operator = "分页查询或多条件查询信息需求列表")
    @RequestMapping("/InforDS")
    public Pagination<InforDemandSide> inforDemandSide(Pagination pagination){
		xxxxxxxxxxxxxxxxx;
	}

十、开启控制台Mybatis的sql输出 :

在application.properties的主配置文件中添加如下配置

简单总结一下 :

整体日志记录就是采用面向切面编程,你可以在配置文件中配置你的Appender或者修改你的日志级别。
如果你想在方法中使用日志记录具体的操作,你可以在你需要记录的类上添加@Slf4j这个注解,在你需要记录日志的地方直接使用 log.xxx() 直接记录你的日志即可。

如果这篇文章对你有帮助请为我点个小赞支持一下!

相关文章