Springboot
之日誌處理
一、使用log4j2
進行日誌管理
1. log4j2
日誌管理框架的簡介
Log4j2
是log4j 1.x
和logback
的改進版,據說採用了一些新技術(無鎖異步、等等),使得日誌的吞吐量、性能比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類。值越小,優先級越高。
五、SpringAOP
和AspectJ
的區別
1. AOP
介紹
AOP(Aspect Orient Programming)
,作爲面向對象編程的一種補充,廣泛應用於處理一些具有橫切性質的系統級服務,如日誌收集、事務管理、安全檢查、緩存、對象池管理等。AOP
實現的關鍵就在於AOP框架自動創建的AOP
代理,AOP
代理則可分爲靜態代理和動態代理兩大類,其中靜態代理是指使用AOP
框架提供的命令進行編譯,從而在編譯階段就可生成 AOP
代理類,因此也稱爲編譯時增強;而動態代理則在運行時藉助於JDK動態代理
、CGLIB
等在內存中“臨時”生成AOP
動態代理類,因此也被稱爲運行時增強。
面向切面的編程(AOP
) 是一種編程範式,旨在通過允許橫切關注點的分離,提高模塊化。AOP
提供切面來將跨越對象關注點模塊化。雖然現在可以獲得許多AOP
框架,但在這裏我們要區分的只有兩個流行的框架:Spring AOP
和AspectJ
。
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());
}
}