之前寫過一篇Spring面向切面編程的具體操作:三種方式配置通知,當然也只是停留在操作層面,今天回頭看這個知識點的時候,發現自己的理解更加深刻,故在此做一點小小的總結。
AOP面向切面編程是spring的核心之一,它的一些術語還是比較抽象的,至少初始的時候我是這麼覺得的,但慢慢接觸了一些設計思想,如代理模式創建實現相同接口的代理對象,以增強指定方法的思想之後,就漸漸理解其中的精妙,當然,理解還是不能完全理解的,只能說慢慢探索,日益精進。
一、簡單案例的理解
面向切面編程的思想被廣泛應用一定有他的道理,一定是因爲它的出現解決了某些繁雜的類似於搬磚似的工作。
我們以一個簡單案例作爲切入,請暫時不要在意其中邏輯,暫時以打印日誌信息作爲事務控制:
首先,我們定義一個賬戶接口AccountService
,裏面包含一些基本的增刪改方法,並創建一個實現類AccountServiceImpl
實現之,暫且以打印信息模擬數據庫操作。
@Service("accountService")
public class AccountServiceImpl implements AccountService {
public void saveAccount() {
System.out.println("==> 正常業務:AccountServiceImpl的saveAccount方法正常執行");
}
public void updateAccount(int i) {
System.out.println("==> 正常業務:AccountServiceImpl的updateAccount方法正常執行");
}
public int deleteAccount() {
System.out.println("==> 正常業務:AccountServiceImpl的deleteAccount方法正常執行");
return 10;
}
}
需求:在每個方法執行前後都打印日誌信息,如果發生異常,打印異常信息。
呃,需求還是很好實現的,隨便一想就有倆可以實現這個簡單的需求:
- 直接在方法裏面打印信息嘛,所有方法都寫上一遍,不怕累,但日誌代碼大量侵入正常業務功能模塊,存在大量耦合,顯然不可取。
- 使用動態代理技術,基於JDK的動態代理技術,創建出與被代理對象實現相同接口的代理對象,在反射調用方法前後對方法進行增強,比如打印必要的日誌信息。
於是我們果斷採用動態代理的技術,對需求進行實現,並進行了測試:
public class aopTest {
public static void main(String[] args) {
//獲取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//獲取對象
final AccountService as = ac.getBean(AccountService.class);
AccountService asProxy = (AccountService)Proxy.newProxyInstance(as.getClass().getClassLoader(), as.getClass().getInterfaces(), new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object value = null;
//獲取方法名
String name = method.getName();
try {
System.out.println(name+"方法 ==>即將執行...");
value = method.invoke(as, args);
System.out.println(name+"方法 ==>環繞返回通知... 返回結果 ==>"+value);
} catch (Throwable e) {
System.out.println(name+"方法 ==>環繞異常通知... 異常信息 ==>"+e);
} finally {
System.out.println(name+"方法 ==>最終執行完畢...");
}
return value;
}
});
//執行方法
asProxy.deleteAccount();
System.out.println("================");
asProxy.saveAccount();
}
}
可以發現動態代理可以實現我們的需求,但JDK的動態代理只能基於接口進行,如果要基於實現類,可以利用第三方庫cglib實現,在此就不贅述了。
ok,說到這,我們成功地使日誌代碼動態地在目標業務方法的前後執行,我們的業務代碼僅僅只需要關注業務自身邏輯,而日誌信息,事務控制等代碼轉移至切面中即可,其中的合理性也是顯而易見的。
二、SpringAOP的簡單構建
spring框架對AOP的支持構建在動態代理的基礎之上,當然也只是支持僅限於方法的攔截。那麼,如何來構建呢,關於構建,我在上一篇基於操作的文章中已經寫明,這邊就選擇其中一種,基於xml+註解的方式。
一、首先引入必要的jar包座標:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
二、定義切面類@Aspect註解標註,並讓spring管理,定義通知和切點:
ps:後置通知和返回通知中文翻譯上可能會有偏差,以英文語義爲準。
/**
* @author Summerday
*
* 記錄日誌工具類(切面類)
*/
@Component
@Aspect
public class Logger {
//提取可重用切入點表達式
@Pointcut("execution(* com.smday.service.impl.*.*(..))")
private void pt1(){}
/**
* 用於打印日誌:計劃讓其在切入點方法執行之前執行(切入點方法就是業務層方法)
* 可以通過JoinPoint獲取目標方法的詳細信息
*/
@Before("pt1()")
public void printBeforeLog(JoinPoint joinPoint){
//目標方法運行時的參數
Object[] args = joinPoint.getArgs();
//獲取方法簽名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法 ==>前置通知...");
}
@After("pt1()")
public void printAfterLog(JoinPoint joinPoint){
//獲取方法簽名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法 ==>後置通知...");
}
//可以指定返回值
@AfterReturning(value = "pt1()",returning = "result")
public void printAfterReturningLog(JoinPoint joinPoint,Object result){
//獲取方法簽名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法 ==>返回通知... 返回結果 ==>"+result);
}
//可以指定異常
@AfterThrowing(value = "pt1()",throwing = "e")
public void printAfterThrowingLog(JoinPoint joinPoint,Exception e){
//獲取方法簽名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法 ==>異常通知... 異常信息 ==>"+e.getCause());
}
//@Around("pt1()")
//只有環繞通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint
public Object printAroundLog(ProceedingJoinPoint pjp){
//獲取參數
Object[] args = pjp.getArgs();
//獲取方法名
String name = pjp.getSignature().getName();
Object proceed = null;
try {
System.out.println(name+"方法 ==>環繞前置通知...");
//利用反射推進目標方法即可,即method.invoke(obj,args)
proceed = pjp.proceed(args);
System.out.println(name+"方法 ==>環繞返回通知... 返回結果 ==>"+proceed);
} catch (Throwable throwable) {
System.out.println(name+"方法 ==>環繞異常通知... 異常信息 ==>"+throwable);
} finally {
System.out.println(name+"方法 ==>環繞後置通知...");
}
//反射調用後的返回值一定返回出去
return proceed;
}
}
三、基於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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.smday"/>
<!-- 啓用AspectJ自動代理-->
<aop:aspectj-autoproxy/>
</beans>
四、測試通知
public class aopTest {
public static void main(String[] args) {
//獲取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//獲取對象
AccountService as = ac.getBean(AccountService.class);
//執行方法
as.deleteAccount();
System.out.println("================");
as.saveAccount();
}
}
我們可以發現執行的順序:依次爲前置通知、方法正常執行、後置通知、返回通知。
三、AOP術語學習
學習aop,免不了學習各種新鮮的術語,結合我們之前的小案例,應該會容易理解的多。
-
切面(Aspect):也就是我們定義的專注於提供輔助功能的模塊,比如安全管理,日誌信息等。
-
連接點(JoinPoint):切面代碼可以通過連接點切入到正常業務之中,圖中每個方法的每個點都是連接點。
-
切入點(PointCut):一個切面不需要通知所有的連接點,而在連接點的基礎之上增加切入的規則,選擇需要增強的點,最終真正通知的點就是切入點。
-
通知方法(Advice):就是切面需要執行的工作,主要有五種通知:
- 前置通知Before:目標方法調用之前執行的通知。
- 後置通知After:目標方法完成之後,無論如何都會執行的通知。
- 返回通知AfterReturning:目標方法成功之後調用的通知。
- 異常通知AfterThrowing:目標方法拋出異常之後調用的通知。
- 環繞通知Around:可以看作前面四種通知的綜合。
-
織入(Weaving):將切面應用到目標對象並創建代理對象的過程,SpringAOP選擇再目標對象的運行期動態創建代理對象。
四、切入點表達式
上面提到:連接點增加切入規則就相當於定義了切入點,當然切入點表達式分爲兩種:within和execution,這裏主要學習execution表達式。
-
寫法:execution(訪問修飾符 返回值 包名.包名……類名.方法名(參數列表))
-
例:
execution(public void com.smday.service.impl.AccountServiceImpl.saveAccount())
-
訪問修飾符可以省略,返回值可以使用通配符*匹配。
-
包名也可以使用
*
匹配,數量代表包的層級,當前包可以使用..
標識,例如* *..AccountServiceImpl.saveAccount()
-
類名和方法名也都可以使用
*
匹配:* *..*.*()
-
參數列表使用
..
可以標識有無參數均可,且參數可爲任意類型。
全通配寫法:* ….*(…)
通常情況下,切入點應當設置再業務層實現類下的所有方法:* com.smday.service.impl.*.*(..)
。
五、SpringAOP總結
-
獲取對象時,生成目標對象的代理對象。
-
根據切入點規則,匹配用戶連接點,得到切入點。
-
當切入點被調用時,通過代理對象攔截。
-
由切面類中的指定的通知執行來進行增強。
Spring自動爲目標對象生成代理對象,默認情況下,如果目標對象實現過接口,則採用java的動態代理機制,如果目標對象沒有實現過接口,則採用cglib動態代理。
六、簡單小實例
一、異常信息寫入文件
@Component
@Aspect
public class ExceptionAspect {
private FileWriter writer = null;
{
try{
writer = new FileWriter("err.log");
}catch (IOException e){
e.printStackTrace();
}
}
@AfterThrowing(value = "execution(* com.smday.service.impl.*.*(..))",throwing = "t")
public void afterThrowing(JoinPoint joinPoint,Throwable t)throws Exception{
//獲取類型信息
Class<?> aClass = joinPoint.getTarget().getClass();
//獲取方法名
String name = joinPoint.getSignature().getName();
//獲取異常信息
String msg = t.getMessage();
String err = "["+aClass+"] == ["+name+"] == ["+msg+"]";
writer.write(err);
writer.flush();
}
}
二、權限簡單管理
- 自定義註解
/**
* 自定義權限註解
* @author Summerday
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authority {
String value();
}
- 定義切面
@Component
@Aspect
public class AuthorityAspect {
@Around("execution(* com.smday.service.impl.*.*(..))&&@annotation(authority)")
public Object around(ProceedingJoinPoint pjp, Authority authority) throws Throwable {
//獲取方法註解定義權限
String value = authority.value();
//方法名
String name = pjp.getSignature().getName();
//權限列表
List<String> authorityList = AopTest.getAuthorityList();
System.out.println("當前用戶擁有的權限列表爲:"+ authorityList);
Object proceed = null;
if(authorityList.contains(value)){
System.out.println("==> ["+name+"]方法已擁有權限...");
proceed = pjp.proceed();
}else {
System.out.println("==> ["+name+"]方法並沒有權限...");
}
return proceed;
}
}
- 測試
public class AopTest {
private static final ThreadLocal<List<String>> AuthorityList = new ThreadLocal<List<String>>();
static {
List<String> list = new ArrayList<String>();
list.add("delete");
AuthorityList.set(list);
}
public static List<String> getAuthorityList() {
return AuthorityList.get();
}
public static void main(String[] args) {
//獲取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//獲取對象
final AccountService as = ac.getBean(AccountService.class);
as.deleteAccount();
System.out.println("=====================================");
as.saveAccount();
}
}