Springboot之日志处理

Springboot之日志处理

一、使用log4j2进行日志管理

1. log4j2日志管理框架的简介

Log4j2log4j 1.xlogback的改进版,据说采用了一些新技术(无锁异步、等等),使得日志的吞吐量、性能比log4j 1.x提高10倍,并解决了一些死锁的bug,而且配置更加简单灵活,官网地址: http://logging.apache.org/log4j/2.x/manual/configuration.html

2. 替换Springboot默认的logback日志框架

先去掉Springboot自带的日志框架

<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>

添加log4j2日志框架

<!--log4j2异步输出的依赖包-->
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>${com.lamx.version}</version>
</dependency>
<!--   Add Log4j2 Dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

二、配置日志处理的注解

为了可以灵活的输出日志,我们配置的日志注解来帮助我们。

import java.lang.annotation.*;

/**
 * @author 墨龙吟
 * @version 1.0.0
 * @ClassName SysLog.java
 * @Email [email protected]
 * @Description TODO  系统日志注解
 * @createTime 2019年08月31日 - 10:51
 */
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {

    /**
     * 方法描述
     * @return
     */
    String description() default "";

    /**
     * 是否打印入参,默认打印
     */
    boolean inputParam() default true;

    /**
     * 是否打印入参,默认打印
     * @return
     */
    boolean outputParam() default true;

    /**
     * 错误提示
     * @return
     */
    String error() default "";
}

三、日志配置文件

