上一章節,介紹了目前開發中常見的
log4j2
及logback
日誌框架的整合知識。在不少時候,咱們在開發一個系統時,無論出於何種考慮,好比是審計要求,或者防抵賴,仍是保留操做痕跡的角度,通常都會有個全局記錄日誌的模塊功能。此模塊通常上會記錄每一個對數據有進行變動的操做記錄,如果在web應用上,還會記錄請求的url,請求的IP,及當前的操做人,操做的方法說明等等。在不少時候,咱們須要記錄請求的參數信息時,一般是利用攔截器
、過濾器
或者AOP
等來進行統一攔截。本章節,就主要來講一說如何利用AOP
實現統一的web
日誌記錄。html
AOP
全稱:Aspect Oriented Programming。是一種面向切面編程的,利用預編譯方式和運行期動態代理實現程序功能統一的一種技術。它也是Spring
很重要的一部分,和IOC
同樣重要。利用AOP
能夠很好的對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度下降,提升程序的可重用性,同時提升了開發的效率。java
簡單來講,就是AOP
能夠在既有的程序基礎上,在無代碼嵌入前提下完成對相關業務的處理,業務方能夠只關注自身業務的邏輯,而無需關係一些和業務無關的事項,好比最多見的日誌
、事務
、權限檢驗
、性能統計
、統一異常處理
等等。git
spring
官網給出的AOP
介紹以下:github
關於AOP
的相關介紹可點擊官網連接查看:aop-introductionweb
如下簡單的說明下:spring
切面(Aspect):切面是一個關注點的模塊化,這個關注點多是橫切多個對象;編程
鏈接點(Join Point):鏈接點是指在程序執行過程當中某個特定的點,好比某方法調用的時候或者處理異常的時候;api
通知(Advice):指在切面的某個特定的鏈接點上執行的動做。Spring切面能夠應用5中通知:springboot
切點(Pointcut):指匹配鏈接點的斷言。通知與一個切入點表達式關聯,並在知足這個切入的鏈接點上運行,例如:當執行某個特定的名稱的方法。app
引入(Introduction):引入也被稱爲內部類型聲明,聲明額外的方法或者某個類型的字段。
目標對象(Target Object):目標對象是被一個或者多個切面所通知的對象。
AOP代理(AOP Proxy):AOP代理是指AOP框架建立的對對象,用來實現切面契約(包括通知方法等功能)
織入(Wearving):指把切面鏈接到其餘應用出程序類型或者對象上,並建立一個被通知的對象。或者說造成代理對象的方法的過程。
如下這張圖,對以上部分概念進行簡單介紹:
Spirng
的AOP
的動態代理實現機制有兩種,分別是:JDK動態代理
和CGLib動態代理
。簡單介紹下兩種代理機制。
JDK動態代理
是面向接口
的代理模式
,若是被代理目標沒有接口那麼Spring也無能爲力,Spring經過java的反射機制生產被代理接口的新的匿名實現類,重寫了其中AOP的加強方法。
CGLib
是一個強大、高性能的Code生產類庫,能夠實現運行期動態擴展java類,Spring在運行期間經過 CGlib繼承要被動態代理的類,重寫父類的方法,實現AOP面向切面編程。
二者對比:
JDK動態代理
是面向接口,在建立代理實現類時比CGLib要快,建立代理速度快。並且JDK動態代理
只能對實現了接口
的類生成代理,而不能針對類。
CGLib動態代理
是經過字節碼
底層繼承要代理類來實現(若是被代理類被final關鍵字所修飾,那麼抱歉會失敗),在建立代理這一塊沒有JDK動態代理快,可是運行速度比JDK動態代理要快。
至於相關原理,你們自行搜索下吧,⊙﹏⊙‖∣
爲了可以靈活定義切入點位置,Spring AOP提供了多種切入點指示符。如下簡單的介紹下。
能夠從上圖中,看見切入點指示符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
依賴。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
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();//一、開始時間 //利用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
本文主要是簡單介紹了利用
AOP
實現統一的web
日誌記錄功能。本示例未演示日誌入庫功能,你們可自行實現。在實際開發過程當中,通常上都是將日誌保存進行異步化後進行入庫處理的,這點須要注意,日誌記錄不能影響正常的方法請求,如果同步的,會本末倒置的。本文只是簡單的使用環繞機制進行講解,你們還能夠試試其餘的註解進行相應實踐下,大都大同小異,只是要注意下各註解的觸發時機。
目前互聯網上不少大佬都有
SpringBoot
系列教程,若有雷同,請多多包涵了。本文是做者在電腦前一字一句敲的,每一步都是本身實踐和理解的。若文中有所錯誤之處,還望提出,謝謝。
499452441
lqdevOps
我的博客:http://blog.lqdev.cn
完整示例:https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-24
原文地址:http://blog.lqdev.cn/2018/08/24/springboot/chapter-twenty-four/