Spring AOP

来源:https://www.cnblogs.com/yangyuanhu/archive/2020/01/09/12173361.html
-Advertisement-
Play Games

Spring整合單元測試 在前面的案例中我麽需要自己創建ApplicationContext對象,然後在調用getBean來獲取需要測試的Bean Spring提供了一種更加方便的方式來創建測試所需的ApplicationContext,並且可以幫助我們把需要測試的Bean直接註入到測試類中 添加依 ...


Spring整合單元測試

在前面的案例中我麽需要自己創建ApplicationContext對象,然後在調用getBean來獲取需要測試的Bean

Spring提供了一種更加方便的方式來創建測試所需的ApplicationContext,並且可以幫助我們把需要測試的Bean直接註入到測試類中

添加依賴:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

測試代碼:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)//固定寫法
@ContextConfiguration("classpath:applicationContext.xml") //指定載入的配置文件
public class MyTest2 {
    @Resource(name = "userService")   //直接使用DI獲取Bean
    private UserService userService;

    @Test
    public void test(){
        userService.getUserDao().save();//測試
    }
}

AOP概念

在軟體業,AOP為Aspect Oriented Programming的縮寫,翻譯為:面向切麵編程,通過預編譯方式和運行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也 是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分 進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效 率。

以上內容來自百度百科,看完你會發現這玩兒說個啥呢?看完和沒看差不多,太過於抽象,咱們還是帶著問題來看AOP吧;

為什麼需要AOP

案例分析

在項目開發中我們經常遇到一系列通用需求比如:許可權控制,日誌輸出,事務管理,數據統計等,這寫看似簡單的需求,在實際開發中卻會帶來麻煩,舉個例子:

在某個的Dao層如UserDao,存在以下幾個方法

public class UserDao{
  public void save(){
    System.out.println("save sql");
  }
  public void delete(){
    System.out.println("delete sql");
  }
  public void update(){
    System.out.println("update sql");
  }
}

在第一個版本中,已經實現了程式的實際功能,但是後來發現資料庫操作出現瓶頸,這時領導說要對這些方法進行執行時間統計,並輸出日誌分析問題;

解決方案

這點小需求對於你來說太easy了,於是你打開了代碼,熟練的添加代碼

public class UserDao{
    public void save(){
        //獲取類名和方法名
        String className = Thread.currentThread().getStackTrace()[1].getClassName();
        String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
        //開始時間
        long startTime = new Date().getTime();
      
                //原邏輯
        System.out.println("save sql");
        
        //耗時
        long runTime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
    }
    public void delete(){
        String className = Thread.currentThread().getStackTrace()[1].getClassName();
        String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
        long startTime = new Date().getTime();

        System.out.println("delete sql");

        long runTime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
    }
    public void update(){
        System.out.println("update sql");
    }

    public static void main(String[] args) {
        new UserDao().save();
    }
}

問題

需求實現了,但是作為優秀的開發工程師你當然不會就這麼完事了,因為上述代碼存在以下問題:

  • 1.修改了源代碼(違反了OCP,違反了對修改封閉,但滿足調用方式不變)

  • 2.大量重覆代碼

再看AOP

我們先不考慮如何解決這些問題,其實AOP之所以出現就是因為,我們需要對一些已經存在的方法進行功能擴展,但是又不能通過修改源代碼或改變調用方式的手段來解決

反過來說就是要在保證不修改源代碼以及調用方式不變的情況下為原本的方法增加功能

而由於需要擴展的方法有很多,於是把這些方法稱作一個切麵,即切麵就是一系列需要擴展功能的方法的集合

AOP的目的

將日誌記錄,性能統計,安全控制,事務處理,異常處理等重覆代碼從業務邏輯代碼中劃分 出來,通過對這些行為的分離,我們希望可以將它們獨立到非業務邏輯的方法中,進而改變這些行為的時候不會影響業務邏輯的代碼。

吐槽:

直接看名字的確是比較抽象的,沒辦法,當你創造了一個全新的東西時,你往往也會想給它取一個nb的名字,而這個解決方案是針對一些固定場景的,我們很難找到一個非常準確的名字去描述這個方案

假設你想出了一個解決方案,那麼你會給他取個什麼名字呢?

