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. 結果:

六、關注下公衆號,感謝

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