目錄
- 一、AOP概述
- 1. AOP簡介
- 2. 如何理解AOP
- 二、AOP編程術語
- 1. 切面(Aspect)
- 2. 連接點(JoinPoint)
- 3. 切入點(Pointcut)
- 4. 目標物件(Target)
- 5. 通知(Advice)
- 三、Aspectj對AOP的實作
- 1. Aspectj的通知型別
- 2. Aspectj的切入點運算式
- 3. Aspectj的開發環境
- 四、AspectJ基于注解的AOP實作
- 1. 實作步驟
- 2. @Before前置通知
- 3. @AfterReturning后置通知
- 4. @Around環繞通知
- 5. @AfterThrowing 例外通知
- 6. @After最終通知
- 7. @Pointcut定義切入點
- 五、總結
- 1. 使用aspectj框架實作aop
- 2. Review
一、AOP概述
1. AOP簡介
AOP(Aspect Orient Programming),面向切面編程,面向切面編程是從動態角度考慮程式運行程序
AOP 底層,就是采用動態代理模式實作的,采用了兩種代理:JDK 的動態代理,與 CGLIB的動態代理
可以看之前寫的動態代理,
https://www.cnblogs.com/mengd/p/13429797.html
- jdk動態代理:使用jdk中的Proxy,Method,InvocaitonHanderl創建代理物件,jdk動態代理要求目標類必須實作介面
- cglib動態代理:第三方的工具庫,創建代理物件,原理是繼承, 通過繼承目標類,創建子類,子類就是代理物件, 要求目標類不能是final的, 方法也不能是final的
動態代理的作用:
- 在目標類源代碼不改變的情況下,增加功能
- 減少代碼的重復
- 專注業務邏輯代碼
- 解耦合,讓你的業務功能和日志分離,事務和非業務功能分離
2. 如何理解AOP
AOP(Aspect Orient Programming)面向切面編程
- Aspect: 切面,給你的目標類增加的功能,就是切面,切面的特點: 一般都是非業務方法,獨立使用的
- Orient:面向
- oop: 面向物件編程
理解:
- 需要在分析專案功能時,找出切面
- 合理的安排切面的執行時間(在目標方法前, 還是目標方法后)
- 合理的安全切面執行的位置,在哪個類,哪個方法增加增強功能
二、AOP編程術語
1. 切面(Aspect)
表示增強的功能, 就是一堆代碼,完成某個一個功能,非業務功能
常見的切面功能有日志, 事務, 統計資訊, 引數檢查, 權限驗證
2. 連接點(JoinPoint)
連接業務方法和切面的位置,就某類中的業務方法
3. 切入點(Pointcut)
指多個連接點方法的集合,多個方法
4. 目標物件(Target)
給哪個類的方法增加功能, 這個類就是目標物件
5. 通知(Advice)
通知表示切面功能執行的時間
一個切面有三個關鍵的要素:
- 切面的功能代碼,切面是干什么
- 切面的執行位置,使用Pointcut表示切面執行的位置
- 切面的執行時間,使用Advice表示時間,在目標方法之前,還是目標方法之后
三、Aspectj對AOP的實作
aop是一個規范,是動態的一個規范化,一個標準
aop的技術實作框架:
- spring:spring在內部實作了aop規范,能做aop的作業,我們專案開發中很少使用spring的aop實作, 因為spring的aop比較笨重
- aspectJ: 一個開源的專門做aop的框架,spring框架中集成了aspectj框架,通過spring就能使用aspectj的功能
aspectJ框架實作aop有兩種方式:
- 使用xml的組態檔 : 配置全域事務
- 使用注解,我們在專案中要做aop功能,一般都使用注解, aspectj有5個注解
1. Aspectj的通知型別
AspectJ 中常用的通知有五種型別
- 前置通知
- 后置通知
- 環繞通知
- 例外通知
- 最終通知
2. Aspectj的切入點運算式

以上運算式共4個部分
execution(訪問權限 方法回傳值 方法宣告(引數) 例外型別)
切入點運算式要匹配的物件就是目標方法的方法名,所以,execution 運算式中明顯就
是方法的簽名,
注意,運算式中黑色文字表示可省略部分,各部分間用空格分開
在其中可以使用以下符號:

常用的幾個:
execution(public * * (..))
指定切入點的位置:任意的公共方法
execution(* set*(..))
指定切入點的位置:任何一個以set開始的方法
execution(* com.xyz.service.*.*(..))
指定切入點的位置:定義在service包里的任意類的任意方法
execution(* com.xyz.service..*.*(..))
指定切入點的位置:定義在service包或者子包里的任意類的任意方法
.. 出現在類名中時,后面必須跟*,表示包、子包下的所有類
execution(* *..service.*.*(..))
指定所有包下的service子包下所有類中所有的方法為切入點
3. Aspectj的開發環境
1. maven依賴
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--spring依賴-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!--aspectj依賴-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
2. 引入AOP約束
在 AspectJ 實作 AOP 時,要引入 AOP 的約束,組態檔中使用的 AOP 約束中的標簽,均是 AspectJ 框架使用的,而非 Spring 框架本身在實作 AOP 時使用的
AspectJ 對于 AOP 的實作有注解和組態檔兩種方式,常用是注解方式
四、AspectJ基于注解的AOP實作
1. 實作步驟
1. 定義業務介面與實作類
package com.md.b1;
/**
* @author MD
* @create 2020-08-09 10:55
*/
public interface SomeService {
void doSome(String name , Integer age);
}
//-------------------------
package com.md.b1;
/**
* @author MD
* @create 2020-08-09 10:55
*/
// 目標類
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
// 給doSome方法增加一個功能,在執行之前輸出時間
System.out.println("目標方法doSome()");
}
}
2. 定義切面類
類中定義了若干普通方法,將作為不同的通知方法,用來增強功能
注意點:
@Aspect
- 這個注解是aspectj框架中的注解
- 作用:表示當前類是切面類
- 切面類:是用來給業務方法增加功能的類,在這個類中有切面的功能代碼
- 位置:類定義的上面
定義方法,方法是實作切面功能的
方法的要求:
- 公共方法
- 方法名稱自定義
- 方法沒有回傳值
- 方法可以有或沒有引數,如果有引數,引數不是自定義的,有幾個引數型別可以使用
package com.md.b1;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
/**
* @author MD
* @create 2020-08-09 10:58
*/
@Aspect
public class MyAspect {
// 前置通知,具體的在下變
@Before(value = https://www.cnblogs.com/mengd/p/"execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
public void myBefore(){
// 就是你切面要執行的功能代碼
System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
}
}
3. 定義目標物件切面類物件
還是在src/main/resources下建立applicationContext.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--把物件交給spring容器,由spring容器統一創建,管理物件-->
<!--宣告目標物件-->
<bean id="someService" class="com.md.b1.SomeServiceImpl"/>
<!--宣告切面類物件-->
<bean id="myAspect" class="com.md.b1.MyAspect"/>
</beans>
4. 注冊AspectJ自動代理
在上面檔案的基礎上添加
<bean id="someService" class="com.md.b1.SomeServiceImpl"/>
<bean id="myAspect" class="com.md.b1.MyAspect"/>
<!--宣告自動代理生成器:
使用的是aspectj框架內部的功能,創建目標物件的代理物件
創建代理物件是在記憶體中實作的,修改目標物件的記憶體中的結構,
創建為代理物件,所以,目標物件就是被修改后的代理物件
aspectj-autoproxy:會把spring容器中的所有目標物件,一次性都生成代理物件
-->
<aop:aspectj-autoproxy />
<aop:aspectj-autoproxy/>的底層是由 AnnotationAwareAspectJAutoProxyCreator 實作的,
從其類名就可看出,是基于 AspectJ 的注解適配自動代理生成器,
其作業原理是,<aop:aspectj-autoproxy/>通過掃描找到@Aspect 定義的切面類,再由切
面類根據切入點找到目標類的目標方法,再由通知型別找到切入的時間點
5. 測驗類中的使用
package com.md;
import com.md.b1.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.security.provider.Sun;
/**
* @author MD
* @create 2020-08-09 15:28
*/
public class MyTest01 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 從容器中獲取目標物件,此時的目標物件是經過了aspectj修改后的代理物件
SomeService proxy = (SomeService) ac.getBean("someService");
//com.sun.proxy.$Proxy8 jdk動態代理
//System.out.println(proxy.getClass().getName());
// 通過代理的物件執行方法,實作目標方法執行,增強了功能
proxy.doSome("張三",19);
// 前置通知,切面功能:在目標方法之前輸出時間:Sun Aug 09 15:33:24 CST 2020
// 目標方法doSome()
}
}
2. @Before前置通知
在目標方法執行之前執行
被注解為前置通知的方法,可以包含一個 JoinPoint 型別引數,該型別的物件本身就是切入點運算式
通過該引數,可獲取切入點運算式、方法簽名、目標物件等
不光前置通知的方法,可以包含一個 JoinPoint 型別引數,所有的通知方法均可包含該引數,
這個JoinPoint引數的值是由框架賦予, 必須是第一個位置的引數
@Aspect
public class MyAspect {
// @Before():前置通知注解
// 屬性:value 是切入點運算式,表示切面功能執行的位置
// 位置:在方法的上面
// 特點:
// 1. 在目標方法之前執行
// 2. 不會改變目標方法的執行結果
// 3. 不會影響目標方法的執行
// @Before(value = https://www.cnblogs.com/mengd/p/"execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
// public void myBefore(){
// // 就是你切面要執行的功能代碼
// System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
// }
// @Before(value = "execution( * *..SomeServiceImpl.do*(..))")
// public void myBefore(){
// // 就是你切面要執行的功能代碼
// System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
// }
/**
* 指定通知方法中的引數 : JoinPoint
* JoinPoint:業務方法,要加入切面功能的業務方法
* 作用是:可以在通知方法中獲取方法執行時的資訊, 例如方法名稱,方法的實參,
* 如果你的切面功能中需要用到方法的資訊,就加入JoinPoint.
* 這個JoinPoint引數的值是由框架賦予, 必須是第一個位置的引數
*/
@Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
public void myBefore(JoinPoint jp){
// 獲取方法的完整定義
System.out.println("方法的定義:"+jp.getSignature());
System.out.println("方法的名稱:"+jp.getSignature().getName());
// 獲取方法的實參
Object[] args = jp.getArgs();
for (Object arg:args){
System.out.println("引數:"+arg);
}
// 方法的定義:void com.md.b1.SomeService.doSome(String,Integer)
// 方法的名稱:doSome
// 引數:張三
// 引數:19
// 就是你切面要執行的功能代碼
System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
}
}
3. @AfterReturning后置通知
在目標方法執行之后執行,由于是目標方法之后執行,所以可以獲取到目標方法的回傳值
該注解的 returning 屬性就是用于指定接收方法回傳值的變數名的
所以,被注解為后置通知的方法,除了可以包含 JoinPoint 引數外,還可以包含用于接識訓傳值的變數,該變數最好為 Object 型別,因為目標方法的回傳值可能是任何型別
增加介面的方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name , Integer age);
}
//--------------------------------------------------
// 目標類
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
// 給doSome方法增加一個功能,在執行之前輸出時間
System.out.println("目標方法doSome()");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("目標方法doOther()");
return "a";
}
}
定義切面
@Aspect
public class MyAspect {
/**
* 后置通知定義方法,方法是實作切面功能的,
* 方法的定義要求:
* 1.公共方法 public
* 2.方法沒有回傳值
* 3.方法名稱自定義
* 4.方法有引數的,推薦是Object ,引數名自定義
*/
/**
* @AfterReturning:后置通知
* 屬性:1.value 切入點運算式
* 2.returning 自定義的變數,表示目標方法的回傳值的,
* 自定義變數名必須和通知方法的形參名一樣,
* 位置:在方法定義的上面
* 特點:
* 1,在目標方法之后執行的,
* 2. 能夠獲取到目標方法的回傳值,可以根據這個回傳值做不同的處理功能
* Object res = doOther();
* 3. 可以修改這個回傳值,但不影響最后的呼叫結果
*
* 后置通知的執行
* Object res = doOther();
* 引數傳遞: 傳值, 傳參考
* myAfterReturing(res);
* System.out.println("res="+res)
*
*/
@AfterReturning(value = https://www.cnblogs.com/mengd/p/"execution(* *..SomeServiceImpl.doOther(..))",returning = "res")
public void myAfterReturing(Object res){
if (res.equals("a")){
// 你可以做一些功能
System.out.println("登陸成功");
}else{
System.out.println("登陸失敗");
}
// Object res:是目標方法執行后的回傳值,可以根據回傳值做切面功能處理
System.out.println("后置通知:獲取的回傳值是:"+res);
// 修改目標方法的回傳值,是否影響最后的方法呼叫結果
// 無影響,
if (res != null){
res = "hello Aspectj";
}
}
}
4. @Around環繞通知
在目標方法執行之前之后執行,
被注解為環繞增強的方法要有回傳值,Object 型別,并且方法可以包含一個 ProceedingJoinPoint 型別的引數,
介面 ProceedingJoinPoint 其有一個proceed()方法,用于執行目標方法,
若目標方法有回傳值,則該方法的回傳值就是目標方法的回傳值,
最后,環繞增強方法將其回傳值回傳,該增強方法實際是攔截了目標方法的執行
首先增加方法和實作
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name,Integer age);
}
//-------------------------------------------
// 目標類
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
// 給doSome方法增加一個功能,在執行之前輸出時間
System.out.println("目標方法doSome()");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("目標方法doOther()");
return "a";
}
@Override
public String doFirst(String name, Integer age) {
System.out.println("目標方法doFirst()");
return "doFirst";
}
}
切面類
@Aspect
public class MyAspect {
/**
* 環繞通知方法的定義格式
* 1.public
* 2.必須有一個回傳值,推薦使用Object
* 3.方法名稱自定義
* 4.方法有引數,固定的引數 ProceedingJoinPoint
*/
/**
* @Around: 環繞通知
* 屬性:value 切入點運算式
* 位置:在方法定義的上面
* 特點:
* 1.它是功能最強的通知
* 2.在目標方法的前和后都能增強功能,
* 3.控制目標方法是否被呼叫執行
* 4.修改原來的目標方法的執行結果, 影響最后的呼叫結果
*
* 環繞通知,等同于jdk動態代理的,InvocationHandler介面
*
* 引數: ProceedingJoinPoint 就等同于 Method
* 作用:執行目標方法的
* 回傳值: 就是目標方法的執行結果,可以被修改,
*
* 環繞通知: 經常做事務, 在目標方法之前開啟事務,執行目標方法, 在目標方法之后提交事務
*/
// @Around(value = https://www.cnblogs.com/mengd/p/"execution(* *..SomeServiceImpl.doFirst(..))")
// public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//
// Object result = null;
//
//
// System.out.println("環繞通知,在目標方法之前加入通知:現在時間:"+new Date());
//
// // 1. 目標方法呼叫,等同于method.invoke(); 在這里等同于Object result = doFirst();
// result = pjp.proceed();
//
// // 2. 在目標方法前后加入功能
// System.out.println("環繞通知,在目標方法之后提交事務");
//
// // 3. 回傳目標方法的執行結果
// return result;
//
// }
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
// 可以獲取到呼叫方法的引數
String name = "";
// 獲取第一個引數的值
Object args[] = pjp.getArgs();
if (args!=null && args.length > 1){
Object arg = args[0];
name = (String) arg;
}
// 實作環繞通知
Object result = null;
System.out.println("環繞通知,在目標方法之前加入通知:現在時間:"+new Date());
// 1. 目標方法呼叫,等同于method.invoke(); 在這里等同于Object result = doFirst();
if ("張三".equals(name)){
// 符合條件,呼叫目標方法
result = pjp.proceed();
}
// 2. 在目標方法前后加入功能
System.out.println("環繞通知,在目標方法之后提交事務");
// 還可以修改目標方法的執行結果,影響方法最后的呼叫結果
if (result != null){
result = "修改了";
}
// 3. 回傳目標方法的執行結果
return result;
}
}
測驗:
public class MyTest03 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 從容器中獲取目標物件
SomeService proxy = (SomeService) ac.getBean("someService");
// 通過代理的物件執行方法,實作目標方法執行,增強了功能
String str = proxy.doFirst("張三",20);
System.out.println(str);
}
}
//環繞通知,在目標方法之前加入通知:現在時間:Mon Aug 10 20:52:07 CST 2020
// 目標方法doFirst()
// 環繞通知,在目標方法之后提交事務
// 修改了
5. @AfterThrowing 例外通知
在目標方法拋出例外后執行,該注解的 throwing 屬性用于指定所發生的例外類物件,當然,被注解為例外通知的方法可以包含一個引數 Throwable,引數名稱為 throwing 指定的名稱,表示發生的例外物件
相當于try-catch中catch里面執行的
增加業務方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
void doSecond();
}
//實作類-----------------------------------------
@Override
public void doSecond() {
System.out.println("執行業務方法doSecond()" + (10/0));
}
切面類:
@Aspect
public class MyAspect {
/**
* 例外通知方法的定義格式
* 1.public
* 2.沒有回傳值
* 3.方法名稱自定義
* 4.方法有個一個Exception, 如果還有是JoinPoint,
*/
/**
* @AfterThrowing:例外通知
* 屬性:1. value 切入點運算式
* 2. throwinng 自定義的變數,表示目標方法拋出的例外物件,
* 變數名必須和方法的引數名一樣
* 特點:
* 1. 在目標方法拋出例外時執行的
* 2. 可以做例外的監控程式, 監控目標方法執行時是不是有例外,
* 如果有例外,可以發送郵件,短信進行通知
*
* 執行就是:
* try{
* SomeServiceImpl.doSecond(..)
* }catch(Exception e){
* myAfterThrowing(e);
* }
*/
@AfterThrowing(value = https://www.cnblogs.com/mengd/p/"execution(* *..SomeServiceImpl.doSecond(..))",
throwing = "ex")
public void myAfterThrowing(Exception ex) {
System.out.println("例外通知:方法發生例外時,執行:"+ex.getMessage());
//發送郵件,短信,通知開發人員
}
}
6. @After最終通知
無論目標方法是否拋出例外,該增強均會被執行
相當于try-catch-finally中finally里面執行的
增加方法及實作
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
void doSecond();
void doThird();
}
//------------------------
@Override
public void doThird() {
System.out.println("執行業務方法doThird()"+ (10/0));
}
切面類
@Aspect
public class MyAspect {
/**
* 最終通知方法的定義格式
* 1.public
* 2.沒有回傳值
* 3.方法名稱自定義
* 4.方法沒有引數, 如果還有是JoinPoint,
*/
/**
* @After :最終通知
* 屬性: value 切入點運算式
* 位置: 在方法的上面
* 特點:
* 1.總是會執行
* 2.在目標方法之后執行的
*
* try{
* SomeServiceImpl.doThird(..)
* }catch(Exception e){
*
* }finally{
* myAfter()
* }
*
*/
@After(value = https://www.cnblogs.com/mengd/p/"execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("執行最終通知,總是會被執行的代碼");
//一般做資源清除作業的,
}
}
7. @Pointcut定義切入點
當較多的通知增強方法使用相同的 execution 切入點運算式時,撰寫、維護均較為麻煩AspectJ 提供了@Pointcut 注解,用于定義 execution 切入點運算式
其用法是,將@Pointcut 注解在一個方法之上,以后所有的 execution 的 value 屬性值均可使用該方法名作為切入點,代表的就是@Pointcut 定義的切入點,這個使用@Pointcut 注解的方法一般使用 private 的標識方法,即沒有實際作用的方法