这项目的resources目录下,增加一个log4j2.xml文件,项目会自动识别为log4j2的配置文件的。

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
    <Properties>
        <Property name="LOG_HOME">${sys:user.dir}/logs</Property>
        <Property name="LOG_EXCEPTION_CONVERSION_WORD">%xEx</Property>
        <Property name="LOG_LEVEL_PATTERN">%5p</Property>
        <Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
        <Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
        <Property name="FILE_LOG_PATTERN">%d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} %-5pid --- [%t] %-40.40c{1.} L%-4line : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
    </Properties>
    <appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <!--<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>-->
            <PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}"/>
        </Console>
        <!--处理DEBUG级别的日志,并把该日志放到logs/debug.log文件中-->
        <!--打印出DEBUG级别日志,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileDebug" fileName="${LOG_HOME}/debug.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/debug-%d{yyyy-MM-dd}-%i.log.gz">
            <Filters>
                <ThresholdFilter level="debug"/>
                <ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="50 MB"/>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
        <!--处理INFO级别的日志,并把该日志放到logs/info.log文件中-->
        <RollingFile name="RollingFileInfo" fileName="${LOG_HOME}/info.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log.gz">
            <Filters>
                <!--只接受INFO级别的日志,其余的全部拒绝处理-->
                <ThresholdFilter level="info"/>
                <ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="10 MB"/>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
        <!--处理WARN级别的日志,并把该日志放到logs/warn.log文件中-->
        <RollingFile name="RollingFileWarn" fileName="${LOG_HOME}/warn.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log.gz">
            <Filters>
                <ThresholdFilter level="warn"/>
                <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="10 MB"/>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
        <!--处理error级别的日志,并把该日志放到logs/error.log文件中-->
        <RollingFile name="RollingFileError" fileName="${LOG_HOME}/error.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log.gz">
            <ThresholdFilter level="error"/>
            <PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="10 MB"/>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
        <RollingRandomAccessFile name="ThreadFile" fileName="${LOG_HOME}/thread.log" filePattern="${LOG_HOME}/$${date:yyyy-MM}/fatal-%d{yyyy-MM-dd}-%i.log">
            <Filters>
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level} [%t] %highlight{%c{1.}.%M(%L)} - %msg%n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="1024 MB" />
            </Policies>
            <DefaultRolloverStrategy max="20" />
        </RollingRandomAccessFile>
        <RollingRandomAccessFile name="TimeGaps" fileName="${LOG_HOME}/timeGaps.log" filePattern="${LOG_HOME}/$${date:yyyy-MM}/fatal-%d{yyyy-MM-dd}-%i.log">
            <Filters>
                <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level} [%t] %highlight{%c{1.}.%M(%L)} - %msg%n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="1024 MB" />
            </Policies>
            <DefaultRolloverStrategy max="20" />
        </RollingRandomAccessFile>
        <!--druid的日志记录追加器-->
        <RollingFile name="druidSqlRollingFile" fileName="${LOG_HOME}/druid-sql.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/druid-sql-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="50 MB"/>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
        <!--慢SQL的日志记录追加器-->
        <!-- dataSource 配置
        <property name="filters" value="stat"/>
        <property name="connectionProperties" value="druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000"/>
        -->
        <RollingFile name="slowSqlRollingFile" fileName="${LOG_HOME}/slow-sql.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/slow-sql-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="50 MB"/>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
    </appenders>
    <loggers>
        <root level="info">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileDebug"/>
            <appender-ref ref="RollingFileInfo"/>
            <appender-ref ref="RollingFileWarn"/>
            <appender-ref ref="RollingFileError"/>
        </root>
        <!-- 线程池监控日志单独打印到一个文件中 -->
        <Logger name="threadMonitorLog" level="info" additivity="false">
            <AppenderRef ref="ThreadFile" />
        </Logger>
        <Logger name="timeGapsLog" level="debug" additivity="false">
            <AppenderRef ref="TimeGaps" />
            <AppenderRef ref="Console" />
        </Logger>
        <!--记录druid-sql的记录-->
        <logger name="druid.sql" level="debug" additivity="false">
            <appender-ref ref="druidSqlRollingFile"/>
            <appender-ref ref="Console"/>
        </logger>
        <!--记录慢sql的记录-->
        <logger name="com.alibaba.druid.filter.stat" level="warn" additivity="false">
            <appender-ref ref="slowSqlRollingFile"/>
            <appender-ref ref="Console"/>
        </logger>
        <!-- 自定义日志输出控制:开始 -->
        <logger name="com.github.busi" level="info"/>
        <logger name="org.springframework" level="info"/>
        <logger name="org.springframework.context.annotation" level="warn"/>
        <logger name="org.springframework.beans.factory.support" level="warn"/>
        <logger name="org.springframework.core.env.PropertySourcesPropertyResolver" level="warn"/>
        <logger name="org.springframework.beans.factory.annotation" level="warn"/>
        <logger name="org.springframework.core.LocalVariableTableParameterNameDiscoverer" level="warn"/>
        <logger name="org.springframework.core.annotation.AnnotationUtils" level="warn"/>
        <logger name="org.springframework.web.servlet.handler" level="info"/>
        <logger name="org.springframework.aop.framework" level="info"/>
        <logger name="org.hibernate.validator.internal" level="info"/>
        <logger name="org.springframework.jmx.export" level="info"/>
        <logger name="org.springframework.core.env" level="info"/>
        <logger name="springfox.documentation" level="warn"/>
        <logger name="org.apache.ibatis.logging.jdbc" level="off"/>
        <!-- 如果需要输出mybatis日志,开启以下配置(主要是dao包) -->
        <!--
        <logger name="org.apache.ibatis.logging.jdbc" level="debug"/>
        <logger name="com.github.busi.mapper" level="debug"/>
        -->
        <!--log4j2 自带过滤日志-->
        <Logger name="org.apache.catalina.startup.DigesterFactory" level="error"/>
        <Logger name="org.apache.catalina.util.LifecycleBase" level="error"/>
        <Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn"/>
        <logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
        <Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn"/>
        <Logger name="org.crsh.plugin" level="warn"/>
        <logger name="org.crsh.ssh" level="warn"/>
        <Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error"/>
        <Logger name="org.hibernate.validator.internal.util.Version" level="warn"/>
        <logger name="org.thymeleaf" level="warn"/>
        <logger name="org.springframework.core.env" level="warn"/>
        <logger name="org.springframework.core.io.support" level="warn"/>
        <logger name="org.springframework.web.filter" level="warn"/>
    </loggers>
</configuration>

三、 使用AspectJ对方法进行切面处理

import com.alibaba.fastjson.JSON;
import com.cloud.app.config.annotation.SystemLog;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import org.slf4j.Logger;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author 墨龙吟
 * @version 1.0.0
 * @ClassName SystemLogConfig.java
 * @Email [email protected]
 * @Description TODO   日志处理AOP配置
 * @createTime 2019年09月24日 - 09:25
 */
@Aspect
@Component
public class SystemLogConfig {

