AOP為Aspect Oriented Programming的縮寫(xiě),意為:面向切面編程,通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是Spring框架中的一個(gè)重要內(nèi)容,它通過(guò)對(duì)既有程序定義一個(gè)切入點(diǎn),然后在其前后切入不同的執(zhí)行內(nèi)容,比如常見(jiàn)的有:打開(kāi)數(shù)據(jù)庫(kù)連接/關(guān)閉數(shù)據(jù)庫(kù)連接、打開(kāi)事務(wù)/關(guān)閉事務(wù)、記錄日志等?;贏OP不會(huì)破壞原來(lái)程序邏輯,因此它可以很好的對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開(kāi)發(fā)的效率。
下面主要講兩個(gè)內(nèi)容:
- 一個(gè)是如何在Spring Boot中引入Aop功能
- 二是如何使用Aop做切面去統(tǒng)一處理Web請(qǐng)求的日志
以下所有操作基于chapter4-2-2工程open in new window進(jìn)行。
#準(zhǔn)備工作
因?yàn)樾枰獙?duì)web請(qǐng)求做切面來(lái)記錄日志,所以先引入web模塊,并創(chuàng)建一個(gè)簡(jiǎn)單的hello請(qǐng)求的處理。
pom.xml
中引入web模塊
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 實(shí)現(xiàn)一個(gè)簡(jiǎn)單請(qǐng)求處理:通過(guò)傳入name參數(shù),返回“hello xxx”的功能。
@RestController
public class HelloController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hello(@RequestParam String name) {
return "Hello " + name;
}
}
下面,我們可以對(duì)上面的/hello請(qǐng)求,進(jìn)行切面日志記錄。
#引入AOP依賴
在Spring Boot中引入AOP就跟引入其他模塊一樣,非常簡(jiǎn)單,只需要在pom.xml
中加入如下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在完成了引入AOP依賴包后,一般來(lái)說(shuō)并不需要去做其他配置。也許在Spring中使用過(guò)注解配置方式的人會(huì)問(wèn)是否需要在程序主類中增加@EnableAspectJAutoProxy
來(lái)啟用,實(shí)際并不需要。
可以看下面關(guān)于AOP的默認(rèn)配置屬性,其中spring.aop.auto
屬性默認(rèn)是開(kāi)啟的,也就是說(shuō)只要引入了AOP依賴后,默認(rèn)已經(jīng)增加了@EnableAspectJAutoProxy
。
# AOP
spring.aop.auto=true # Add @EnableAspectJAutoProxy.
spring.aop.proxy-target-class=false # Whether subclass-based (CGLIB) proxies are to be created (true) as
opposed to standard Java interface-based proxies (false).
而當(dāng)我們需要使用CGLIB來(lái)實(shí)現(xiàn)AOP的時(shí)候,需要配置spring.aop.proxy-target-class=true
,不然默認(rèn)使用的是標(biāo)準(zhǔn)Java的實(shí)現(xiàn)。
#實(shí)現(xiàn)Web層的日志切面
實(shí)現(xiàn)AOP的切面主要有以下幾個(gè)要素:
- 使用
@Aspect
注解將一個(gè)java類定義為切面類 - 使用
@Pointcut
定義一個(gè)切入點(diǎn),可以是一個(gè)規(guī)則表達(dá)式,比如下例中某個(gè)package下的所有函數(shù),也可以是一個(gè)注解等。 - 根據(jù)需要在切入點(diǎn)不同位置的切入內(nèi)容
- 使用
@Before
在切入點(diǎn)開(kāi)始處切入內(nèi)容 - 使用
@After
在切入點(diǎn)結(jié)尾處切入內(nèi)容 - 使用
@AfterReturning
在切入點(diǎn)return內(nèi)容之后切入內(nèi)容(可以用來(lái)對(duì)處理返回值做一些加工處理) - 使用
@Around
在切入點(diǎn)前后切入內(nèi)容,并自己控制何時(shí)執(zhí)行切入點(diǎn)自身的內(nèi)容 - 使用
@AfterThrowing
用來(lái)處理當(dāng)切入內(nèi)容部分拋出異常之后的處理邏輯
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
@Pointcut("execution(public * com.didispace.web..*.*(..))")
public void webLog(){}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到請(qǐng)求,記錄請(qǐng)求內(nèi)容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 記錄下請(qǐng)求內(nèi)容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 處理完請(qǐng)求,返回內(nèi)容
logger.info("RESPONSE : " + ret);
}
}
可以看上面的例子,通過(guò)@Pointcut
定義的切入點(diǎn)為com.didispace.web
包下的所有函數(shù)(對(duì)web層所有請(qǐng)求處理做切入點(diǎn)),然后通過(guò)@Before
實(shí)現(xiàn),對(duì)請(qǐng)求內(nèi)容的日志記錄(本文只是說(shuō)明過(guò)程,可以根據(jù)需要調(diào)整內(nèi)容),最后通過(guò)@AfterReturning
記錄請(qǐng)求返回的對(duì)象。
通過(guò)運(yùn)行程序并訪問(wèn):http://localhost:8080/hello?name=didi
,可以獲得下面的日志輸出
2016-05-19 13:42:13,156 INFO WebLogAspect:41 - URL : http://localhost:8080/hello
2016-05-19 13:42:13,156 INFO WebLogAspect:42 - HTTP_METHOD : http://localhost:8080/hello
2016-05-19 13:42:13,157 INFO WebLogAspect:43 - IP : 0:0:0:0:0:0:0:1
2016-05-19 13:42:13,160 INFO WebLogAspect:44 - CLASS_METHOD : com.didispace.web.HelloController.hello
2016-05-19 13:42:13,160 INFO WebLogAspect:45 - ARGS : [didi]
2016-05-19 13:42:13,170 INFO WebLogAspect:52 - RESPONSE:Hello didi
#優(yōu)化:AOP切面中的同步問(wèn)題
在WebLogAspect切面中,分別通過(guò)doBefore和doAfterReturning兩個(gè)獨(dú)立函數(shù)實(shí)現(xiàn)了切點(diǎn)頭部和切點(diǎn)返回后執(zhí)行的內(nèi)容,若我們想統(tǒng)計(jì)請(qǐng)求的處理時(shí)間,就需要在doBefore處記錄時(shí)間,并在doAfterReturning處通過(guò)當(dāng)前時(shí)間與開(kāi)始處記錄的時(shí)間計(jì)算得到請(qǐng)求處理的消耗時(shí)間。
那么我們是否可以在WebLogAspect切面中定義一個(gè)成員變量來(lái)給doBefore和doAfterReturning一起訪問(wèn)呢?是否會(huì)有同步問(wèn)題呢?
的確,直接在這里定義基本類型會(huì)有同步問(wèn)題,所以我們可以引入ThreadLocal對(duì)象,像下面這樣進(jìn)行記錄:
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
ThreadLocal<Long> startTime = new ThreadLocal<>();
@Pointcut("execution(public * com.didispace.web..*.*(..))")
public void webLog(){}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
startTime.set(System.currentTimeMillis());
// 省略日志記錄內(nèi)容
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 處理完請(qǐng)求,返回內(nèi)容
logger.info("RESPONSE : " + ret);
logger.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get()));
}
}
#優(yōu)化:AOP切面的優(yōu)先級(jí)
由于通過(guò)AOP實(shí)現(xiàn),程序得到了很好的解耦,但是也會(huì)帶來(lái)一些問(wèn)題,比如:我們可能會(huì)對(duì)Web層做多個(gè)切面,校驗(yàn)用戶,校驗(yàn)頭信息等等,這個(gè)時(shí)候經(jīng)常會(huì)碰到切面的處理順序問(wèn)題。
所以,我們需要定義每個(gè)切面的優(yōu)先級(jí),我們需要@Order(i)
注解來(lái)標(biāo)識(shí)切面的優(yōu)先級(jí)。i的值越小,優(yōu)先級(jí)越高。假設(shè)我們還有一個(gè)切面是CheckNameAspect
用來(lái)校驗(yàn)name必須為didi,我們?yōu)槠湓O(shè)置@Order(10)
,而上文中WebLogAspect設(shè)置為@Order(5)
,所以WebLogAspect有更高的優(yōu)先級(jí),這個(gè)時(shí)候執(zhí)行順序是這樣的:
- 在
@Before
中優(yōu)先執(zhí)行@Order(5)
的內(nèi)容,再執(zhí)行@Order(10)
的內(nèi)容 - 在
@After
和@AfterReturning
中優(yōu)先執(zhí)行@Order(10)
的內(nèi)容,再執(zhí)行@Order(5)
的內(nèi)容
所以我們可以這樣子總結(jié):
- 在切入點(diǎn)前的操作,按order的值由小到大執(zhí)行
- 在切入點(diǎn)后的操作,按order的值由大到小執(zhí)行
#代碼示例
本文的相關(guān)例子可以查看下面?zhèn)}庫(kù)中的chapter4-2-4
目錄:
- Github:https://github.com/dyc87112/SpringBoot-Learningopen in new window
- Gitee:https://gitee.com/didispace/SpringBoot-Learning