面向切面編程是 Spring 最為重要的功能之一,在數(shù)據(jù)庫事務(wù)管理中廣泛應(yīng)用。
全部章節(jié)傳送門:
Spring學(xué)習(xí)筆記(一):Spring IoC 容器
Spring學(xué)習(xí)筆記(二):Spring Bean 裝配
Spring學(xué)習(xí)筆記(三): Spring 面向切面
Spring學(xué)習(xí)筆記(四): Spring 數(shù)據(jù)庫編程
Spring學(xué)習(xí)筆記(五): Spring 事務(wù)管理
AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向?qū)ο缶幊蹋┑难a(bǔ)充和完善。
使用"橫切"技術(shù),AOP把軟件系統(tǒng)分為兩個部分:核心關(guān)注點和橫切關(guān)注點。業(yè)務(wù)處理的主要流程是核心關(guān)注點,與之關(guān)系不大的部分是橫切關(guān)注點。橫切關(guān)注點的一個特點是,他們經(jīng)常發(fā)生在核心關(guān)注點的多處,而各處基本相似,比如權(quán)限認(rèn)證、日志、事物。AOP的作用在于分離系統(tǒng)中的各種關(guān)注點,將核心關(guān)注點和橫切關(guān)注點分離開來。
AOP 常用術(shù)語

