SpringAOP深入理解+特別術語理解


之前寫過一篇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總結

  1. 獲取對象時,生成目標對象的代理對象。

  2. 根據切入點規則,匹配用戶連接點,得到切入點。

  3. 當切入點被調用時,通過代理對象攔截。

  4. 由切面類中的指定的通知執行來進行增強。

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();
    }
}

二、權限簡單管理

  1. 自定義註解
/**
 * 自定義權限註解
 * @author Summerday
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authority {
    String value();
}
  1. 定義切面
@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;
    }
}
  1. 測試
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();
    }
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章