SpringBoot | 第二十四章:日誌管理之AOP統一日誌
一點知識上一章節,介紹了目前開發中常見的
log4j2
及logback
日誌框架的整合知識。在很多時候,我們在開發一個系統時,不管出於何種考慮,比如是審計要求,或者防抵賴,還是保留操作痕跡的角度,一般都會有個全域性記錄日誌的模組功能。此模組一般上會記錄每個對資料有進行變更的操作記錄,若是在web應用上,還會記錄請求的url,請求的IP,及當前的操作人,操作的方法說明等等。在很多時候,我們需要記錄請求的引數資訊時,通常是利用攔截器
、過濾器
或者AOP
等來進行統一攔截。本章節,就主要來說一說如何利用AOP
實現統一的web
日誌記錄。
何為AOP
AOP
全稱:Aspect Oriented Programming。是一種面向切面程式設計的,利用預編譯方式和執行期動態代理實現程式功能統一的一種技術。它也是Spring
很重要的一部分,和IOC
一樣重要。利用AOP
可以很好的對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。
簡單來說,就是AOP
可以在既有的程式基礎上,在無程式碼嵌入前提下完成對相關業務的處理,業務方可以只關注自身業務的邏輯,而無需關係一些和業務無關的事項,比如最常見的日誌
、事務
、許可權檢驗
、效能統計
、統一異常處理
等等。
spring
官網給出的AOP
介紹如下:
AOP基本概念
關於AOP
的相關介紹可點選官網連結檢視:
以下簡單的說明下:
-
切面(Aspect):切面是一個關注點的模組化,這個關注點可能是橫切多個物件;
-
連線點(Join Point):連線點是指在程式執行過程中某個特定的點,比如某方法呼叫的時候或者處理異常的時候;
- 通知(Advice):指在切面的某個特定的連線點上執行的動作。Spring切面可以應用5中通知:
- 前置通知(Before):在目標方法或者說連線點被呼叫前執行的通知;
- 後置通知(After):指在某個連線點完成後執行的通知;
- 返回通知(After-returning):指在某個連線點成功執行之後執行的通知;
- 異常通知(After-throwing):指在方法丟擲異常後執行的通知;
- 環繞通知(Around):指包圍一個連線點通知,在被通知的方法呼叫之前和之後執行自定義的方法。
-
切點(Pointcut):指匹配連線點的斷言。通知與一個切入點表示式關聯,並在滿足這個切入的連線點上執行,例如:當執行某個特定的名稱的方法。
-
引入(Introduction):引入也被稱為內部型別宣告,宣告額外的方法或者某個型別的欄位。
-
目標物件(Target Object):目標物件是被一個或者多個切面所通知的物件。
-
AOP代理(AOP Proxy):AOP代理是指AOP框架建立的對物件,用來實現切面契約(包括通知方法等功能)
- 織入(Wearving):指把切面連線到其他應用出程式型別或者物件上,並建立一個被通知的物件。或者說形成代理物件的方法的過程。
以下這張圖,對以上部分概念進行簡單介紹:
代理機制
Spirng
的AOP
的動態代理實現機制有兩種,分別是:JDK動態代理
和CGLib動態代理
。簡單介紹下兩種代理機制。
- JDK動態代理
JDK動態代理
是面向介面
的代理模式
,如果被代理目標沒有介面那麼Spring也無能為力,Spring透過java的反射機制生產被代理介面的新的匿名實現類,重寫了其中AOP的增強方法。
- CGLib動態代理
CGLib
是一個強大、高效能的Code生產類庫,可以實現執行期動態擴充套件java類,Spring在執行期間透過 CGlib繼承要被動態代理的類,重寫父類的方法,實現AOP面向切面程式設計。
兩者對比:
-
JDK動態代理
是面向介面,在建立代理實現類時比CGLib要快,建立代理速度快。而且JDK動態代理
只能對實現了介面
的類生成代理,而不能針對類。 -
CGLib動態代理
是透過位元組碼
底層繼承要代理類來實現(如果被代理類被final關鍵字所修飾,那麼抱歉會失敗),在建立代理這一塊沒有JDK動態代理快,但是執行速度比JDK動態代理要快。
至於相關原理,大家自行搜尋下吧,⊙﹏⊙‖∣
切入點指示符簡單介紹
為了能夠靈活定義切入點位置,Spring AOP提供了多種切入點指示符。以下簡單的介紹下。
- execution:匹配執行方法的連線點
可以從上圖中,看見切入點指示符execution
的語法結構為:execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
。這也是最常使用的一個指示符了。
-
within:用於匹配指定型別內的方法執行;
-
this:用於匹配當前AOP代理物件型別的執行方法;注意是AOP代理物件的型別匹配,這樣就可能包括引入介面也型別匹配;
-
target:用於匹配當前目標物件型別的執行方法;注意是目標物件的型別匹配,這樣就不包括引入介面也型別匹配;
-
args:用於匹配當前執行的方法傳入的引數為指定型別的執行方法;
-
@within:用於匹配所以持有指定註解型別內的方法;
-
@target:用於匹配當前目標物件型別的執行方法,其中目標物件持有指定的註解;
-
@args:用於匹配當前執行的方法傳入的引數持有指定註解的執行;
-
@annotation:用於匹配當前執行方法持有指定註解的方法;
-
bean:Spring AOP擴充套件的,AspectJ沒有對於指示符,用於匹配特定名稱的Bean物件的執行方法;
- reference pointcut:表示引用其他命名切入點,只有@ApectJ風格支援,Schema風格不支援。
對於相關的語法和使用,大家可檢視:https://blog.csdn.net/zhengchao1991/article/details/53391244。裡面有較為詳細的介紹。這裡就不多加闡述了。
統一日誌記錄介紹完相關知識後,我們開始來使用
AOP
實現統一的日誌記錄功能。本文直接利用@Around
環繞模式來實現,同時自定義一個日誌註解類,來個性化記錄日誌資訊。
0.加入Aop
依賴。
org.springframework.boot spring-boot-starter-aop
1.編寫自定義日誌註解類Log
。
/**
* 日誌註解類
* @author oKong
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})//只能在方法上使用此註解
public @interface Log {
/**
* 日誌描述,這裡使用了@AliasFor 別名。spring提供的
* @return
*/
@AliasFor("desc")
String value() default "";
/**
* 日誌描述
* @return
*/
@AliasFor("value")
String desc() default "";
/**
* 是否不記錄日誌
* @return
*/
boolean ignore() default false;
}
友情提示:熟悉Spring
常用註解類的朋友,對@AliasFor
應該不陌生。它是Spring
提供的一個註解,主要是給註解的屬性起名別的。讓使用註解時,更加的容易理解(比如給value屬性起別名)。一般上是配對別名。由於是Spring
框架提供的,所以要使其生效,可以使用AnnotationUtils.synthesizeAnnotation
或者AnnotationUtils.getAnnotation
方法呼叫獲取註解,以下程式碼中會有個簡單示例。
2.編寫切面類。
/**
* 日誌切面類
* @author xiedeshou
*
*/
//加入@Aspect 申明一個切面
@Aspect
@Component
@Slf4j
public class LogAspect {
//設定切入點:這裡直接攔截被@RestController註解的類
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
public void pointcut() {
}
/**
* 切面方法,記錄日誌
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long beginTime = System.currentTimeMillis();//1、開始時間
//利用RequestContextHolder獲取requst物件
ServletRequestAttributes requestAttr = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes();
String uri = requestAttr.getRequest().getRequestURI();
log.info("開始計時: {} URI: {}", new Date(),uri);
//訪問目標方法的引數 可動態改變引數值
Object[] args = joinPoint.getArgs();
//方法名獲取
String methodName = joinPoint.getSignature().getName();
log.info("請求方法:{}, 請求引數: {}", methodName, Arrays.toString(args));
//可能在反向代理請求進來時,獲取的IP存在不正確行 這裡直接摘抄一段來自網上獲取ip的程式碼
log.info("請求ip:{}", getIpAddr(requestAttr.getRequest()));
Signature signature = joinPoint.getSignature();
if(!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("暫不支援非方法註解");
}
//呼叫實際方法
Object object = joinPoint.proceed();
//獲取執行的方法
MethodSignature methodSign = (MethodSignature) signature;
Method method = methodSign.getMethod();
//判斷是否包含了 無需記錄日誌的方法
Log logAnno = AnnotationUtils.getAnnotation(method, Log.class);
if(logAnno != null && logAnno.ignore()) {
return object;
}
log.info("log註解描述:{}", logAnno.desc());
long endTime = System.currentTimeMillis();
log.info("結束計時: {}, URI: {},耗時:{}", new Date(),uri,endTime - beginTime);
//模擬異常
//System.out.println(1/0);
return object;
}
/**
* 指定攔截器規則;也可直接使用within(@org.springframework.web.bind.annotation.RestController *)
* 這樣簡單點 可以通用
* @param 異常物件
*/
@AfterThrowing(pointcut="pointcut()",throwing="e")
public void afterThrowable(Throwable e) {
log.error("切面發生了異常:", e);
//這裡可以做個統一異常處理
//自定義一個異常 包裝後排除
//throw new AopException("xxx);
}
/**
* 轉至:https://my.oschina.net/u/994081/blog/185982
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根據網路卡取本機配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.error("獲取ip異常:{}" ,e.getMessage());
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 對於透過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
// ipAddress = this.getRequest().getRemoteAddr();
return ipAddress;
}
}
3.啟動類加入註解@EnableAspectJAutoProxy
,生效註解。另一說法,預設引入pom依賴就是預設開啟的。無所謂,加了就是了,加上總之是個好習慣,因為不知道後續版本是否會修改預設值呢~
@SpringBootApplication
@EnableAspectJAutoProxy
@Slf4j
public class Chapter24Application {
public static void main(String[] args) {
SpringApplication.run(Chapter24Application.class, args);
log.info("Chapter24啟動!");
}
}
4.編寫控制層。
/**
* aop統一異常示例
* @author xiedeshou
*
*/
@RestController
public class DemoController {
/**
* 簡單方法示例
* @param hello
* @return
*/
@RequestMapping("/aop")
@Log(value="請求了aopDemo方法")
public String aopDemo(String hello) {
return "請求引數為:" + hello;
}
/**
* 不攔截日誌示例
* @param hello
* @return
*/
@RequestMapping("/notaop")
@Log(ignore=true)
public String notAopDemo(String hello) {
return "此方法不記錄日誌,請求引數為:" + hello;
}
}
友情提示:在編寫了切面類後,若符合切面攔截條件的方法,IDE會進行標識的。
5.啟動應用,訪問api,即可看見控制檯輸出了對應資訊了。
訪問了:/aop,輸出
2018-08-23 22:54:59.003 INFO 12928 --- [nio-8080-exec-3] c.l.l.s.chapter24.config.LogAspect : 開始計時: Fri Aug 23 22:54:59 CST 2018 URI: /aop
2018-08-23 22:54:59.004 INFO 12928 --- [nio-8080-exec-3] c.l.l.s.chapter24.config.LogAspect : 請求方法:aopDemo, 請求引數: [oKong]
2018-08-23 22:54:59.005 INFO 12928 --- [nio-8080-exec-3] c.l.l.s.chapter24.config.LogAspect : 請求ip:192.168.2.107
2018-08-23 22:54:59.005 INFO 12928 --- [nio-8080-exec-3] c.l.l.s.chapter24.config.LogAspect : log註解描述:請求了aopDemo方法
2018-08-23 22:54:59.005 INFO 12928 --- [nio-8080-exec-3] c.l.l.s.chapter24.config.LogAspect : 結束計時: Fri Aug 23 22:54:59 CST 2018, URI: /aop,耗時:2
參考資料 - https://blog.csdn.net/zhengchao1991/article/details/53391244
- https://blog.csdn.net/wqh8522/article/details/72887209
最後本文主要是簡單介紹了利用
AOP
實現統一的web
日誌記錄功能。本示例未演示日誌入庫功能,大家可自行實現。在實際開發過程中,一般上都是將日誌儲存進行非同步化後進行入庫處理的,這點需要注意,日誌記錄不能影響正常的方法請求,若是同步的,會本末倒置的。本文只是簡單的使用環繞機制進行講解,大家還可以試試其他的註解進行相應實踐下,大都大同小異,只是要注意下各註解的觸發時機。
老生常談目前網際網路上很多大佬都有
SpringBoot
系列教程,如有雷同,請多多包涵了。本文是作者在電腦前一字一句敲的,每一步都是自己實踐和理解的。若文中有所錯誤之處,還望提出,謝謝。
- 個人QQ:
499452441
- 微信公眾號:
lqdevOps
個人部落格:http://blog.lqdev.cn
完整示例:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2730/viewspace-2812475/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 統一日誌管理
- Spring AOP實現統一日誌輸出Spring
- Java專案如何統一日誌管理Java
- SpringBoot日誌管理Spring Boot
- 【Java分享客棧】超簡潔SpringBoot使用AOP統一日誌管理-純乾貨幹到便祕JavaSpring Boot
- SpringBoot部落格開發之AOP日誌處理Spring Boot
- Springboot AOP 自定義註解實現系統日誌Spring Boot
- SpringBoot自定義註解、AOP列印日誌Spring Boot
- Golang一日一庫之 日誌庫 zapGolang
- Core + Vue 後臺管理基礎框架9——統一日誌Vue框架
- Spring Boot AOP 掃盲,實現介面訪問的統一日誌記錄Spring Boot
- ELK+kafka+Winlogbeat/FileBeat搭建統一日誌收集分析管理系統Kafka
- AOP行為日誌
- 在SpringBoot中使用Logback管理日誌Spring Boot
- SpringBoot 日誌框架Spring Boot框架
- java B2B2C Springboot電子商務平臺原始碼-統一日誌管理ELKJavaSpring Boot原始碼
- Spring AOP 日誌攔截器的事務管理Spring
- 日誌管理
- Springboot日誌相關Spring Boot
- SpringBoot日誌實現Spring Boot
- logstash收集springboot日誌Spring Boot
- 55.SpringBoot日誌Spring Boot
- Spring Boot 入門(五):整合 AOP 進行日誌管理Spring Boot
- springboot專案配置logback日誌系統Spring Boot
- ELK日誌系統之通用應用程式日誌接入方案
- 重做日誌管理
- linux日誌管理Linux
- Mysql 日誌管理MySql
- 許可權控制及AOP日誌
- SpringBoot使用ELK日誌收集Spring Boot
- SpringBoot專案整合日誌Spring Boot
- Springboot漫遊日誌(1)Spring Boot
- SpringBoot專案使用AOP及自定義註解儲存操作日誌Spring Boot
- ELK日誌系統之使用Rsyslog快速方便的收集Nginx日誌Nginx
- 【MySQL日誌】MySQL日誌檔案初級管理MySql
- SpringBoot指定日誌檔案和日誌Profile功能Spring Boot
- 日誌服務之告警接入與管理
- MySQL日誌管理,舊MySql