AOP相關術語

AOP這一概念是AOP聯盟aopalliance提出的,相關的概念也出自aopalliance定義

  • 連接點(joinpoint)

是擴展內容與原有內容的交互的點,可以理解為可以被擴展的地方,通常是一個方法,而AspectJ中也支持屬性作為連接點

示例:案例中的三個方法

  • 切點(pointcut)

切點指的是要被擴展(增加了功能)的內容,包括方法或屬性(joinpoint)

示例:案例中的兩個增加了功能的方法

  • 通知(adivce)

通知指的是要在切點上增加的功能

按照執行時機不同分為:

​ 前置,後置,異常,最終,環繞,引介

​ 引介通知指的是在不修改類代碼的前提下,為類增加方法或屬性(瞭解即可非重點)

示例:上述案例中的輸出執行時間功能

  • 目標(target)

目標就是要應用通知的對象,即要被增強的對象

示例:上述案例中的userDao

  • 織入(weaving)

織入是一個動詞,描述的是將擴展功能應用到target的這個過程

示例:案例中修改源代碼的過程

  • 代理(proxy)

Spring是使用代理來完成AOP,對某個對象增強後就得到一個代理對象;

Spring AOP的整個過程就是對target應用advice最後產生proxy,我們最後使用的都是proxy對象; 狸貓換太子,偷梁換柱;

  • 切麵(aspect)

是切入點和通知的結合切麵,是一個抽象概念; 一個切麵指的是所有應用了同一個通知的切入點的集合

示例:案例中的save 和 delete方法共同組成一個切麵

AOP的傳統實現

就在官方努力退出動態代理時,民間開發者也安耐不住自己躁動的新,開發了自己的一套實現AOP的方案,兩者都是利用代理對象,都屬於代理模式,但是實現原理略有不同;

動態代理(官方)

JDK1.4出現

介面:

public interface UserDao {
    public void save();
    public void delete();
}

實現類:

package com.yh.demo4;

public class UserDaoImpl implements UserDao {
    public void save(){
        System.out.println("save run");
    }
    public void delete(){
        System.out.println("delete run");
    }
}

代理類及測試代碼:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;

public class MyProxy implements InvocationHandler {
    private Object target;


    public MyProxy(Object target) {
        this.target = target;
    }

    //創建代理對象 本質是動態的產生一個target對象的介面實現類
    public Object createProxy(){
        Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        return o;
    }

    //方法處理 動態代理核心方法,在調用代理對象方法時都會自動調用該方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //獲取類信息和方法名
        String className = target.getClass().getName();
        String methodName = method.getName();
        //記錄開始時間
        long startTime = new Date().getTime();

        //調用原始方法
        Object result = method.invoke(target,args);

        //計算耗時
        long runtime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]\n",className,methodName,runtime);
        //返回原始方法執行結果
        return result;
    }

    public static void main(String[] args) {
        //目標對象
        UserDao userDao = new UserDaoImpl();
        //代理對象
        UserDao proxyDao = (UserDao) new MyProxy(userDao).createProxy();

        proxyDao.save();
        proxyDao.delete();
    }
}

當我們要對某些方法進行許可權控制時也非常簡單,只需要判斷方法名稱,然後增加許可權控制邏輯即可;

註意:
1.動態代理,要求被代理的target對象必須實現了某個介面,且僅能代理介面中聲明的方法,這給開髮帶來了一些限制,當target不是某介面實現類時,則無法使用動態代理,CGLib則可以解決這個問題
2.被攔截的方法包括介面中聲明的方法以及代理對象和目標對象都有的方法如:toString
3.對代理對象執行這些方法將造成死迴圈

CGLib(民間)

CGLib是第三方庫,需要添加依賴:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.5</version>
</dependency>

被代理類:

public class UserDaoImpl {
    public void save(){
        System.out.println("save run");
    }

    public void delete(){
        System.out.println("delete run");
    }
}

代理類及測試代碼:

public class MyProxy implements MethodInterceptor {
    private Object target;
    public MyProxy(Object target) {
        this.target = target;
    }

