從約定編程開始
首先拋開Spring AOP的各種複雜概念,看一個簡單的約定編程示例,後面再從這個約定編程示例的角度出發回過頭看AOP的各種概念時,可能會好理解許多。
首先定義一個簡單的接口和其實現類,邏輯很簡單:
public interface HelloService {
void sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello(String name) {
if (name == null || name.trim() == "") {
throw new RuntimeException("parameter is null !");
}
System.out.println("hello " + name);
}
}
這個接口和實現類就定義了我們最簡單的服務,下面創建一個攔截器接口來定義一些攔截方法:
public interface Interceptor {
/**
* 事前方法
*/
boolean before();
/**
* 事後方法
*/
void after();
/**
* 取代原有事件方法
* @param invocation 回調參數,使用invocation的proceed方法,回調原有事件
* @return 原有事件返回對象
*/
Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException;
/**
* 事後返回方法,事件沒有發生異常時執行
*/
void afterRunning();
/**
* 事後異常方法,事件發生異常時執行
*/
void afterThrowing();
/**
* 是否使用around方法取代原有方法的標識
*/
boolean useAround();
}
其中用到了一個Invocation對象,該對象的定義如下:
public class Invocation {
private Object[] params;
private Method method;
private Object target;
public Invocation(Object target, Method method, Object[] params) {
this.target = target;
this.method = method;
this.params = params;
}
/**
* 以反射的形式調用原有方法
* @return 方法調用結果
*/
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, params);
}
}
該對象通過構造方法,設置相應的調用target對象和該對象方法method以及其參數。其中proceed方法即是在完成該對象構造之後,對target對象以反射的形式對method進行調用,並傳入params參數。
下面根據該攔截器接口創建一個攔截器的實現類:
public class MyInterceptor implements Interceptor {
@Override
public boolean before() {
System.out.println("before ...");
return true;
}
@Override
public void after() {
System.out.println("after ...");
}
@Override
public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
System.out.println("around before ...");
Object obj = invocation.proceed();
System.out.println("around after ...");
return obj;
}
@Override
public void afterRunning() {
System.out.println("afterRunning ...");
}
@Override
public void afterThrowing() {
System.out.println("afterThrowing ...");
}
@Override
public boolean useAround() {
return true;
}
}
其中useAround方法負責控制是否需要使用around方法來反射調用target的方法,如果爲false則直接調用target的方法。此外,該代碼中需要關注的是around方法,其通過Invocation對象調用其proceed方法來實現反射調用target的方法。
有了以上的定義,我們希望將HelloService中提供的服務通過攔截器織入到一個約定的流程中,而這個約定的流程定義如下:
爲了實現這樣一個約定的流程,我們需要對原有的HelloService對象進行代理,這樣就需要另一個來實現代理的類ProxyBean,該類中具有一個getProxyBean()靜態方法,定義如下(具體的實現後面會說明):
/**
* 綁定代理對象
* @param target 被代理的對象
* @param interceptor 攔截器
* @return 代理對象
*/
public static Object getProxyBean(Object target, Interceptor interceptor);
該方法有幾個需要注意的地方:
- target(被代理的對象)需要具有接口,interceptor對象是上述定義的攔截器接口對象。
- 該方法返回的對象(記爲proxy)可以使用target對象所具有的接口類型進行轉換。
有了這個proxy代理對象,下面對上述的約定過程進行詳細的說明。當通過proxy代理對象的調用target方法時,其執行的過程如下:
- 使用proxy調用方法時首先會執行before方法。
- 如果攔截器的useAround方法返回true,則執行攔截器的around方法,其中通過Invocation對象的proceed方法來反射執行target的方法,而不調用target對象對應的方法。如果useAround方法返回爲false,則直接調用target的方法。
- 無論怎麼樣,在完成調用事件之後,都會執行攔截器的after方法。
- 在執行around方法或者回調target的方法時發生異常,就還行攔截器的afterThrowing方法,如果沒有異常,則執行afterReturing方法。
明確了該流程,下面看一下具體ProxyBean類中的getProxyBean方法的實現:
public class ProxyBean implements InvocationHandler {
private Object target = null;
private Interceptor interceptor = null;
/**
* 綁定代理對象
* @param target 被代理的對象
* @param interceptor 攔截器
* @return 代理對象
*/
public static Object getProxyBean(Object target, Interceptor interceptor) {
ProxyBean proxyBean = new ProxyBean();
proxyBean.target = target; // 保存被代理的對象
proxyBean.interceptor = interceptor; // 保存攔截器
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
proxyBean); // 創建代理對象
return proxy; // 返回代理對象
}
}
注意該代碼中主要使用到的就是Proxy.newProxyInstance方法,該方法的定義如下:
/**
* Returns an instance of a proxy class for the specified interfaces
* that dispatches method invocations to the specified invocation
* handler.
*
* @param loader the class loader to define the proxy class
* @param interfaces the list of interfaces for the proxy class
* to implement
* @param h the invocation handler to dispatch method invocations to
* @return a proxy instance with the specified invocation handler of a
* proxy class that is defined by the specified class loader
* and that implements the specified interfaces
*/
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h);
其中具有的三個參數解釋爲:
- classloader:被代理對象的類加載器
- interfaces:代理對象實現的接口
- invocationHandler:用來分配方法調用的InvocationHandler(當前ProxyBean類)
這裏的ProxyBean類即是用來處理代理方法的InvocationHandler,其實現了InvocationHandler接口並實現瞭如下的invoke方法:
/**
* 代理對象處理方法邏輯
* @param proxy 代理對象
* @param method 當前方法
* @param args 運行參數
* @return 方法調用結果
* @throws Throwable 異常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 定義異常標識
boolean exceptionFlag = false;
Invocation invocation = new Invocation(target, method, args);
Object retObj = null;
try {
if (this.interceptor.before()) {
retObj = this.interceptor.around(invocation);
} else {
retObj = method.invoke(target, args);
}
} catch (Exception e) {
// 產生異常時設置異常標識
exceptionFlag = true;
}
this.interceptor.after();
if (exceptionFlag) {
this.interceptor.afterThrowing();
} else {
this.interceptor.afterRunning();
return retObj;
}
return null;
}
此時,通過上述getProxyBean方法得到了目標對象target到代理對象proxy的綁定。此時通過proxy調用target對象的方法是,會通過這的invoke方法進行調用。而在該invoke方法中實現了上述的約定過程。從而將目標對象的方法織入了約定的流程。下面進行一下測試:
@Test
public void testProxy() {
HelloService helloService = new HelloServiceImpl();
HelloService proxy = (HelloService) ProxyBean.getProxyBean(helloService, new MyInterceptor());
proxy.sayHello("yitian");
System.out.println("-------------name is null!--------------");
proxy.sayHello(null);
}
這裏我們將最開始定義的HelloService對象作爲目標對象target,然後通過ProxyBean.getProxyBean方法得到該對象的代理對象,並使用MyInterceptor實現相應約定下的各個方法。最後調用proxy代理對象的sayHello方法,即進入到了invoke方法中根據約定的流程進行處理。第一次調用sayHello方法中存在參數,第二次中不存在參數,因此會拋出異常,從而測試afterReturning方法和afterThrowing方法的執行。下面運行該測試,得到的輸出如下:
before ...
around before ...
hello yitian
around after ...
after ...
afterRunning ...
-------------name is null!--------------
before ...
around before ...
after ...
afterThrowing ...
可以看到該輸出正式我們在上面約定的流程,從而將目標對象HelloService織入了約定的流程。此時如果需要代理其他對象,僅需要爲ProxyBean.getProxyBean方法傳入具有相應接口的對象既可以完成其的動態代理。
實際上上述的約定編程的過程和Spring AOP的實現在原理上是一致的,Spring AOP的根本也是根據約定的各種流程,將服務織入其中,下面回過來看一下AOP的相關概念。
Spring AOP的相關概念
爲什麼使用AOP?
AOP最爲經典的應用場景是數據庫事務的管理。傳統的JDBC中的事務操作充斥着大量的try-catch-finally語句,例如如下:
@Service
public class JdbcServiceImpl implements JdbcService {
@Autowired
private DataSource dataSource;
@Override
public int insertUser(User user) {
Connection connection = null;
int result = 0;
try {
// 創建數據庫連接
connection = dataSource.getConnection();
// 設置是否自動提交
connection.setAutoCommit(false);
// 開啓事務並設置隔離級別
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
PreparedStatement statement = connection.prepareStatement(
"insert into t_user (user_name, sex, note) values (?, ?, ?)");
statement.setString(1, user.getUserName());
statement.setInt(2, user.getSex().getId());
statement.setString(3, user.getNote());
// 執行SQL
result = statement.executeUpdate();
// 提交事務
connection.commit();
} catch (SQLException e) {
// 發生異常時回滾事務
if (connection != null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 關閉數據庫連接
try {
if (connection != null || !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return result;
}
}
同時也可以意識到JDBC中的數據庫事務操作很容易抽象爲一個具有約定過程的編程模式,該約定的過程如下:
那麼是不是也可以使用上述約定編程的模式將,其中公共的方法交由數據庫事務管理器共同實現,用戶僅實現自己的SQL執行方法,並將其織入到約定的流程中去呢?答案當然是肯定的。因此AOP的使用可以很好的簡化代碼,提供更清晰和方便的邏輯過程,同時提高代碼的可維護性和可靠性。
AOP基本概念和術語
根據上面約定編程的理解,下面看一下Spring AOP的相關概念。
- 連接點(join point):對應的被攔截的對象,Spring AOP只支持方法,因此這裏的連接點是目標對象的的被攔截的方法。例如上述中HelloServiceImpl對象的sayHello方法。
- 切點(point out):有時切面不單單應用於單個方法,也可以是多個類的不同方法。這時需要通過正則表達式和指示器的規則去定義,從而適配連接點。切點就是提供這樣一個功能的概念。
- 通知(advice):就是按照約定的流程中具有的方法,分爲前置通知(before advice),後置通知(after advice),環繞通知(around advice),事後返回通知(afterReturning advice)和異常通知(afterThrowing advice),他們會被織入到根據約定的流程中執行。
- 目標對象(target):被動態代理的對象,例如上述的HelloServiceImpl對象。
- 引入(introduction):是指引入新的類或其他方法,來增強現有Bean的功能。
- 織入(weaving):它是一個通過動態代理技術,爲原有目標對象生成代理對象,然後將使用定義匹配的連接點攔截,並按照約定將各類通知織入約定流程的過程。
- 切面(aspect):是一個定義切點,各類通知和引入的內容,Spring AOP將通過它的其中定義的信息來增強Bean的功能或者將對應的方法織入流程。
下面使用上述的HelloServiceImpl對象的約定過程來看一下相對應的概念:
AOP開發實例
引入AOP依賴:
<!-- 配置Aspect依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
創建連接點
下面使用@Aspect註解的方式進行AOP的相關開發實例。首先確定連接點(join point),也就是需要攔截的方法,下面創建一個UserService類,其中包含一個printUser方法,該方法作爲join point。
public interface UserService {
/**
* 這裏的printUser方法即爲連接點方法
*/
void printUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void printUser(User user) {
if (user == null) {
throw new RuntimeException("檢查用戶參數是否爲空!");
}
System.out.println(user);
}
}
開發切面
有了連接點,現在就需要創建一個切面來描述AOP所需要的其他概念,下面使用@Aspect註解來定義一個切面:
@Aspect
public class MyAspect {
@Pointcut("execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.printUser(..))")
public void pointCut() {
}
@Before("pointCut()")
public void before() {
System.out.println("before ...");
}
@After("pointCut()")
public void after() {
System.out.println("after ...");
}
@AfterReturning("pointCut()")
public void afterRunning() {
System.out.println("after returning ...");
}
@AfterThrowing("pointCut()")
public void afterThrowing() {
System.out.println("after throwing ...");
}
}
其中@PointCut註解標註這個方法爲一個切點,其作用是向Spring描述指定類的指定方法需要啓用AOP編程。並通過切點定義了@Before,@After,@AfterReturning和@AfterThrowing相應的通知。這些通知的註解使用了PointCut進行定義,表示這些方法是在什麼連接點下啓用AOP編程的。也就是上述定義的通知均是針對切點中匹配的連接點printUser來執行的。
此外在PointCut定義時使用到了下面的正則式:
execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.printUser(..))
其中:
- execution表示在執行的時候,攔截裏面的正則式匹配的方法
- * 表示任意返回類型的方法
- cn.zyt.springbootlearning.service.impl.UserServiceImpl指定目標對象的全限定名
- printUser指定目標對象的連接點方法
- (..)表示任意參數進行匹配
測試AOP
有了如上切面的定義,下面對切面進行測試。創建如下的Controller用於測試:
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/print")
@ResponseBody
public User printUser(Long id, String username, int sex, String note) {
User user = new User(id, username, sex, note);
userService.printUser(user);
return user;
}
}
爲了將定義的切面注入到IoC容器中,進行如下的設置:
@Configuration
public class AspectConfiguration {
@Bean(name = "myAspect")
public MyAspect initMyAspect() {
return new MyAspect();
}
}
啓動Spring Boot項目,使用如下的URL進行測試:
http://localhost:8080/user/print?id=1&useName=yitian&sex=1¬e=none
可以看到Console中的輸出如下:
before ...
User{id=1, userName='null', note='none', sex=MALE}
after ...
after returning ...
顯然Spring成功的通過動態代理技術吧我們定義的切面以及其中定義的連接點和各種通知織入到了流程中。
環繞通知
在上述切面中定義的通知類沒有環繞通知,環繞通知是所有通知中功能最強大的,但也不好控制。一般而言使用環繞通知需要大幅度修改原有目標對象的服務邏輯,它是一個取代原有目標對象方法的通知,同時也提供了回調原有目標對象方法的能力。下面下上述定義的切面中加入環繞通知:
/**
* 環繞通知
*/
@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before ...");
// 回調目標對象的原有方法
joinPoint.proceed();
System.out.println("around after ...");
}
下面用上面同樣的測試URL進行測試,得到如下的輸出:
around before ...
before ...
User{id=1, userName='null', note='none', sex=MALE}
around after ...
after ...
after returning ...
當在joinPoint.proceed()方法一行加入斷點進行debug時,可以看到如下的調試內容:
可以看到ProceedingJoinPoint對象是一個被Spring封裝過的代理對象,它包含目標對象target,被代理的joinpoint method:printUser等內容。因此可以通過該方法在回調目標對象方法的同時增強該方法前後相關的內容。
引入
上面在進行UserService的printUser的邏輯時,檢查參數是否爲null,如果是則直接拋出異常,但事實上我們應該可以對參數進行檢測,如果爲空則不再print用戶信息。當然直接修改printUser的方法邏輯可以容易的實現該功能。但這裏假設UserService對象是第三方所提供的,我們無法直接修改其源碼內容。此時Spring允許使用AOP的方式增強該接口的功能,也就是使用引入的方式爲該接口引入新的接口,例如這裏引入一個用戶信息監測的接口UserValidator,其定義如下:
public interface UserValidator {
/**
* 檢查用戶對象是否爲空
*/
boolean validate(User user);
}
public class UserValidatorImpl implements UserValidator {
@Override
public boolean validate(User user) {
System.out.println("爲UserService接口引入新接口:" + UserValidator.class.getSimpleName());
return user != null;
}
}
這樣即可以通過AOP引入的方式將UserValidator接口增強到UserService接口中,在MyAspect類總加入如下代碼來實現參數驗證的功能:
/**
* 爲UserService接口引入UserValidator接口
*/
@DeclareParents(value = "cn.zyt.springbootlearning.service.impl.UserServiceImpl", defaultImpl = UserValidatorImpl.class)
public UserValidator userValidator;
這裏使用的@DeclareParents註解的作用是引入新的接口來增強類的功能,有兩個必須配置的屬性:
- value:指明需要增強的目標類
- defaultImpl:指明引入的類
這裏需要注意value屬性中+號的使用:+號代表該UserServiceImpl 類的所有子類,而目前UserServiceImpl 沒有子類,所以這裏不需要帶+號。具體的問題見:https://blog.csdn.net/yitian_z/article/details/104441427
在UserController中使用如下方法進行測試:
@RequestMapping("/checkandprint")
@ResponseBody
public User checkAndPrintUser(Long id, String username, int sex, String note) {
User user = new User(id, username, sex, note);
UserValidator userValidator = (UserValidator) userService;
if (userValidator.validate(user)) {
userService.printUser(user);
}
return user;
}
使用如下URL進行請求:
http://localhost:8080/user/checkandprint?id=1&useName=yitian&sex=1¬e=none
得到的輸出如下:
爲UserService接口引入新接口:UserValidator
around before ...
before ...
User{id=1, userName='null', note='none', sex=MALE}
around after ...
after ...
after returning ...
從代碼中可以看到,這裏直接將useService對象強轉爲了userValidator對象,然後就可以使用其validate方法來對參數進行驗證。能夠這樣處理的原因,是因爲Spring AOP在對UserServiceImpl對象進行動態代理時,在UserService原有接口的同時,多掛載了UserValidator接口,因此這裏可以使用強制類型轉換。而底層的原理,還記得之前的Proxy.newProxyInstance()方法嗎:
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h);
該方法第二個參數是目標對象的所有instance接口類型,因此在Spring得到代理對象時,會將增強的UseValidator接口也傳遞進去,從而是代理對象同時具有兩個接口。這就是引入的功能。
獲取通知參數
在MyAspect當前具有的通知中,都是沒有參數傳遞的。當然也可以傳遞參數給通知,需要在切點加入相應的正則式就可以了。
/**
* 在前置通知中獲取參數(帶參數的前置通知)
* @param joinPoint 連接點
* @param user 參數名
*/
@Before("pointCut() && args(user)")
public void beforeParam(JoinPoint joinPoint, User user) {
Object[] args = joinPoint.getArgs();
System.out.println("before ... ");
}
該前置通知中,除了使用pointCut設置切點之外,使用了args(user)來約定將連接點printUser方法中名爲user的參數傳遞進來。需要注意的是,對於非環繞通知而言,JoinPoint類型的參數會被自動傳遞到通知中去,對對於環繞通知而言,可以使用ProceedingJoinPoint類型的參數獲取參數。下面對該方法進行調試,得到如下內容:
通過debug的內容可以看到,joinpoint類型的參數中,args屬性和約定傳入的user對象參數的值一致的,也就是非環繞通知中的JointPoint類型的參數會將連接點(printUser)方法中的參數自動傳遞給通知。
織入
織入是一個生成動態代理對象並將切面和目標對象方法編織成爲約定流程的過程。Spring中推薦使用接口加實現類的方式來實現動態代理,也就是JDK的方式。但實際上動態代理的實現方法有很多,例如CGLIB,javasist,ASM等。Spring採用JDK+CGLIB的方式來實現動態代理,對於JDK而言它需要被代理的對象具有接口,而對於CGLIB則不需要。因此在默認情況下,當目標對象具有接口時,Spring會使用JDK的方式進行代理,如果沒有接口時則使用CGLIB的方式生成代理對象。
因此當UserServiceImpl類沒有實現UserService接口的情況下,在UserController中使用如下的非接口注入方式,也可以依賴注入:
@Autowired
private UserServiceImpl userServiceImpl;
此時,Spring將使用CGLIB的方式來生成相應的代理對象。
多個切面
上面的主要過程是對一個連接點方法使用一個切面進行運行。當對一個連接點方法同時運行多個切面的話,其運行過程以及切面的運行順序就是怎樣的呢?下面新創建一個連接點,然後創建多個切面進行測試。
在UserService接口中定義一個新的連接點方法和接口:
public interface UserService {
void printUser(User user);
/**
* AOP多個切面測試
*/
void manyAspects();
}
// 實現類
@Service
public class UserServiceImpl implements UserService {
// ...
@Override
public void manyAspects() {
System.out.println("測試多個Aspect的運行順序");
}
}
定義多個切面,同時攔截目標對象的manyAspects方法:
@Aspect
public class ManyAspect1 {
@Pointcut("execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.manyAspects(..))")
public void manyAspects() {
}
@Before("manyAspects()")
public void before() {
System.out.println("ManyAspect1: before ...");
}
@After("manyAspects()")
public void after() {
System.out.println("ManyAspect1: after ...");
}
@AfterReturning("manyAspects()")
public void afterReturning() {
System.out.println("ManyAspect1: afterReturning ...");
}
@AfterThrowing("manyAspects()")
public void afterThrowing() {
System.out.println("ManyAspect1: afterThrowing ...");
}
}
@Aspect
public class ManyAspect2 {
@Pointcut("execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.manyAspects(..))")
public void manyAspects() {
}
@Before("manyAspects()")
public void before() {
System.out.println("ManyAspect2: before ...");
}
@After("manyAspects()")
public void after() {
System.out.println("ManyAspect2: after ...");
}
@AfterReturning("manyAspects()")
public void afterReturning() {
System.out.println("ManyAspect2: afterReturning ...");
}
@AfterThrowing("manyAspects()")
public void afterThrowing() {
System.out.println("ManyAspect2: afterThrowing ...");
}
}
@Aspect
public class ManyAspect3 {
@Pointcut("execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.manyAspects(..))")
public void manyAspects() {
}
@Before("manyAspects()")
public void before() {
System.out.println("ManyAspect3: before ...");
}
@After("manyAspects()")
public void after() {
System.out.println("ManyAspect3: after ...");
}
@AfterReturning("manyAspects()")
public void afterReturning() {
System.out.println("ManyAspect3: afterReturning ...");
}
@AfterThrowing("manyAspects()")
public void afterThrowing() {
System.out.println("ManyAspect3: afterThrowing ...");
}
}
在UserController中加入如下方法進行測試:
@RequestMapping("/manyAspects")
public void manyAspects() {
userService.manyAspects();
}
使用如下的URL進行請求:
http://localhost:8080/user/manyAspects
結果輸出如下:
ManyAspect1: before ...
ManyAspect2: before ...
ManyAspect3: before ...
測試多個Aspect的運行順序
ManyAspect3: after ...
ManyAspect3: afterReturning ...
ManyAspect2: after ...
ManyAspect2: afterReturning ...
ManyAspect1: after ...
ManyAspect1: afterReturning ...
從輸出結果中可以看到,對於前置通知都是從小到大運行,對於後置通知都是從大到小的順序運行,因此這裏默認是一個責任鏈模式。如果需要指定切面的運行順序,也可以使用@Order(index)註解,標註在Aspect類上使用index指明運行順序,例如@Order(1),這表示被標註的切面首先運行,依次類推,其運行方式依然是指定順序的責任鏈模式。