前言
上一章節(jié)晰韵,介紹了目前開(kāi)發(fā)中常見(jiàn)的
log4j2
及logback
日志框架的整合知識(shí)。在很多時(shí)候熟妓,我們?cè)陂_(kāi)發(fā)一個(gè)系統(tǒng)時(shí)雪猪,不管出于何種考慮,比如是審計(jì)要求起愈,或者防抵賴只恨,還是保留操作痕跡的角度,一般都會(huì)有個(gè)全局記錄日志的模塊功能抬虽。此模塊一般上會(huì)記錄每個(gè)對(duì)數(shù)據(jù)有進(jìn)行變更的操作記錄官觅,若是在web應(yīng)用上,還會(huì)記錄請(qǐng)求的url,請(qǐng)求的IP,及當(dāng)前的操作人阐污,操作的方法說(shuō)明等等休涤。在很多時(shí)候,我們需要記錄請(qǐng)求的參數(shù)信息時(shí)笛辟,通常是利用攔截器
功氨、過(guò)濾器
或者AOP
等來(lái)進(jìn)行統(tǒng)一攔截。本章節(jié)手幢,就主要來(lái)說(shuō)一說(shuō)如何利用AOP
實(shí)現(xiàn)統(tǒng)一的web
日志記錄捷凄。
一點(diǎn)知識(shí)
何為AOP
AOP
全稱:Aspect Oriented Programming。是一種面向切面編程的围来,利用預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能統(tǒng)一的一種技術(shù)跺涤。它也是Spring
很重要的一部分匈睁,和IOC
一樣重要。利用AOP
可以很好的對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離桶错,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低航唆,提高程序的可重用性,同時(shí)提高了開(kāi)發(fā)的效率牛曹。
簡(jiǎn)單來(lái)說(shuō)佛点,就是AOP
可以在既有的程序基礎(chǔ)上,在無(wú)代碼嵌入前提下完成對(duì)相關(guān)業(yè)務(wù)的處理黎比,業(yè)務(wù)方可以只關(guān)注自身業(yè)務(wù)的邏輯超营,而無(wú)需關(guān)系一些和業(yè)務(wù)無(wú)關(guān)的事項(xiàng),比如最常見(jiàn)的日志
阅虫、事務(wù)
演闭、權(quán)限檢驗(yàn)
、性能統(tǒng)計(jì)
颓帝、統(tǒng)一異常處理
等等米碰。
spring
官網(wǎng)給出的AOP
介紹如下:
AOP基本概念
關(guān)于AOP
的相關(guān)介紹可點(diǎn)擊官網(wǎng)鏈接查看:aop-introduction
以下簡(jiǎn)單的說(shuō)明下:
切面(Aspect):切面是一個(gè)關(guān)注點(diǎn)的模塊化,這個(gè)關(guān)注點(diǎn)可能是橫切多個(gè)對(duì)象购城;
連接點(diǎn)(Join Point):連接點(diǎn)是指在程序執(zhí)行過(guò)程中某個(gè)特定的點(diǎn)吕座,比如某方法調(diào)用的時(shí)候或者處理異常的時(shí)候;
-
通知(Advice):指在切面的某個(gè)特定的連接點(diǎn)上執(zhí)行的動(dòng)作瘪板。Spring切面可以應(yīng)用5中通知:
- 前置通知(Before):在目標(biāo)方法或者說(shuō)連接點(diǎn)被調(diào)用前執(zhí)行的通知吴趴;
- 后置通知(After):指在某個(gè)連接點(diǎn)完成后執(zhí)行的通知;
- 返回通知(After-returning):指在某個(gè)連接點(diǎn)成功執(zhí)行之后執(zhí)行的通知侮攀;
- 異常通知(After-throwing):指在方法拋出異常后執(zhí)行的通知锣枝;
- 環(huán)繞通知(Around):指包圍一個(gè)連接點(diǎn)通知,在被通知的方法調(diào)用之前和之后執(zhí)行自定義的方法兰英。
切點(diǎn)(Pointcut):指匹配連接點(diǎn)的斷言撇叁。通知與一個(gè)切入點(diǎn)表達(dá)式關(guān)聯(lián),并在滿足這個(gè)切入的連接點(diǎn)上運(yùn)行畦贸,例如:當(dāng)執(zhí)行某個(gè)特定的名稱的方法陨闹。
引入(Introduction):引入也被稱為內(nèi)部類型聲明,聲明額外的方法或者某個(gè)類型的字段薄坏。
目標(biāo)對(duì)象(Target Object):目標(biāo)對(duì)象是被一個(gè)或者多個(gè)切面所通知的對(duì)象正林。
AOP代理(AOP Proxy):AOP代理是指AOP框架創(chuàng)建的對(duì)對(duì)象,用來(lái)實(shí)現(xiàn)切面契約(包括通知方法等功能)
織入(Wearving):指把切面連接到其他應(yīng)用出程序類型或者對(duì)象上颤殴,并創(chuàng)建一個(gè)被通知的對(duì)象觅廓。或者說(shuō)形成代理對(duì)象的方法的過(guò)程涵但。
以下這張圖杈绸,對(duì)以上部分概念進(jìn)行簡(jiǎn)單介紹:
代理機(jī)制
Spirng
的AOP
的動(dòng)態(tài)代理實(shí)現(xiàn)機(jī)制有兩種帖蔓,分別是:JDK動(dòng)態(tài)代理
和CGLib動(dòng)態(tài)代理
。簡(jiǎn)單介紹下兩種代理機(jī)制瞳脓。
- JDK動(dòng)態(tài)代理
JDK動(dòng)態(tài)代理
是面向接口
的代理模式
塑娇,如果被代理目標(biāo)沒(méi)有接口那么Spring也無(wú)能為力,Spring通過(guò)java的反射機(jī)制生產(chǎn)被代理接口的新的匿名實(shí)現(xiàn)類劫侧,重寫了其中AOP的增強(qiáng)方法埋酬。
- CGLib動(dòng)態(tài)代理
CGLib
是一個(gè)強(qiáng)大、高性能的Code生產(chǎn)類庫(kù)烧栋,可以實(shí)現(xiàn)運(yùn)行期動(dòng)態(tài)擴(kuò)展java類写妥,Spring在運(yùn)行期間通過(guò) CGlib繼承要被動(dòng)態(tài)代理的類,重寫父類的方法审姓,實(shí)現(xiàn)AOP面向切面編程珍特。
兩者對(duì)比:
JDK動(dòng)態(tài)代理
是面向接口,在創(chuàng)建代理實(shí)現(xiàn)類時(shí)比CGLib要快魔吐,創(chuàng)建代理速度快扎筒。而且JDK動(dòng)態(tài)代理
只能對(duì)實(shí)現(xiàn)了接口
的類生成代理,而不能針對(duì)類酬姆。CGLib動(dòng)態(tài)代理
是通過(guò)字節(jié)碼
底層繼承要代理類來(lái)實(shí)現(xiàn)(如果被代理類被final關(guān)鍵字所修飾嗜桌,那么抱歉會(huì)失敗)辞色,在創(chuàng)建代理這一塊沒(méi)有JDK動(dòng)態(tài)代理快骨宠,但是運(yùn)行速度比JDK動(dòng)態(tài)代理要快。
至于相關(guān)原理淫僻,大家自行搜索下吧诱篷,⊙﹏⊙‖∣
切入點(diǎn)指示符簡(jiǎn)單介紹
為了能夠靈活定義切入點(diǎn)位置壶唤,Spring AOP提供了多種切入點(diǎn)指示符雳灵。以下簡(jiǎn)單的介紹下。
- execution:匹配執(zhí)行方法的連接點(diǎn)
可以從上圖中闸盔,看見(jiàn)切入點(diǎn)指示符execution
的語(yǔ)法結(jié)構(gòu)為:execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
悯辙。這也是最常使用的一個(gè)指示符了。
within:用于匹配指定類型內(nèi)的方法執(zhí)行迎吵;
this:用于匹配當(dāng)前AOP代理對(duì)象類型的執(zhí)行方法躲撰;注意是AOP代理對(duì)象的類型匹配睬塌,這樣就可能包括引入接口也類型匹配铣焊;
target:用于匹配當(dāng)前目標(biāo)對(duì)象類型的執(zhí)行方法;注意是目標(biāo)對(duì)象的類型匹配搔预,這樣就不包括引入接口也類型匹配蔫巩;
args:用于匹配當(dāng)前執(zhí)行的方法傳入的參數(shù)為指定類型的執(zhí)行方法谆棱;
@within:用于匹配所以持有指定注解類型內(nèi)的方法快压;
@target:用于匹配當(dāng)前目標(biāo)對(duì)象類型的執(zhí)行方法,其中目標(biāo)對(duì)象持有指定的注解垃瞧;
@args:用于匹配當(dāng)前執(zhí)行的方法傳入的參數(shù)持有指定注解的執(zhí)行蔫劣;
@annotation:用于匹配當(dāng)前執(zhí)行方法持有指定注解的方法;
bean:Spring AOP擴(kuò)展的个从,AspectJ沒(méi)有對(duì)于指示符脉幢,用于匹配特定名稱的Bean對(duì)象的執(zhí)行方法;
reference pointcut:表示引用其他命名切入點(diǎn)嗦锐,只有@ApectJ風(fēng)格支持嫌松,Schema風(fēng)格不支持。
對(duì)于相關(guān)的語(yǔ)法和使用意推,大家可查看:https://blog.csdn.net/zhengchao1991/article/details/53391244豆瘫。里面有較為詳細(xì)的介紹。這里就不多加闡述了菊值。
統(tǒng)一日志記錄
介紹完相關(guān)知識(shí)后外驱,我們開(kāi)始來(lái)使用
AOP
實(shí)現(xiàn)統(tǒng)一的日志記錄功能。本文直接利用@Around
環(huán)繞模式來(lái)實(shí)現(xiàn)腻窒,同時(shí)自定義一個(gè)日志注解類昵宇,來(lái)個(gè)性化記錄日志信息。
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
常用注解類的朋友蒋譬,對(duì)@AliasFor
應(yīng)該不陌生。它是Spring
提供的一個(gè)注解愉适,主要是給注解的屬性起名別的犯助。讓使用注解時(shí),更加的容易理解(比如給value屬性起別名)维咸。一般上是配對(duì)別名剂买。由于是Spring
框架提供的,所以要使其生效癌蓖,可以使用AnnotationUtils.synthesizeAnnotation
或者AnnotationUtils.getAnnotation
方法調(diào)用獲取注解瞬哼,以下代碼中會(huì)有個(gè)簡(jiǎn)單示例。
2.編寫切面類租副。
/**
* 日志切面類
* @author xiedeshou
*
*/
//加入@Aspect 申明一個(gè)切面
@Aspect
@Component
@Slf4j
public class LogAspect {
//設(shè)置切入點(diǎn):這里直接攔截被@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坐慰、開(kāi)始時(shí)間
//利用RequestContextHolder獲取requst對(duì)象
ServletRequestAttributes requestAttr = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes();
String uri = requestAttr.getRequest().getRequestURI();
log.info("開(kāi)始計(jì)時(shí): {} URI: {}", new Date(),uri);
//訪問(wèn)目標(biāo)方法的參數(shù) 可動(dòng)態(tài)改變參數(shù)值
Object[] args = joinPoint.getArgs();
//方法名獲取
String methodName = joinPoint.getSignature().getName();
log.info("請(qǐng)求方法:{}, 請(qǐng)求參數(shù): {}", methodName, Arrays.toString(args));
//可能在反向代理請(qǐng)求進(jìn)來(lái)時(shí),獲取的IP存在不正確行 這里直接摘抄一段來(lái)自網(wǎng)上獲取ip的代碼
log.info("請(qǐng)求ip:{}", getIpAddr(requestAttr.getRequest()));
Signature signature = joinPoint.getSignature();
if(!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("暫不支持非方法注解");
}
//調(diào)用實(shí)際方法
Object object = joinPoint.proceed();
//獲取執(zhí)行的方法
MethodSignature methodSign = (MethodSignature) signature;
Method method = methodSign.getMethod();
//判斷是否包含了 無(wú)需記錄日志的方法
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("結(jié)束計(jì)時(shí): {}, URI: {},耗時(shí):{}", new Date(),uri,endTime - beginTime);
//模擬異常
//System.out.println(1/0);
return object;
}
/**
* 指定攔截器規(guī)則用僧;也可直接使用within(@org.springframework.web.bind.annotation.RestController *)
* 這樣簡(jiǎn)單點(diǎn) 可以通用
* @param 異常對(duì)象
*/
@AfterThrowing(pointcut="pointcut()",throwing="e")
public void afterThrowable(Throwable e) {
log.error("切面發(fā)生了異常:", e);
//這里可以做個(gè)統(tǒng)一異常處理
//自定義一個(gè)異常 包裝后排除
//throw new AopException("xxx);
}
/**
* 轉(zhuǎn)至: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")) {
// 根據(jù)網(wǎng)卡取本機(jī)配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.error("獲取ip異常:{}" ,e.getMessage());
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 對(duì)于通過(guò)多個(gè)代理的情況结胀,第一個(gè)IP為客戶端真實(shí)IP,多個(gè)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.啟動(dòng)類加入注解@EnableAspectJAutoProxy
两残,生效注解。另一說(shuō)法把跨,默認(rèn)引入pom依賴就是默認(rèn)開(kāi)啟的人弓。無(wú)所謂,加了就是了着逐,加上總之是個(gè)好習(xí)慣崔赌,因?yàn)椴恢篮罄m(xù)版本是否會(huì)修改默認(rèn)值呢~
@SpringBootApplication
@EnableAspectJAutoProxy
@Slf4j
public class Chapter24Application {
public static void main(String[] args) {
SpringApplication.run(Chapter24Application.class, args);
log.info("Chapter24啟動(dòng)!");
}
}
4.編寫控制層。
/**
* aop統(tǒng)一異常示例
* @author xiedeshou
*
*/
@RestController
public class DemoController {
/**
* 簡(jiǎn)單方法示例
* @param hello
* @return
*/
@RequestMapping("/aop")
@Log(value="請(qǐng)求了aopDemo方法")
public String aopDemo(String hello) {
return "請(qǐng)求參數(shù)為:" + hello;
}
/**
* 不攔截日志示例
* @param hello
* @return
*/
@RequestMapping("/notaop")
@Log(ignore=true)
public String notAopDemo(String hello) {
return "此方法不記錄日志耸别,請(qǐng)求參數(shù)為:" + hello;
}
}
友情提示:在編寫了切面類后健芭,若符合切面攔截條件的方法,IDE會(huì)進(jìn)行標(biāo)識(shí)的秀姐。
5.啟動(dòng)應(yīng)用慈迈,訪問(wèn)api,即可看見(jiàn)控制臺(tái)輸出了對(duì)應(yīng)信息了。
訪問(wèn)了:/aop省有,輸出
2018-08-23 22:54:59.003 INFO 12928 --- [nio-8080-exec-3] c.l.l.s.chapter24.config.LogAspect : 開(kāi)始計(jì)時(shí): 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 : 請(qǐng)求方法:aopDemo, 請(qǐng)求參數(shù): [oKong]
2018-08-23 22:54:59.005 INFO 12928 --- [nio-8080-exec-3] c.l.l.s.chapter24.config.LogAspect : 請(qǐng)求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注解描述:請(qǐng)求了aopDemo方法
2018-08-23 22:54:59.005 INFO 12928 --- [nio-8080-exec-3] c.l.l.s.chapter24.config.LogAspect : 結(jié)束計(jì)時(shí): Fri Aug 23 22:54:59 CST 2018, URI: /aop,耗時(shí):2
參考資料
- https://blog.csdn.net/zhengchao1991/article/details/53391244
- https://blog.csdn.net/wqh8522/article/details/72887209
總結(jié)
本文主要是簡(jiǎn)單介紹了利用
AOP
實(shí)現(xiàn)統(tǒng)一的web
日志記錄功能痒留。本示例未演示日志入庫(kù)功能,大家可自行實(shí)現(xiàn)蠢沿。在實(shí)際開(kāi)發(fā)過(guò)程中伸头,一般上都是將日志保存進(jìn)行異步化后進(jìn)行入庫(kù)處理的,這點(diǎn)需要注意舷蟀,日志記錄不能影響正常的方法請(qǐng)求恤磷,若是同步的,會(huì)本末倒置的野宜。本文只是簡(jiǎn)單的使用環(huán)繞機(jī)制進(jìn)行講解扫步,大家還可以試試其他的注解進(jìn)行相應(yīng)實(shí)踐下,大都大同小異匈子,只是要注意下各注解的觸發(fā)時(shí)機(jī)河胎。
最后
目前互聯(lián)網(wǎng)上很多大佬都有
SpringBoot
系列教程,如有雷同旬牲,請(qǐng)多多包涵了仿粹。本文是作者在電腦前一字一句敲的搁吓,每一步都是自己實(shí)踐和理解的原茅。若文中有所錯(cuò)誤之處,還望提出堕仔,謝謝擂橘。
老生常談
- 個(gè)人QQ:
499452441
- 微信公眾號(hào):
lqdevOps
個(gè)人博客: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/