- 切面(Aspect): 通常是一個類,里面可以定義切入點和通知。
- 通知(Advice): 通知是切面開啟后,切面的方法,它根據(jù)在代理對象真實方法調(diào)用前、后的順序和邏輯區(qū)分,一共五類:
- 前置通知(before): 在動態(tài)代理反射原有對象或者執(zhí)行環(huán)繞通知前執(zhí)行的通知功能。
- 后置通知(after): 在動態(tài)代理反射原有對象或者執(zhí)行環(huán)繞通知后執(zhí)行的通知功能,無論是否拋出異常,它都會被執(zhí)行。
- 返回通知(afterReturning): 在動態(tài)代理反射原有對象或者執(zhí)行環(huán)繞通知后正常返回(無異常)執(zhí)行的通知功能。
- 異常通知(afterThrowing): 在動態(tài)代理反射原有對象或者執(zhí)行環(huán)繞通知產(chǎn)生異常后執(zhí)行的通知功能。
- 環(huán)繞通知(around): 在動態(tài)代理中,它可以取代當(dāng)前被攔截對象的方法,提供回調(diào)原有被攔截對象的方法。
- 引入(Introduction): 在不修改代碼的前提下,引入可以在運(yùn)行期為類動態(tài)地添加一些方法或字段。
- 連接點(join point): 被攔截到的點。
- 切點(Pointcut): 對連接點進(jìn)行攔截的定義。
- 織入(Weaving): 將切面應(yīng)用到目標(biāo)對象并導(dǎo)致代理對象創(chuàng)建的過程。
Spring 對 AOP 的支持
Spring AOP 是一種基于方法攔截的 AOP,它只支持方法攔截。在 Spring 中有4種方法去實現(xiàn) AOP 的攔截功能。
- 使用 ProxyFactoryBean 和對應(yīng)的接口實現(xiàn) AOP。
- 使用 XML 配置 AOP。
- 使用 @AspectJ 注解驅(qū)動切面。
- 使用 AspectJ 注入切面。
其中,真正常用的是 @AspectJ 注解,有時候 XML 配置也有一定的輔助作用,另外2種很少使用。
使用 @AspectJ 注解開發(fā) Spring AOP
使用 @AspectJ 注解的方式是當(dāng)下的主流方式。
創(chuàng)建一個 maven 項目, 引入 Spring 相關(guān)依賴。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
選擇連接點
創(chuàng)建一個接口,使用其中的方法作為連接點。
package com.wyk.aopdemo.service;
import com.wyk.aopdemo.domain.Role;
public interface RoleService {
public void printRole(Role role);
}
然后編寫它的實現(xiàn)類。
package com.wyk.aopdemo.service.impl;
import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleService;
import org.springframework.stereotype.Component;
@Component
public class RoleServiceImpl implements RoleService {
public void printRole(Role role) {
System.out.println("{id: " + role.getId() + ", role_name: "
+ role.getRoleName() + ", note: " + role.getNote() + "}");
}
}
其中的 Role 是一個實體類。
package com.wyk.aopdemo.domain;
public class Role {
private Long id;
private String roleName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
創(chuàng)建切面和切點
在類上添加 @Aspect 注解即可將其變成一個切面。
package com.wyk.aopdemo.aspect;
import org.aspectj.lang.annotation.*;
@Aspect
public class RoleAspect {
@Pointcut("execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..))")
public void print() {
}
@Before("print()")
public void before() {
System.out.println("before ...");
}
@After("print()")
public void after() {
System.out.println("after ...");
}
@AfterReturning("print()")
public void afterReturning() {
System.out.println("afterReturn ...");
}
@AfterThrowing("print()")
public void afterThrowing() {
System.out.println("afterThrowing ...");
}
}
其中的 @Pointcut 注解用來定義切點,其他注解代表通知,很好理解。如過不定義切點則需要在通知上直接寫正則表達(dá)式,比較麻煩。
對切點中的正則表達(dá)式簡單分析一下。
execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..))
- execution: 代表執(zhí)行方法的時候會觸發(fā)。
- *:代表任意返回類型的方法。
- com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole: 代表類的全限定名。
- (..): 任意的參數(shù)。
生成 AOP 實例
最后需要添加方法配置 Spring 的 Bean 。
package com.wyk.aopdemo.config;
import com.wyk.aopdemo.aspect.RoleAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.wyk.aopdemo")
public class AppConfig {
@Bean
public RoleAspect getRoleAspect() {
return new RoleAspect();
}
}
其中 @EnableAspectJAutoProxy 注解用來啟用 AspectJ 框架的自動代理,這個時候 Spring 會生成動態(tài)代理對象,進(jìn)而可以使用 AOP , 而其中的方法用來生成一個切面實例。
進(jìn)行測試
編寫一個測試類進(jìn)行測試。
package com.wyk.aopdemo;
import com.wyk.aopdemo.config.AppConfig;
import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
RoleService roleService = (RoleService)ctx.getBean(RoleService.class);
Role role = new Role();
role.setId(1L);
role.setRoleName("wyk");
role.setNote("haha");
roleService.printRole(role);
System.out.println("############################");
//測試異常通知
role = null;
roleService.printRole(role);
}
}
運(yùn)行程序,在控制臺查看結(jié)果。很顯然切面的通知已經(jīng)通過 AOP 織入約定的流程當(dāng)中。
before ...
{id: 1, role_name: wyk, note: haha}
after ...
afterReturn ...
############################
before ...
after ...
afterThrowing ...
環(huán)繞通知
環(huán)繞通知是 Spring AOP 當(dāng)中最強(qiáng)大的 AOP ,它可以同時實現(xiàn)前置通知和后置通知。它保留了調(diào)度被代理對象原有方法的功能,所以它既強(qiáng)大又靈活,但是可控性不強(qiáng),如果不需要大量改變業(yè)務(wù)邏輯,一般而言并不需要使用它。
在 RoleAspect 類中添加環(huán)繞通知方法。
@Around("print()")
public void around(ProceedingJoinPoint jp) {
System.out.println("around before ...");
try {
jp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("around after ...");
}
在一個切面里通過 @Around 注解加入環(huán)繞通知,其中方法中有一個 ProceedingJoinPoint 參數(shù),它由 Spring 提供,可以反射連接點方法。
運(yùn)行程序,查看結(jié)果。
around before ...
before ...
{id: 1, role_name: wyk, note: haha}
around after ...
after ...
afterReturn ...
############################
around before ...
before ...
...(異常堆棧信息)
around after ...
after ...
afterReturn ...
根據(jù)控制臺輸出,可以看到執(zhí)行順序。
織入
織入是生成代理對象并把切面內(nèi)容放入約定流程的過程,上述示例中連接點所在的類都是擁有接口的類。事實上即使沒有接口,Spring 也能提供 AOP 功能,但是在 Spring 中建議使用接口編程,因為這樣的好處是使定義和實現(xiàn)分離,有利于變化和替換,更加靈活。
給通知傳遞參數(shù)
如果需要給通知傳遞參數(shù)的話,則需要修改通知方法。首先修改連接點為一個多參數(shù)的方法。
public void printRole(Role role, int sort) {
System.out.println("{id: " + role.getId() + ", role_name: "
+ role.getRoleName() + ", note: " + role.getNote() + "}");
System.out.println(sort);
}
這樣在通知中應(yīng)該如下定義方法。
@Before("execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..)) && args(role, sort)")
public void before(Role role, Sort sort) {
System.out.println("before ...");
}
引入
有時候,我們希望在流程中引入其它類的方法來得到更好的實現(xiàn),這時需要使用 Spring 的引入技術(shù)。
首先新建一個接口,用來檢查角色是否為空。
package com.wyk.aopdemo.service;
import com.wyk.aopdemo.domain.Role;
public interface RoleVerifier {
public boolean verify(Role role);
}
編寫其實現(xiàn)類。
package com.wyk.aopdemo.service.impl;
import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleVerifier;
import org.springframework.stereotype.Component;
public class RoleVerifierImpl implements RoleVerifier {
public boolean verify(Role role) {
return role != null;
}
}
在切面 RoleAspect 類中添加一個新屬性,用來將 RoleVerifier 加入到切面之中。
@DeclareParents(value = "com.wyk.aopdemo.service.impl.RoleServiceImpl+",
defaultImpl = RoleVerifierImpl.class)
public RoleVerifier roleVerifier;
注解 @DeclareParents 用來引入類。
- value = "com.wyk.aopdemo.service.impl.RoleServiceImpl+" 表示對 RoleServiceImpl 類進(jìn)行增強(qiáng),也就是在 RoleServiceImpl 中添加一個新的接口。
- defaultImpl 代表默認(rèn)的實現(xiàn)類。
修改測試方法,進(jìn)行測試。
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
RoleService roleService = (RoleService)ctx.getBean(RoleService.class);
RoleVerifier roleVerifier = (RoleVerifier)roleService;
Role role = new Role();
role.setId(1L);
role.setRoleName("wyk");
role.setNote("haha");
if(roleVerifier.verify(role)) {
roleService.printRole(role);
}
}
}
此方法的原理是讓代理對象掛到 RoleService 和 RoleVerifier 兩個接口之下,這樣就可以在它們之間進(jìn)行強(qiáng)制轉(zhuǎn)換。
使用 XML 配置開發(fā) Spring AOP
使用 XML 方式開發(fā) AOP 和使用注解的原理相同。
接口和接口實現(xiàn)類和上面的相同,這里不在描述。切面類和前面的例子也相同,只是不需要添加注解。
package com.wyk.xmlaop.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
public class XmlAspect {
public void before() {
System.out.println("before ...");
}
public void after() {
System.out.println("after ...");
}
public void afterThrowing() {
System.out.println("after-throwing ...");
}
public void afterReturning() {
System.out.println("after-returning ...");
}
public void around(ProceedingJoinPoint jp) {
System.out.println("around before ...");
try {
jp.proceed();
} catch (Throwable e) {
new RuntimeException("回調(diào)原有流程,產(chǎn)生異常 ...");
}
System.out.println("around after ...");
}
}
添加通知
在類路徑下面添加 XML 配置文件 spring-cfg.xml 。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
">
<bean id="xmlAspect" class="com.wyk.xmlaop.aspect.XmlAspect" />
<bean id="roleService" class="com.wyk.xmlaop.service.impl.RoleServiceImpl" />
<aop:config>
<!-- 引用xmlAspect作為切面 -->
<aop:aspect ref="xmlAspect">
<!--定義切點-->
<aop:pointcut id="printRole" expression="execution(* com.wyk.xmlaop.service.impl.RoleServiceImpl.printRole(..))" />
<!--定義通知,引入切點-->
<aop:before method="before" pointcut-ref="printRole" />
<aop:after method="after" pointcut-ref="printRole" />
<aop:after-throwing method="afterThrowing" pointcut-ref="printRole" />
<aop:after-returning method="afterReturning" pointcut-ref="printRole" />
<aop:around method="around" pointcut-ref="printRole" />
</aop:aspect>
</aop:config>
</beans>
在文件中通過<aop:config>定義 AOP 的內(nèi)容信息。
- <aop:aspect> 定義切面類。
- <aop:before> 定義前置通知。
- <aop:after> 定義后置通知。
- <aop:after-throwing> 定義異常通知。
- <aop:after-returning> 定義返回通知。
- <aop:around> 定義環(huán)繞通知。
進(jìn)行測試
創(chuàng)建測試類進(jìn)行測試。
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
RoleService roleService = ctx.getBean(RoleService.class);
Role role = new Role();
role.setId(1L);
role.setRoleName("wyk");
role.setNote("haha");
roleService.printRole(role);
}
}
運(yùn)行程序,查看結(jié)果。
before ...
around before ...
{id: 1, role_name: wyk, note: haha}
around after ...
after-returning ...
after ...
這里的順序貌似和注解版不一樣,不知道為啥。。。
給通知添加參數(shù)
修改 before 方法。
public void before(Role role) {
System.out.println("before ...");
}
然后 XML 中的配置也要修改。
<aop:before method="before" pointcut="execution(* com.wyk.xmlaop.service.impl.RoleServiceImpl.printRole(..)) and aegs(role)" />
和注解版不同的是這里使用 and 而不是 && ,這是因為 & 是 xml 保留字符。
引入
引入方法和上面的例子相同,這里不再贅述,其中 XML 需要添加如下配置。
<aop:declare-parents type-matching="com.wyk.xmlaop.service.impl.RoleServiceImpl+"
implement-interface="com.wyk.xmlaop.service.RoleVerifier"
default-impl="com.wyk.xmlaop.service.RoleVerifierImpl" />
多個切面
Spring 中也能支持一個連接點包含多個切面,但多個切面之間的順序是隨機(jī)的,可以在切面類上使用 @Order注解進(jìn)行排序。如果是 XML 配置文件,也可以在配置標(biāo)簽上添加 order 屬性。