五、總結
前面的概念有些繞,看了代碼就比較清晰了,感覺和python的裝飾器很像,只不過py的沒有這么繞,就是為已經存在的物件添加額外的功能
總的來說就是在一個方法前或一個方法后執行一些通用的方法,提高效率,把那些業務的方法寫一塊,那些非業務的方法或那些業務方法經常使用的方法寫成切面類,使用方便還便于管理
1. 使用aspectj框架實作aop
使用aop:目的是給已經存在的一些類和方法增加額外的功能,前提是不改變原來類的代碼
- 新建maven專案
- 加入依賴:spring依賴和aspectj依賴以及junit單元測驗
- 創建目標類
- 介面和它的實作類,要做的是給類中的方法增加功能
- 創建切面類:普通類
- 在類的上面加入@Aspect
- 在類中定義方法,這個方法就是切面要執行的功能代碼
- 在方法的上面加入aspectj中的通知注解,例如:@Before
- 還需要指定切入點運算式,execution()
- 創建spring的組態檔,宣告物件,把物件交給容器統一管理,宣告物件可以使用注解或者<bean>
- 宣告目標物件
- 宣告切面類物件
- 宣告aspectj框架中的自動代理生成器標簽,自動代理生成器:用來完成代理物件的自動創建功能
- 創建測驗類
- 從spring容器中獲取目標物件(實際上就是代理物件),通過代理執行,實作aop的功能增強
2. Review



強制使用cglib代理
目標類有介面,還想用cglib代理
proxy-target-class="true" : 這句話就是告訴框架,要使用cglib動態代理
<aop:aspectj-autoproxy proxy-target-class="true" />
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/84961.html
標籤:Java
上一篇:Jfinal資料庫基本操作(—) 連接資料庫以及Model創建
下一篇:LeetCode 圖篇