    /** 将日志输出到日志记录器 */
    private static final Logger logger = LoggerFactory.getLogger("timeGapsLog");

    private final static String ERROR_INFO = " [ 异常信息: {} ] ";

    private final static String UNKNOWN = "unknown";

    private final static String DEFAULT_ADDR = "0:0:0:0:0:0:0:1";

    private final static String DEFAULT_IP = "127.0.0.1";

    /** 配置时间 */
    private ThreadLocal<Long> timeThreadLocal;

    /** 是否进行日志打印 */
    private boolean debug;

    /**
     * 获取IP真实地址
     * @param request
     * @return
     */
    private String getAddrIp(HttpServletRequest request) {
        if (request != null) {
            // 如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy - Client - IP");
            }
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
            if (StringUtils.isNoneBlank(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {
                ip = ip.split(",")[0].trim();
            }
            if (DEFAULT_ADDR.equalsIgnoreCase(ip)) {
                ip = DEFAULT_IP;
            }
            return ip;
        }
        return "获取IP失败";
    }

    /**
     * 获取request请求的信息
     * @param joinPoint
     */
    private void getRequestObj(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        HttpServletRequest request = null;
        for (Object arg : args) {
            if (arg instanceof HttpServletRequest) {
                request = (HttpServletRequest) arg;
                String url = request.getRequestURL().toString();
                String ip = getAddrIp(request);
                logger.info(" [ ip 地址: {} ]", ip);
                logger.info(" [ 请求地址: {} ]", url);
                break;
            }
        }
    }

    /**
     * 获取文件描述
     * @param joinPoint
     * @return
     */
    private void getMethodDescription(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String description = method.getAnnotation(SystemLog.class).description();
        if (StringUtils.isNoneEmpty(description)) {
            logger.info(" [ 方法描述: {} ]", description);
        } else {
            logger.info(" [ 方法描述: {} ]", "无描述");
        }
    }

    /**
     * 获取方法请求参数
     * @param joinPoint
     */
    private void getMethodParam(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        boolean inputParam = method.getAnnotation(SystemLog.class).inputParam();
        if (inputParam) {
            Object[] args = joinPoint.getArgs();
            if (args != null && args.length > 0) {
                for (Object arg : args) {
                    if (arg instanceof HttpServletRequest) {
                        HttpServletRequest request = (HttpServletRequest) arg;
                        String contentType = request.getContentType();
                        if (contentType != null && contentType.contains("application/json")) {
                            continue;
                        } else {
                            Map<String, String[]> parameterMap = request.getParameterMap();
                            logger.info(" [ 请求参数: {} ]", parameterMap);
                            break;
                        }
                    }
                }
            }
        }
    }

    /**
     * 在切点方法之前执行
     * 前置通知 用于拦截Controller层记录用户的操作
     * @param joinPoint  切点
     */
    @Before(value = "@annotation(com.cloud.app.config.annotation.SystemLog)")
    public void deBefore(JoinPoint joinPoint) {
        timeThreadLocal = new ThreadLocal<>();
        timeThreadLocal.set(System.currentTimeMillis());
        try {
            logger.info(" [ 执行方法: {}.{}() ]", joinPoint.getTarget().getClass().getName(), joinPoint.getSignature().getName());
            // 获取请求信息
            getRequestObj(joinPoint);
            // 获取方法描述
            getMethodDescription(joinPoint);
        } catch (Exception e) {
            logger.error(" [ @Before : 发生了异常 ] ");
            logger.error(ERROR_INFO, e.getMessage());
        }
    }

    /**
     *   环绕通知:
     *   注意:Spring AOP的环绕通知会影响到AfterThrowing通知的运行,不要同时使用
     *
     *   环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
     *   环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
     */
    @Around(value = "@annotation(com.cloud.app.config.annotation.SystemLog)")
    public Object  around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 获取方法参数
        getMethodParam(proceedingJoinPoint);
        return proceedingJoinPoint.proceed();
    }