    //創建代理對象 本質是動態的產生一個target對象的介面實現類
    public Object createProxy(){
        //CGLib核心類
        Enhancer enhancer = new Enhancer();
        //指定要代理的對象類型
        enhancer.setSuperclass(target.getClass());
        //設置方法回調 即代理調用代理對象的方法時會執行的方法
        enhancer.setCallback(this);
        //創建代理對象
        Object o = enhancer.create();
        return o;
    }
    /***
     * @param o 代理對象
     * @param method 客戶要執行的方法
     * @param objects 方法參數
     * @param methodProxy 方法代理對象 用於執行父類(目標)方法
     * @return  原始方法的返回值
     * @throws Throwable*/
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //註意不要對第一個參數(代理對象) 執行任何方法,會造成死迴圈

        //獲取類信息和方法名
        String className = target.getClass().getName();
        String methodName = method.getName();
        //記錄開始時間
        long startTime = new Date().getTime();

        //調用原始方法1 傳入的是目標對象
        Object result = method.invoke(target,objects);
      
          //調用原始方法2 傳入的是代理對象
        Object result = methodProxy.invokeSuper(o,objects);

        //計算耗時
        long runtime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]\n",className,methodName,runtime);
        //返回原始方法執行結果
        return result;
    }

    public static void main(String[] args) {
        //目標對象
        UserDaoImpl userDao = new UserDaoImpl();
        //代理對象
        UserDaoImpl proxyDao = (UserDaoImpl) new MyProxy(userDao).createProxy();

        proxyDao.save();
        proxyDao.delete();
    }
}

註意:
1.CGLib可以攔截代理目標對象的所有方法
2.CGLib採用的是產生一個繼承目標類的代理類方式產生代理對象,所以如果類被final修飾將無法使用CGLib

利用上述兩種方法我們就可以實現OAP了

Spring中的AOP

Spring在運行期,可以自動生成動態代理對象,不需要特殊的編譯器,Spring AOP的底層就是通過JDK動態代理和CGLib動態代理技術 為目標Bean執行橫向織入。並且Spring會自動選擇代理方式

1.若目標對象實現了若幹介面,spring使用JDK的java.lang.reflect.Proxy類代理。

2.若目標對象沒有實現任何介面,spring使用CGLIB庫生成目標對象的子類。

Spring通知類型

  • 前置org.springframework.aop.MethodBeforeAdvice用於在原始方法執行前的預處理

  • 後置org.springframework.aop.AfterReturningAdvice用於在原始方法執行後的後處理

  • 環繞org.aopalliance.intercept.MethodInterceptor這個名字不知道誰給起的,其實不算是通知,而是叫攔截器,在這裡我們可以阻止原始方法的執行,而其他通知做不到

  • 異常org.springframework.aop.ThrowsAdvice用於在原始方法拋出異常時處理

  • 引介org.springframework.aop.IntroductionInterceptor在目標類中添加一些新的方法和屬

    性(非重點)

Spring切麵類型

普通的切麵(Advisor)

普通切麵指的是未指定具體切入點的切麵,那麼將把目標對象中所有方法作為切入點(全部增強)

介面:

public interface StudentDao {
    public void save();
    public void update();
    public void delete();
    public void select();
}

實現類:

public class StudentDaoImpl implements StudentDao {
    public void save() { System.out.println("save run"); }

    public void update() { System.out.println("update run"); }

    public void delete() { System.out.println("delete run"); }

    public void select() { System.out.println("select run"); }
}

配置文件:

<!--目標Bean-->
<bean id="studentDao" class="com.yh.demo7.StudentDaoImpl"/>
<!--通知-->
<bean id="before" class="com.yh.demo7.MyAdvice"/>

<!---代理Bean-->
 <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
         <!--指定目標-->
       <property name="target" ref="studentDao"/>
       <!--指定目標實現的介面-->
       <property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
       <!--通知(攔截)Bean的名稱 多個用逗號隔開-->
       <property name="interceptorNames" value="before"/>

       <!--其他設置:-->
       <!--告知spring目標對象是否是一個普通類 true時使用CGlib-->
       <property name="proxyTargetClass" value="true"/>
       <!--代理類是否採用單例,預設true-->
       <property name="singleton" value="false"/>
       <!--是否強制使用CGlib-->
       <property name="optimize" value="true"/>
