Aspect-oriented Programming (AOP) 即面向切面程式設計。
簡單來說,AOP 是一種程式設計正規化,允許我們模組化地定義橫跨多個物件的行為。AOP 可以幫助我們將應用程式的關注點分離,使得程式碼更加清晰、易於維護和擴充套件。
大白話:在方法執行前後執行指定程式碼,比如紀錄檔記錄、事務開啟/提交/回滾等。
AOP可以幫助我們解決在程式碼中耦合度高的問題,讓我們的程式碼更加模組化和易於維護。
具體來說,AOP可以通過在執行時動態地將通用功能(例如紀錄檔記錄、效能分析、事務管理)應用於多個模組,而無需修改它們的程式碼。這樣可以避免程式碼重複和巢狀,提高程式碼的複用性和可維護性。
另外,在複雜的業務場景中,多個模組可能需要共用某些共同的功能,而AOP可以讓這些功能從模組中抽離出來,以便更好地進行組織和重用。
總的來說,AOP可以讓我們更好地實現程式碼的分離和聚合,從而獲得更高效、更可靠的程式碼。
大白話:增強原方法的功能,解耦通用功能,透明化靜默操作。
舉例 事務切面切面:
增強原方法的功能:原本方法使用的是資料庫連線預設的策略自動提交事務的,有了切面能夠保證方法內同一事務了;
解耦通用功能:但是很多方法都需要做事務的控制,有了切面不需要我們每一個方法都加幾行相同的程式碼;
透明化靜默操作:方法本身需要知道我怎麼開的事務?需要知道我什麼時候多列印了紀錄檔嗎?
@Override
public UserPO findByUsername(@AutoTrim String username) {
log.info("execute findByUsername by username={}", username);
Optional<UserPO> opt = userRepository.findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("username"), username));
Assert.isTrue(opt.isPresent(), "沒有找到使用者");
UserPO userPO = opt.get();
log.info("execute findByUsername by username={}; return {} ", username, userPO);
return userPO;
}
@Override
public UserPO findByEmail(@AutoTrim String email) {
log.info("execute findByEmail by email={}", email);
Optional<UserPO> opt = userRepository.findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("email"), email));
Assert.isTrue(opt.isPresent(), "沒有找到使用者");
UserPO userPO = opt.get();
log.info("execute findByEmail by email={}; return {} ", email, userPO);
return userPO;
}
@Override
public UserPO findByUsername(@AutoTrim String username) {
Optional<UserPO> opt = userRepository.findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("username"), username));
Assert.isTrue(opt.isPresent(), "沒有找到使用者");
return opt.get();
}
@Override
public UserPO findByEmail(@AutoTrim String email) {
Optional<UserPO> opt = userRepository.findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("email"), email));
Assert.isTrue(opt.isPresent(), "沒有找到使用者");
return opt.get();
}
此處程式碼演示 【part1】
演示內容:
UserServiceImpl
注入Bean,測試介面檢視紀錄檔列印;UserServiceAopImpl
注入Bean,測試介面檢視紀錄檔列印;Pointcut
為logPointcut2()
;@ConditionalOnProperty
,修改yml檔案;例如:測試環境要開啟紀錄檔,生產環境要關閉紀錄檔,通過設定靈活控制。PS:我們在此暫不考慮基於
XmlApplicationContext
系列;
AOP的實現依賴於多型和動態代理。
為了更好的理解,我們可以先舉例一個靜態代理的類來分析。
此處程式碼演示 【part2】
演示內容:
UserServiceStaticAopImpl
注入Bean,測試介面檢視紀錄檔列印;UserServiceStaticAopAnyImpl
注入Bean,測試介面檢視紀錄檔列印;PS:我們在此暫不考慮基於
XmlApplicationContext
系列;
靜態代理的AOP結構(簡單易理解版):
切面:指橫跨多個類的一個關注點,它與不同類中相似的方法相對應。如儲存資料時新增紀錄檔,可以新建一個切面設定紀錄檔記錄邏輯。
大白話:一個模組化的切面程式,也可以理解為是一個實現切面功能的類;
用@Aspect定義的Bean Class,或者Spring xml設定裡的aop:aspect標籤:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
連線點:指在應用程式執行過程中的某個特定位置,如方法呼叫或例外處理等。
可以理解為要切面的物件型別,比如要加切面的目標是構造器,或者一個方法,或者是一個屬性的賦值;
AspectJ中可以有很多種,詳見AspectJ Join Points;
SpringAOP中只有一種,就是方法執行(Method execution);
通知:指在切面的某個連線點上執行的程式碼。通知有許多型別,包括「前置通知」、「後置通知」、「返回通知」、「異常通知」、「環繞通知」,其中「環繞通知」能夠完全控制目標方法的執行。
切入點:指一個或多個連線點,通常定義在一個正規表示式中,描述哪些方法會被攔截。
參考:
Join Points and Pointcuts
Spring 之AOP AspectJ切入點語法詳解
例:
within(com.supalle.springaop..*.UserServiceAopImpl)
execution(* com.supalle.springaop.*.*(..))
within(com.supalle.springaop.TestController) && @annotation(com.supalle.springaop.Log)
詳見:AOP Concepts
此處程式碼演示 【part3】
XmlApplicationContext
的應用需要開啟<aop:aspectj-autoproxy />
AnnotationConfigApplicationContext
的應用需要開啟@EnableAspectJAutoProxy
;SpringBoot環境下可以不用,在org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
裡已經預設啟用;
package com.supalle.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* 紀錄檔切面
*
* @author supalle
* @see {@link https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts}
*/
@Slf4j
@Aspect
@Component
//@ConditionalOnProperty(value = "log-execution", havingValue = "true")
public class LogAspect {
@Pointcut("within(com.supalle.springaop..*.UserServiceAopImpl)")
// the pointcut within *AopImpl
private void logPointcut() {
}
@Pointcut("within(com.supalle.springaop..*.UserServiceAopImpl) || within(com.zaxxer.hikari.HikariDataSource)")
private void logPointcut2() {
}
@Before("logPointcut()")
public void beforeLog(JoinPoint joinPoint) {
log.info("Before Advice 1 execute {}", joinPoint.getSignature().getName());
}
// 同一個Aspect內,@Order排序無效,需要靠方法名排序,比如把當前方法改為beforeLog2,就能執行在beforeLog前面
@Before("logPointcut()")
public void beforeLog2(JoinPoint joinPoint) {
log.info("Before Advice 2 execute {}", joinPoint.getSignature().getName());
}
@AfterReturning(value = "logPointcut()", returning = "returnValue")
public void afterReturningLog(JoinPoint joinPoint, Object returnValue) {
log.info("AfterReturning Advice execute {}; {} ", joinPoint.getSignature().getName(), returnValue);
}
@AfterThrowing(value = "logPointcut()", throwing = "throwable")
public void afterThrowingLog(JoinPoint joinPoint, Throwable throwable) {
log.info("AfterThrowing Advice execute {}; {}", joinPoint.getSignature().getName(), throwable.getMessage());
}
@After("logPointcut()")
public void afterLog(JoinPoint joinPoint) {
log.info("After Advice execute {}", joinPoint.getSignature().getName());
}
@Around("logPointcut()")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
Signature joinPointSignature = joinPoint.getSignature();
String name = joinPointSignature.getName();
Logger logger = LoggerFactory.getLogger(joinPointSignature.getDeclaringTypeName());
Object[] args = joinPoint.getArgs();
String[] argNames = ((MethodSignature) joinPointSignature).getParameterNames();
String argString = "";
if (args != null && args.length > 0) {
argString = " by " + IntStream.range(0, args.length)
.mapToObj(index -> argNames[index] + "=" + args[index])
.collect(Collectors.joining(" , "));
}
logger.info("Around Advice execute {}{}", name, argString);
Object returnValue = joinPoint.proceed();
logger.info("Around Advice execute {}{}; return {} ", name, argString, returnValue);
return returnValue;
}
}
略
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors
中會查詢容器中所有的Advisor型別Bean,也屬於AutoProxy的支援;
LogBeforeAdvice
:定義一個Advice,加@Component
註冊為Beanpackage com.supalle.springaop.advice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry的支援
*/
@Slf4j
@Component
public class LogBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
log.info("LogBeforeAdvice execute {}", method.getName());
}
}
LogBeforeAdvisor
:定義一個Advisor,加@Component
註冊為Beanpackage com.supalle.springaop.advisor;
import com.supalle.springaop.advice.LogBeforeAdvice;
import lombok.RequiredArgsConstructor;
import org.aopalliance.aop.Advice;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class LogBeforeAdvisor extends AbstractPointcutAdvisor {
private final LogBeforeAdvice advice;
@Override
public Pointcut getPointcut() {
Pointcut pointcut = new Pointcut() {
@Override
public ClassFilter getClassFilter() {
return clazz -> "UserServiceAopImpl".equals(clazz.getSimpleName());
}
@Override
public MethodMatcher getMethodMatcher() {
return MethodMatcher.TRUE;
}
};
return pointcut;
}
@Override
public Advice getAdvice() {
return advice;
}
}
執行程式碼後偵錯介面,看控制檯列印輸出:
: LogBeforeAdvice execute findByUsername
用的不多,可以參考Programmatic Creation of @AspectJ Proxies
動態代理是指在程式執行時動態生成代理類的技術,即不需要手工編寫代理類的原始碼,而是在程式執行期間通過反射等機制動態地生成。
動態代理模式可以幫助我們減少重複的程式碼,提高程式碼的可維護性和可延伸性。通常情況下,我們都是通過實現介面來建立代理物件,但是如果一個類沒有實現任何介面,我們仍然可以通過動態代理來建立代理物件。動態代理模式適用於一些橫切關注點(cross-cutting concerns)的處理,例如紀錄檔、安全、事務等功能。在 Java 中,動態代理模式主要有兩種實現方式:
基於 JDK 動態代理(JDK Dynamic Proxy):JDK 提供了一個 java.lang.reflect.Proxy 類,可以動態地建立實現一組給定介面的代理類。要求被代理物件必須實現至少一個介面,並通過 Proxy 類的靜態方法 newProxyInstance 來建立代理物件。
基於 CGLIB 動態代理:CGLIB(Code Generation Library) 是一個基於 ASM(Java 位元組碼操作框架)的高效能位元組碼生成庫,可以在執行時動態生成位元組碼,並生成對應的代理類。要求被代理物件必須有預設建構函式,並通過 CGLIB 庫提供的代理工廠(Enhancer 類)來建立代理物件。
總之,動態代理模式可以幫助我們在程式執行期間動態地生成代理類,從而達到增強功能、新增處理邏輯等目的。
Spring AOP 就是動態代理的一種落地實現,底層是通過JDK Proxy和Cglib&ASM策略來進行動態生成代理類;
Spring Native應用下實現AOP的話,剛出來的時候我看過一遍,因為無法動態生成位元組碼,所以是在AOT編譯時,給應用中所有的類都生成了一個代理類,當這個類在實際執行時需要動態代理時,則載入這個代理類直接使用。(時過境遷,現在不知道實現方式變了沒有)
AOT(Ahead-of-time )compiler:預先編譯器;
JIT(Just-In-Time)compiler:即時編譯器;
除了上文說的AOP基本概念在Spring中有對應的類外,Spring AOP APIs中還有其它幾個核心類,用於具體的AOP實現。
Pointcut
+ Advice
的組合;
- JdkDynamicAopProxy
- ObjenesisCglibAopProxy
org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy
用UML畫出他們的結構圖如下:
以UserService
為例,Spring AOP生成的代理類大致結構:
仔細思考,上文舉例的靜態代理實現AOP的程式碼,是不夠健全的,尤其是在Advice
的順序上;
此處程式碼演示 【part2】:beforeAspects
切面中新增 System.out.println(1/0);使其丟擲異常
參照Spring官方檔案的原文:
Advice: Action taken by an aspect at a particular join point. Different types of advice include "around", "before", and "after" advice. (Advice types are discussed later.) Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.
這句「Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.」翻譯過來就是「包括Spring在內的許多AOP框架都將Advice建模為攔截器,並在連線點周圍維護攔截器鏈。」
很好,使用類似於javax.servlet.Filter
攔截器的方式,就可以很好的控制Advice
的執行順序。
詳見:
org.springframework.aop.framework.AdvisedSupport#getInterceptorsAndDynamicInterceptionAdvice
時序圖大致如下:
呼叫鏈的順序,基於Advised中的advisors集合順序,主要的排序策略如下:
Advisor
的order
值,沒有則取Advice
的order
值,還沒有則為null
,約等於最低優先順序;@Aspect
類中定義多個Advice時,先按Around
, Before
, After
, AfterReturning
, AfterThrowing
排序,再按方法名排序;Advice
類實現多個Advice
型別時,按 MethodInterceptor
,BeforeAdvice
,AfterReturningAdvice
,AfterThrowsAdvice
排序;AbstractAdvisingBeanPostProcessor
的後置處理器新增的Advisor
,後置處理器可以通過beforeExistingAdvisors
屬性控制是否排序在所有已經存在的Advisor
前;
- Spring官方檔案:Advice Ordering
- 排序Advisors:
org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#sortAdvisors
- 排序同一Aspect中的Advice:
org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvisorMethods
PS:如果Advisor/Advice是實現了Ordered介面的話,@Order註解是不生效的。
ProxyConfig
下分幾個家族,都可以新增Advisor
,建立AOP代理類。
排序:Ordered=Ordered.HIGHEST_PRECEDENCE; // Integer.MIN_VALUE ;方法內寫死,繼承才可以修改
注意:AsyncAnnotationAdvisor
是在AsyncAnnotationBeanPostProcessor
後置處理器中建立和匹配,beforeExistingAdvisors=true;排序在所有已經存在的Advisor前。
Spring非同步執行的AOP支援。
排序:Ordered=Ordered.HIGHEST_PRECEDENCE; // 繼承自父類別
AsyncExecutionInterceptor
Spring註解@Async
非同步執行的AOP支援。
排序:Ordered=Ordered.LOWEST_PRECEDENCE; // 繼承自父類別
AbstractPointcutAdvisor
,可set;
注意:RetryConfiguration
實現IntroductionAdvisor
,排序優先於同order的未實現IntroductionAdvisor
的Advisor。
Spring註解@Retryable
方法失敗重試的AOP支援。
排序:Ordered=Ordered.LOWEST_PRECEDENCE; // 繼承自父類別
AbstractPointcutAdvisor
,可set,
預設來自@EnableCaching
的order
屬性,也是LOWEST_PRECEDENCE
,可以直接在註解裡修改。
Spring註解快取@EnableCaching
、@Cacheable
等的AOP支援。
排序:Ordered=Ordered.LOWEST_PRECEDENCE; // 繼承自父類別
AbstractPointcutAdvisor
,可set,
預設來自@EnableTransactionManagement
的order
屬性,也是LOWEST_PRECEDENCE
,可以直接在註解裡修改。
Spring註解事務@Transactional
的AOP支援。
Spring AOP代理類建立的時機,主要是在Bean被參照之前,依靠BeanPostProcessor
來實現;
這就要靠ProxyProcessorSupport
家族的AbstractAdvisingBeanPostProcessor
和AbstractAutoProxyCreator
了。
AbstractAdvisingBeanPostProcessor
對單個Advisor
進行AOP切面的新增;AbstractAutoProxyCreator
對所有滿足Pointcut
的Bean進行切面的新增;兩者大部分情況會在postProcessAfterInitialization()
勾點方法中建立Bean的AOP代理物件,但也有一些特殊情況,比如自定義TargetSource
的時候,會在postProcessBeforeInstantiation()
勾點方法中建立代理物件;
還有一種情況是在getEarlyBeanReference()
勾點方法時建立,也會提前建立好AOP代理物件;
詳見原始碼:
org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization
另外註釋一下:postProcessBeforeInstantiation()
勾點執行時,Bean已經完成了 建立物件、屬性填充、依賴注入、初始化方法的執行;
其它自動建立的還有ProxyFactoryBean
、AbstractSingletonProxyFactoryBean
、ScopedProxyFactoryBean
的派生類也會在初始化Bean時自動建立;
Spring IOC容器在掃描所有的BeanDefinition
後,會先按順序初始化完所有的BeanPostProcessor
,在初始化其它普通的Bean
;而AbstractAdvisingBeanPostProcessor
和AbstractAutoProxyCreator
這兩個處理自動AOP的後置處理器排序都是 Ordered=LOWEST_PRECEDENCE
,因此,在這兩後置處理器之前初始化的Bean
都無法被他們進行自動AOP代理。
Customizing Beans by Using a BeanPostProcessor
Spring官方提示:
BeanPostProcessor instances and AOP auto-proxying
Classes that implement the BeanPostProcessor interface are special and are treated differently by the container. All BeanPostProcessor instances and beans that they directly reference are instantiated on startup, as part of the special startup phase of the ApplicationContext. Next, all BeanPostProcessor instances are registered in a sorted fashion and applied to all further beans in the container. Because AOP auto-proxying is implemented as a BeanPostProcessor itself, neither BeanPostProcessor instances nor the beans they directly reference are eligible for auto-proxying and, thus, do not have aspects woven into them.
原始碼詳見:org.springframework.context.support.PostProcessorRegistrationDelegate#registerBeanPostProcessors
AbstractAutoProxyCreator
中,有三個Map
物件儲存AOP代理類的建立資訊,分別是 targetSourcedBeans
、advisedBeans
、earlyProxyReferences
,當三者記憶體在Bean的代理物件時,則不會重複建立;AbstractAdvisingBeanPostProcessor
中會判斷 bean instanceof Advised
,如果已經是AOP代理物件,則不會再建立新的代理物件,直接在此代理物件上新增Advisor
;常說的Spring三級快取指的是org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
類中的三個Map型別的屬性,分別是:
Map<String, Object> singletonObjects; // 存放初始化完成的單例Bean
Map<String, Object> earlySingletonObjects; // 存放getEarlyBeanReference()勾點建立的早期Bean
Map<String, ObjectFactory<?>> singletonFactories; // 存放earlySingletonObject的工廠範例
IOC預設開啟允許迴圈參照,但是在依賴注入時,但是Spring並不想把建立AOP代理的行為提前,因此設計了getEarlyBeanReference
勾點和singletonFactories
、earlySingletonObjects
,以支援迴圈參照。
在範例化Bean
後,先建立出earlySingletonObject
的工廠範例,以備後續依賴注入的其它Bean
參照當前Bean
,當真實存在迴圈依賴時,呼叫earlySingletonObject
的工廠的方法構建早期Bean
供其注入,並將這個早期Bean
存放到earlySingletonObjects
中,以備迴圈依賴不止一次;最終在當前Bean初始化完成後,移除早期Bean
,將最終的Bean
放入singletonFactories
中,成為正式暴露的Bean範例;org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
篇幅太長,略
Spring快取切面主要依賴於AOP的實現,當物件內進行this.doSomething()呼叫時,因為沒有經過代理物件,所以也沒有快取切面的功能;
與快取切面一樣,Spring事務切面主要依賴於AOP的實現,當物件內進行this.doSomething()呼叫時,因為沒有經過代理物件,所以也沒有事務切面的功能;
因為事務切面會在執行方法之前開啟事務,之後再加鎖,當鎖住的程式碼執行完成後,在提交事務,
因此synchronized程式碼塊執行是在事務之內執行的,可以推斷在程式碼塊執行完時,事務還未提交,其他執行緒進入synchronized程式碼塊後,讀取的庫存資料不是最新的。
解決上面的方法,比較簡單的可以在update方法之前加上synchronizedQ在還沒有開事務之間就加鎖,那麼就可以保證執行緒同步