aop 全稱Aspect Oriented Programming意味面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是Spring框架中的一個重要內容,它通過對既有程序定義一個切入點,然後在其前後切入不同的執行內容,比如常見的有:打開數據庫連接/關閉數據庫連接、打開事務/關閉事務、記錄日誌等。基於AOP不會破壞原來程序邏輯,因此它可以很好的對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率
1.springBoot引入aop
1.1 在pom.xml文件中引入依賴
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
我們先看一下aop的默認配置:
# 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).
其中spring.aop.auto屬性默認是開啓的,也就是說只要引入了AOP依賴後,默認已經增加了@EnableAspectJAutoProxy
其中需要注意的是而當我們需要使用CGLIB來實現AOP的時候,需要配置spring.aop.proxy-target-class=true,不然默認使用的是標準Java的實現。
2.配置日誌
在配置日誌之前先講幾個知識點:
實現AOP的切面主要有以下幾個要素:
- 使用@Aspect註解將一個java類定義爲切面類
- 使用@Pointcut定義一個切入點,可以是一個規則表達式,比如下例中某個package下的所有函數,也可以是一個註解等。
- 根據需要在切入點不同位置的切入內容
- 使用@Before在切入點開始處切入內容
- 使用@After在切入點結尾處切入內容
- 使用@AfterReturning在切入點return內容之後切入內容(可以用來對處理返回值做一些加工處理)
使用@Around在切入點前後切入內容,並自己控制何時執行切入點自身的內容 - 使用@AfterThrowing用來處理當切入內容部分拋出異常之後的處理邏輯
具體代碼如下:
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
@Pointcut("execution(public * com.boot.zhiyi.serviceImpl..*.*(..))")
public void serviceLog(){}
@Before("serviceLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到請求,記錄請求內容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 記錄下請求內容
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 = "serviceLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 處理完請求,返回內容
logger.info("RESPONSE : " + ret);
}
}
上面的代碼是實現的一個配置類,其中加上@Aspect就意味着我們此處配置切面,通過@Pointcut定義的切入點爲com.boot.zhiyi.serviceImpl包下的所有函數(對service層所有請求處理做切入點),然後通過@Before實現,對請求內容的日誌記錄(本文只是說明過程,可以根據需要調整內容),最後通過@AfterReturning記錄請求返回的對象。
3.AOP切面中的同步問題
若我們想統計請求的處理時間,就需要在doBefore處記錄時間,並在doAfterReturning處通過當前時間與開始處記錄的時間計算得到請求處理的消耗時間。
那麼我們是否可以在WebLogAspect切面中定義一個成員變量來給doBefore和doAfterReturning一起訪問呢?是否會有同步問題呢?
的確,直接在這裏定義基本類型會有同步問題,所以我們可以引入ThreadLocal對象
具體用法參考以下代碼:
@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());
// 省略日誌記錄內容
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 處理完請求,返回內容
logger.info("RESPONSE : " + ret);
logger.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get()));
}
}
4.配置aop的優先級問題
由於通過AOP實現,程序得到了很好的解耦,但是也會帶來一些問題,比如:我們可能會對Web層做多個切面,校驗用戶,校驗頭信息等等,這個時候經常會碰到切面的處理順序問題。
所以,我們需要定義每個切面的優先級,我們需要@Order(i)註解來標識切面的優先級。i的值越小,優先級越高。假設我們還有一個切面是CheckNameAspect用來校驗name必須爲didi,我們爲其設置@Order(10),而上文中WebLogAspect設置爲@Order(5),所以WebLogAspect有更高的優先級,這個時候執行順序是這樣的:
在@Before中優先執行@Order(5)的內容,再執行@Order(10)的內容
在@After和@AfterReturning中優先執行@Order(10)的內容,再執行@Order(5)的內容
所以我們可以這樣子總結:
- 在切入點前的操作,按order的值由小到大執行
- 在切入點後的操作,按order的值由大到小執行