</bean>

通知類:

public class MyAdvice implements MethodBeforeAdvice,AfterReturningAdvice, MethodInterceptor {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知....");
    }
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("後置通知....");
    }
  
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("環繞前....");
        Object result = methodInvocation.proceed(); //執行原始方法
        System.out.println("環繞後....");
        return result;
    }
}

測試代碼:

@RunWith(SpringJUnit4ClassRunner.class)//固定寫法
@ContextConfiguration("classpath:applicationContext4.xml") //指定載入的配置文件
public class MyTest4 {
    @Resource(name = "studentDaoProxy")
    private StudentDao studentDao;
    @Test
    public void test(){
        studentDao.delete();
        studentDao.save();
        studentDao.update();
        studentDao.select();
    }
}

切入點切麵使用(PointcutAdvisor)

顧名思義,也就是指定為目標對象中僅某進行增強

使用正則匹配方法的切麵:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd">

        <!--目標Bean-->
        <bean id="studentDao" class="com.yh.demo7.StudentDaoImpl"/>
        <!--通知-->
        <bean id="advice" class="com.yh.demo7.MyAdvice"/>

        <!--組織切麵信息-->
        <bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
                <!--指定一個正則表達式  方法名稱匹配則將方法作為切入點-->
                <property name="pattern" value=".*save"/>
                <!--指定多個正則表達式  多個表達式用逗號隔開即可-->
                <property name="patterns" value=".*save,.*update"/>
                <!--指定要應用的通知-->
                <property name="advice" ref="advice"/>
        </bean>

        <!---代理Bean-->
        <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
                <!--指定目標-->
                <property name="target" ref="studentDao"/>
                <!--指定目標實現的介面-->
                <property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
                <!--指向Pointcut切入點-->
                <property name="interceptorNames" value="myAdvisor"/>
        </bean>
</beans>

使用預設的切點切麵:

<bean id="pointcutBean" class="org.springframework.aop.support.NameMatchMethodPointcut">
    <property name="mappedNames">
        <list>
            <value>*save</value>
        </list>
    </property>
</bean>

<bean id="myAdvisor2" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="advice"/>
        <property name="pointcut" ref="pointcutBean"/>
</bean>

自動生成代理 待更新....................

​ 基於BeanName生成代理

​ 基於切點信息生成代理


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 項目圖 首先是menu.html <!DOCTYPE html> <html> <head> <title>深圳麥當勞前海二餐廳</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, ini ...
  • 項目整體圖 首先是order.html <!DOCTYPE html> <html> <head> <title>訂單</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial- ...
  • 效果圖 首先是index.html <!DOCTYPE html> <html> <head> <title>首頁</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-sc ...
  • DOM盒模型和位置 client offset scroll 和滾動的關係 概覽 在dom裡面有幾個描述盒子位置信息的值, pading border margin width height client clientWidth 不要border clientHeight 不要border offs ...
  • 在spring實例化 之前bean對象封裝成 beanDefinition 對象 想瞭解詳情的請參考上一篇文章 好了 我們聊聊 Bean 的實例化過程的幾個重要角色 BeanDefinitionRegistryPostProcessor 介面 Refresh().invokeBeanFactoryP ...
  • 具體的LinkedList數據結構參考這個鏈接 隊列用法主要來自於Deque,而Deque又繼承自Queue 先從下麵一段代碼說起吧 如果對於隊列的數據結構不瞭解的話,對上面的註釋內容會比較迷糊。Queue介面主要是使LinkedList具有隊列的能力。隊列類似於我們生活中的排隊;特點就是先進先出, ...
  • 一、冒泡排序 1.也就是依次選出最大的放在最後面 package com.bjpowernode.java_learning; ​ public class D70_1_BubbleSort { public static void main(String[] args) { /* * 冒泡排序演算法 ...
  • 本系列筆記主要基於《深入理解Java虛擬機:JVM高級特性與最佳實踐 第2版》,是這本書的讀書筆記。 jstat命令用來查看JVM統計信息,可以查看類載入信息、垃圾收集的信息、JIT編譯信息等等,功能非常豐富。 所有的JDK工具都可以在Oracle官網的 "Java Tools Reference" ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...