AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過(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)容,比如常見的有:打開數(shù)據(jù)庫(kù)連接/關(guān)閉數(shù)據(jù)庫(kù)連接、打開事務(wù)/關(guān)閉事務(wù)、記錄日志等。基于AOP不會(huì)破壞原來(lái)程序邏輯,因此它可以很好的對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。
下面主要講兩個(gè)內(nèi)容,一個(gè)是如何在Spring Boot中引入Aop功能,二是如何使用Aop做切面去統(tǒng)一處理Web請(qǐng)求的日志。
準(zhǔn)備工作
因?yàn)樾枰獙?duì)web請(qǐng)求做切面來(lái)記錄日志,所以先引入web模塊,并創(chuàng)建一個(gè)簡(jiǎn)單的hello請(qǐng)求的處理。
- pom.xml中引入web模塊
<!--Web應(yīng)用-->
<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”的功能。
package com.pingkeke.rdf.controller;
import org.springframework.web.bind.annotation.*;
/**
* 實(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中加入如下依賴:
<!--引入AOP依賴-->
<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)是開啟的,也就是說(shuō)只要引入了AOP依賴后,默認(rèn)已經(jīng)增加了@EnableAspectJAutoProxy。
# AOP
# 其中spring.aop.auto屬性默認(rèn)是開啟的
# Add @EnableAspectJAutoProxy.
spring.aop.auto=true
# 而當(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)。
# Whether subclass-based (CGLIB) proxies are to be created (true)
# as opposed to standard Java interface-based proxies (false).
spring.aop.proxy-target-class=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)開始處切入內(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)容部分拋出異常之后的處理邏輯
package com.pingkeke.rdf.aspect;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* Web層日志切面.
*/
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
@Pointcut("execution(public * com.pingkeke.rdf.controller..*.*(..))")
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.pingkeke.rdf.controller包下的所有函數(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/users,可以獲得下面的日志輸出
2017-09-18 22:27:20.337 INFO 5012 --- [nio-8080-exec-2] com.pingkeke.rdf.aspect.WebLogAspect : URL : http://localhost:8080/users
2017-09-18 22:27:20.338 INFO 5012 --- [nio-8080-exec-2] com.pingkeke.rdf.aspect.WebLogAspect : HTTP_METHOD : GET
2017-09-18 22:27:20.338 INFO 5012 --- [nio-8080-exec-2] com.pingkeke.rdf.aspect.WebLogAspect : IP : 0:0:0:0:0:0:0:1
2017-09-18 22:27:20.338 INFO 5012 --- [nio-8080-exec-2] com.pingkeke.rdf.aspect.WebLogAspect : CLASS_METHOD : com.pingkeke.rdf.controller.UserController.getUsers
2017-09-18 22:27:20.338 INFO 5012 --- [nio-8080-exec-2] com.pingkeke.rdf.aspect.WebLogAspect : ARGS : []
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_
2017-09-18 22:27:20.354 INFO 5012 --- [nio-8080-exec-2] com.pingkeke.rdf.aspect.WebLogAspect : RESPONSE : [com.pingkeke.rdf.domain.User@67508c46, com.pingkeke.rdf.domain.User@b4c589, com.pingkeke.rdf.domain.User@fd77eae]
優(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í)間與開始處記錄的時(shí)間計(jì)算得到請(qǐng)求處理的消耗時(shí)間。
那么我們是否可以在WebLogAspect切面中定義一個(gè)成員變量來(lái)給doBefore和doAfterReturning一起訪問(wèn)呢?是否會(huì)有同步問(wèn)題呢?
的確,直接在這里定義基本類型會(huì)有同步問(wèn)題,所以我們可以引入ThreadLocal對(duì)象,像下面這樣進(jìn)行記錄:
package com.pingkeke.rdf.aspect;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* Web層日志切面.
*/
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
ThreadLocal<Long> startTime = new ThreadLocal<>();
@Pointcut("execution(public * com.pingkeke.rdf.controller..*.*(..))")
public void webLog(){}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
startTime.set(System.currentTimeMillis());
// 接收到請(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);
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必須為Bobby,我們?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í)行。