    /**
     * 切点方法返回后执行(后置增强), 方法正常退出时执行, 输出执行的输出结果
     * @param joinPoint
     * @param target
     */
    @AfterReturning(value = "@annotation(com.cloud.app.config.annotation.SystemLog)", returning = "target")
    public void doAfter(JoinPoint joinPoint,Object target) {
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            boolean outputParam = method.getAnnotation(SystemLog.class).outputParam();
            if (outputParam) {
                logger.info(" [ 返回结果: {} ]", JSON.toJSONString(target));
            }
        } catch (Exception e) {
            logger.error(" [ @AfterReturning : 发生了异常 ] ");
            logger.error(ERROR_INFO, e.getMessage());
        }
    }

    /**
     * 切点方法抛异常执行 (异常抛出增强), 对于方法发生异常之后, 执行异常输出
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(pointcut = "@annotation(com.cloud.app.config.annotation.SystemLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        try {
            logger.info("@AfterThrowing获得的信息:");
            logger.error(ERROR_INFO, ExceptionUtils.getStackTrace(e));
        } catch (Exception ex) {
            //记录本地异常日志
            logger.error(" [ @AfterThrowing: 发生了异常 ] ");
            logger.error(ERROR_INFO ,ex.getMessage());
        }
    }

    /**
     * 在切点方法之后执行(不管是抛出异常或者正常退出都会执行), 打印执行方法的时间
     * @param joinPoint
     */
    @After(value = "@annotation(com.cloud.app.config.annotation.SystemLog)")
    public void after(JoinPoint joinPoint) {
        try {
            logger.info(" [ 共耗时长: {} ms ]", System.currentTimeMillis() - timeThreadLocal.get());
        } finally {
            timeThreadLocal.remove();
        }
    }

}

四、对于AspectJ切面编程过程分析

1. 常用注解
@Before           在切点方法之前执行
@After            在切点方法之后执行
@AfterReturning   切点方法返回后执行
@AfterThrowing    切点方法抛异常执行
@Around           属于环绕增强,能控制切点执行前,执行后,用这个注解后,程序抛异常,会影响@AfterThrowing这个注解
2. 正常执行流程

3. 异常执行流程

4. 多个Aspect执行的流程

多个Aspect拦截一个方法,哪个Aspect先执行是随机的,如果需要定义顺序,可以使用@Order注解修饰Aspect类。值越小,优先级越高。

五、SpringAOPAspectJ的区别

1. AOP介绍

AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。AOP实现的关键就在于AOP框架自动创建的AOP代理,AOP代理则可分为静态代理和动态代理两大类,其中静态代理是指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于JDK动态代理CGLIB等在内存中“临时”生成AOP动态代理类,因此也被称为运行时增强。

面向切面的编程(AOP) 是一种编程范式,旨在通过允许横切关注点的分离,提高模块化。AOP提供切面来将跨越对象关注点模块化。虽然现在可以获得许多AOP框架,但在这里我们要区分的只有两个流行的框架:Spring AOPAspectJ

2. Aspectj介绍

AspectJ是一个面向切面的框架,他定义了AOP的一些语法,有一个专门的字节码生成器来生成遵守java规范的 class文件。

AspectJ的通知类型不仅包括我们之前了解过的三种通知:前置通知、后置通知、环绕通知,在Aspect中还有异常通知以及一种最终通知即无论程序是否正常执行,最终通知的代码会得到执行。

AspectJ提供了一套自己的表达式语言即切点表达式,切入点表达式可以标识切面织入到哪些类的哪些方法当中。只要把切面的实现配置好,再把这个切入点表达式写好就可以了,不需要一些额外的xml配置。

3. SpringAOP介绍

Spring AOP也是对目标类增强,生成代理类。但是与AspectJ的最大区别在于——Spring AOP的运行时增强,而AspectJ是编译时增强。

六、 效果展示

1. 使用
/**
 * @author 墨龙吟
 * @version 1.0.0
 * @ClassName HomeController.java
 * @Email [email protected]
 * @Description TODO
 * @createTime 2019年08月31日 - 10:49
 */
@RestController
@RequestMapping("/api")
public class HomeController {

    @Autowired
    private UserService userService;

    @GetMapping("/all")
    @SystemLog(description = "获取所有数据")
    public ResultUtil all(HttpServletRequest request) {
        List<UserEntity> list = userService.all();
        return ResultUtil.ok.setMsg("成功").setData(list).setCode(HttpStatus.OK.value());
    }
}
2. 结果:

六、关注下公众号,感谢

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章