Spring之AOP詳解


1.背景

2.AOP的概念

AOP:用鳥語解釋就是 面向切面編程,詳細的解釋大家可以看百度百科,

百度百科:https://baike.baidu.com/item/AOP/1332219?fr=aladdin

不過估計看了后還是一頭霧水...

通俗的理解是:假設有方法M1、M2、M3....,現在需要在這些方法中增加新的業務邏輯(如:打印方法名稱、方法參數、執行結果),但是不把這些新的業務邏輯寫到這些方法(M1、M2、M3....)中,這就是AOP的思想;

簡單一句話說就是:在方法中增加新的業務邏輯,但是不修改方法中的代碼。

3.AOP的底層原理

實現方式一:有接口的情況下,使用JDK動態代理

實現方式二:沒有接口情況,使用 CGLIB 動態代理

3.1.JDK動態代理實現AOP思想

需求:假設有方法M1、M2、M3....,現在需要在這些方法中增加新的業務邏輯(如:打印方法名稱、方法參數、執行結果),但是不把這些新的業務邏輯寫到這些方法(M1、M2、M3....)中,這就是AOP的思想;

步驟一:准備准備一個dao接口、dao實現、測試

dao接口

package com.ldp.dao;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-12 10:44
 * @Description:
 */
public interface IProductDao {
    /**
     * 保存產品
     *
     * @param name
     * @param price
     * @return
     */
    int saveProduct(String name, Integer price);

    /**
     * 根據id刪除產品
     *
     * @param id
     * @return
     */
    int deleteProduct(Integer id);

    /**
     * 根據id修改產品
     *
     * @param id
     * @param name
     * @param price
     * @return
     */
    int updateProduct(Integer id, String name, Integer price);

    /**
     * 根據id查詢產品
     *
     * @return
     */
    String queryProduct(Integer id);
}
View Code

dao實現

package com.ldp.dao.impl;

import com.ldp.dao.IProductDao;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-12 10:51
 * @Description:
 */
public class ProductDaoImpl implements IProductDao {
    @Override
    public int saveProduct(String name, Integer price) {
        System.out.println("模擬 saveProduct---------------");
        return 1;
    }

    @Override
    public int deleteProduct(Integer id) {
        System.out.println("模擬 deleteProduct---------------");
        return 1;
    }

    @Override
    public int updateProduct(Integer id, String name, Integer price) {
        System.out.println("模擬 updateProduct---------------");
        return 1;
    }

    @Override
    public String queryProduct(Integer id) {
        System.out.println("模擬 queryProduct---------------");
        return "模擬返回產品數據";
    }
}
View Code

dao測試

/**
     * 沒有代理的測試
     */
    @Test
    public void test01() {
        // 創建到dao實例對象
        IProductDao productDao = new ProductDaoImpl();

        // 測增加
        int saveProduct = productDao.saveProduct("蘋果", 3);
        System.out.println(saveProduct);

        // 測試刪除
        int deleteProduct = productDao.deleteProduct(12);
        System.out.println(deleteProduct);

        // 測試修改
        int updateProduct = productDao.updateProduct(11, "修改蘋果", 4);
        System.out.println(updateProduct);

        // 測試查詢
        String queryProduct = productDao.queryProduct(11);
        System.out.println(queryProduct);
    }
View Code

步驟二:編寫一個InvocationHandler的實例對象,編寫需要增強的具體功能

package com.ldp.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-12 11:01
 * @Description:
 */
public class MyDaoProxy implements InvocationHandler {
    private Object object;

    /**
     * 定義一個有參數的構造方法
     */
    public MyDaoProxy(Object object) {
        this.object = object;
    }

    /**
     * 這里使用了反射的機制
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 方法執行前
        System.out.println("方法執行前執行.....請求方法名:" + method.getName() + ",請求參數:" + Arrays.toString(args));

        // 執行固有的方法(被增強的方法)
        Object result = method.invoke(object, args);

        // 方法執行后
        System.out.println("方法執行后.....方法執行結果:" + result);

        return result;
    }
}
View Code

步驟三:使用代理對象生成dao實現

// 使用動態代理 創建到dao實例對象 TestProxy.class.getClassLoader()中的TestProxy為當前測試類對象
        // IProductDao productDao = new ProductDaoImpl();
        Class[] interfaces = {IProductDao.class};
        IProductDao productDao = (IProductDao) Proxy.newProxyInstance(TestProxy.class.getClassLoader(), interfaces, new MyDaoProxy(new ProductDaoImpl()));

步驟四:測試代理對象生成的dao

 /**
     * 代理對象的測試
     * newProxyInstance(ClassLoader loader, 類<?>[] interfaces, InvocationHandler h)
     * 返回指定接口的代理類的實例,該接口將方法調用分派給指定的調用處理程序。
     */
    @Test
    public void test02() {
        // 使用動態代理 創建到dao實例對象 TestProxy.class.getClassLoader()中的TestProxy為當前測試類對象
        // IProductDao productDao = new ProductDaoImpl();
        Class[] interfaces = {IProductDao.class};
        IProductDao productDao = (IProductDao) Proxy.newProxyInstance(TestProxy.class.getClassLoader(), interfaces, new MyDaoProxy(new ProductDaoImpl()));
        // 測增加
        int saveProduct = productDao.saveProduct("蘋果", 3);
        System.out.println(saveProduct);

        // 測試刪除
        int deleteProduct = productDao.deleteProduct(12);
        System.out.println(deleteProduct);

        // 測試修改
        int updateProduct = productDao.updateProduct(11, "修改蘋果", 4);
        System.out.println(updateProduct);

        // 測試查詢
        String queryProduct = productDao.queryProduct(11);
        System.out.println(queryProduct);
    }
View Code

參考資料

jdk8Api:https://www.matools.com/api/java8

3.2.cglib動態代理實現aop思想

 步驟一:引入jar包

步驟二:編寫一個普通的需要被增強的方法

package com.ldp.controller;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-12 12:07
 * @Description:
 */
public class ProductController {
    /**
     * 產品查詢
     *
     * @param name
     * @param price
     * @return
     */
    public String queryProduct(String name, Integer price) {
        System.out.println("模擬查詢中............");
        return "香蕉";
    }
}
View Code

步驟三:編寫一個cglib動態代理類

package com.ldp.proxy;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-12 11:50
 * @Description:
 */
public class MyDaoCglibProxy implements MethodInterceptor {
    private Object target;

    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        // 設置回調方法
        enhancer.setCallback(this);
        // 創建代理對象
        return enhancer.create();
    }

    /**
     * 實現MethodInterceptor接口中重寫的方法
     * <p>
     * 回調方法
     */
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 方法執行前
        System.out.println("方法執行前執行.....請求方法名:" + method.getName() + ",請求參數:" + Arrays.toString(args));

        // 執行固有的方法(被增強的方法)
        Object result = proxy.invokeSuper(object, args);

        // 方法執行后
        System.out.println("方法執行后.....方法執行結果:" + result);
        return result;
    }
}
View Code

步驟四:測試

    /**
     * CGLIB 代理對象的測試
     */
    @Test
    public void test03() {
        // 創建動態代理
        MyDaoCglibProxy cglibProxy = new MyDaoCglibProxy();
        // 獲取代理的實例對象
        ProductController productController = (ProductController) cglibProxy.getInstance(new ProductController());
        System.out.println(productController.queryProduct("香蕉", 100));
    }
View Code

測試結果如下:

方法執行前執行.....請求方法名:queryProduct,請求參數:[香蕉, 100]
模擬查詢中............
方法執行后.....方法執行結果:香蕉
香蕉

4.AOP的幾個專業術語

AOP的基本概念

1、連接點:可以被增強的方法

2、切入點:實際被增強的方法

3、通知(增強):

  3.1.實際增強的邏輯部分叫做通知

  3.2.通知類型包括

  1. 前置通知(執行方法前執行,通常用作參數日志輸出、權限校驗等)
  2. 后置通知(邏輯代碼執行完,准備執行return的代碼時通知,通常用作執行結果日志輸出、結果加密等)
  3. 環繞通知(是前置通知和后置通知的綜合,方法執行前和方法執行后都要執行,通常用作方法性能統計、接口耗時、統一加密、解密等)
  4. 異常通知(相當於try{}catch ()中catch執行的部分,程序拋出異常時執行,通常用作告警處理、事務回滾等)
  5. 最終通知(相當於try{}catch (Exception e){}finally { }中的finally執行的部分,通常用在關閉資源、清理緩存等業務邏輯中)

4、切面:把通知(增強)應用到切入點的過程

5.AOP實現技術

1、Spring 框架一般都是基於 AspectJ 實現 AOP 操作

(1)AspectJ 不是 Spring 組成部分,獨立 AOP 框架,一般把 AspectJ 和 Spirng 框架一起使 用,進行 AOP 操作

2、基於 AspectJ 實現 AOP 操作

(1)基於 xml 配置文件實現

(2)基於注解方式實現(使用)

3、在項目工程里面引入 AOP 相關依賴

 4、切入點表達式

(1)切入點表達式作用:知道對哪個類里面的哪個方法進行增強

(2)語法結構: execution([權限修飾符] [返回類型] [類全路徑] [方法名稱]([參數列表]) )

說明:* 號表示通配符,.. 符號表示一個或多個參數

舉例 1:對 com.ldp.controller.ProductController 類里面的 queryProduct 進行增強 execution(* com.ldp.controller.ProductController.queryProduct (..))

舉例 2:對 com.ldp.controller.ProductController 類里面的所有的方法進行增強 execution(* com.ldp.controller.ProductController .* (..))

舉例 3:對 com.ldp.controller 包里面所有以controller結尾的類里面所有方法進行增強 execution(* com.ldp.controller.*Controller .* (..))

6.AOP案例

6.1.AOP 操作(AspectJ 注解)

這里使用spring中的aop實現對controller里日志輸出來加深對aop的理解

實現步驟:

准備工作:

步驟一:創建一個controller類並定義方法

public class UserController {
    /**
     * 查詢用戶
     */
    public Object queryUser(String name, Integer age) {
        System.out.println("數據查詢中模擬...........");
        return "張無忌,18歲";
    }
}
View Code

步驟二:創建一個增強類,編寫增強邏輯,也就是說建立一個切面

public class LogAop {
    public void method01() {
        System.out.println("method01-前置通知-增強邏輯-------------");
    }

    public void method02() {
        System.out.println("method02-后置通知-增強邏輯-------------");
    }

    public void method03() {
        System.out.println("method03-環繞通知-增強邏輯-------------");
    }

    public void method04() {
        System.out.println("method04-異常通知-增強邏輯-------------");
    }

    public void method05() {
        System.out.println("method05-最終通知-增強邏輯-------------");
    }
}
View Code

AOP配置

步驟一:在 spring 配置文件中,開啟注解掃描

<?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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 開啟注解掃描 -->
    <context:component-scan base-package="com.ldp.aop"></context:component-scan>
</beans>
View Code

步驟二:增強類上加上注解,以及在方法加不同類型的通知

類上加注解

方法上加不同類型的通知

package com.ldp.aop;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-14 9:52
 * @Description: <p>
 * 前置通知(執行方法前執行,通常用作參數日志輸出、權限校驗等)
 * 后置通知(邏輯代碼執行完,准備執行return的代碼時通知,通常用作執行結果日志輸出、結果加密等)
 * 環繞通知(是前置通知和后置通知的綜合,方法執行前和方法執行后都要執行,通常用作方法性能統計、接口耗時、統一加密、解密等)
 * 異常通知(相當於try{}catch ()中catch執行的部分,程序拋出異常時執行,通常用作告警處理、事務回滾等)
 * 最終通知(相當於try{}catch (Exception e){}finally { }中的finally執行的部分,通常用在關閉資源、清理緩存等業務邏輯中)
 * </p>
 * 語法結構: execution([權限修飾符] [返回類型] [類全路徑] [方法名稱]([參數列表]) )
 */
@Component
@Aspect
public class LogAop {
    /**
     * 前置通知
     */
    @Before(value = "execution(* com.ldp.controller.*Controller.*(..))")
    public void method01() {
        System.out.println("method01-前置通知-增強邏輯-------------");
    }

    /**
     * 后置通知
     */
    @AfterReturning(value = "execution(* com.ldp.controller.*Controller.*(..))")
    public void method02() {
        System.out.println("method02-后置通知-增強邏輯-------------");
    }

    /**
     * 環繞通知
     */
    @Around(value = "execution(* com.ldp.controller.*Controller.*(..))")
    public void method03() {
        System.out.println("method03-環繞通知-增強邏輯-------------");
    }

    /**
     * 異常通知
     */
    @AfterThrowing(value = "execution(* com.ldp.controller.*Controller.*(..))")
    public void method04() {
        System.out.println("method04-異常通知-增強邏輯-------------");
    }

    /**
     * 最終通知
     */
    @After(value = "execution(* com.ldp.controller.*Controller.*(..))")
    public void method05() {
        System.out.println("method05-最終通知-增強邏輯-------------");
    }
}
View Code

步驟三:controller類上加上注解

步驟四:測試

package com.ldp.test;

import com.ldp.controller.UserController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-14 10:19
 * @Description:
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:bean01.xml")
public class TestAop {
    @Autowired
    private UserController userController;

    /**
     * 測試
     */
    @Test
    public void test01() {
        Object queryUser = userController.queryUser("張無忌", 18);
        System.out.println("queryUser=" + queryUser);
    }
}
View Code

注意:當前測試需要關閉環繞通知

當沒有異常的時候,執行結果

method01- @Before-前置通知-增強邏輯-------------
數據查詢中模擬...........
method02- @AfterReturning-后置通知-增強邏輯-------------
method05- @After-最終通知-增強邏輯-------------
queryUser=張無忌,18歲

當有異常的時候

method01- @Before-前置通知-增強邏輯-------------
數據查詢中模擬...........
method04- @AfterThrowing-異常通知-增強邏輯-------------
method05- @After-最終通知-增強邏輯-------------

6.2.XML實現AOP配置(現在很少使用,了解)

步驟一:創建一個普通的增強類和增強方法

public class LogAop03 {
    public void method01() {
        System.out.println("-----xml的方式-----------");
    }
}

步驟二:xml中配置aop

<?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.xsd
       http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 開啟注解掃描 -->
    <context:component-scan base-package="com.ldp.*"></context:component-scan>
    <!-- 開啟 Aspect 生成代理對象-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    <bean id="logAop03" class="com.ldp.aop.LogAop03"></bean>
    <!--配置 aop 增強-->
    <aop:config>
        <!--切入點-->
        <aop:pointcut id="p" expression="execution(* com.ldp.controller.*Controller.*(..))"/>
        <!--配置切面-->
        <aop:aspect ref="logAop03">
            <!--增強作用在具體的方法上-->
            <aop:before method="method01" pointcut-ref="p"/>
        </aop:aspect>
    </aop:config>
</beans>
View Code

步驟三:測試

測試與之前的一樣,略。

7.AOP優化與擴展

1.抽取相同的切入點

 2.同一個通知有多個切入點

 3.同一個方法有多個通知

使用@Order(數字) 排序

 4.使用全注解開發

刪除之前的配置文件,添加一個Aop的配置對象

@Configuration
@ComponentScan(basePackages = {"com.ldp"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {

}
View Code

測試

8.ProceedingJoinPoint 與 JoinPoint 的使用

二者的關系

 作用:

1、用來獲取被增強方法的參數、方法名、權限命名、注解等,實際上能獲取到這些在結合反射可以實現很強大的功能;

2、在環繞通知時讓代理執行方法, proceedingJoinPoint.proceed();

上面的日志實際實現案例

案例一:不使用環繞通知的情況

package com.ldp.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-14 9:52
 * @Description: <p>
 */
@Component
@Aspect
public class LogAop04 {
    /**
     * 抽取相同的切入點
     */
    @Pointcut(value = "execution(* com.ldp.controller.*Controller.*(..))")
    public void methodPointcut01() {
    }

    /**
     * 前置通知
     */
    @Before(value = "methodPointcut01()")
    public void method01(JoinPoint joinPoint) {
        // 獲取請求參數
        Object[] args = joinPoint.getArgs();
        Signature pointSignature = joinPoint.getSignature();
        // 獲取包+類名
        String declaringTypeName = pointSignature.getDeclaringTypeName();
        // 獲取方法名
        String name = pointSignature.getName();
        System.out.println("參數:" + Arrays.toString(args));
        System.out.println("類權限命名:" + declaringTypeName);
        System.out.println("方法名:" + name);
    }

    /**
     * 后置通知
     */
    @AfterReturning(returning = "result", value = "methodPointcut01()")
    public void method02(Object result) {
        System.out.println("方法執行結果:" + result);
    }


    /**
     * 異常通知
     */
    @AfterThrowing(throwing = "ex", value = "methodPointcut01()")
    public void method04(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("連接點方法為:" + methodName + ",參數為:" + args + ",異常為:" + ex.getMessage());
    }

    /**
     * 最終通知
     */
    @After(value = "methodPointcut01()")
    public void method05() {
        System.out.println("method05- @After-最終通知-增強邏輯-------------");
    }
}
View Code

案例二:使用環繞通知的情況

package com.ldp.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * @Copyright (C) XXXXXXXXXXX科技股份技有限公司
 * @Author: lidongping
 * @Date: 2021-01-14 9:52
 * @Description: <p>
 */
@Component
@Aspect
public class LogAop04Around {
    /**
     * 環繞通知
     */
    @Around(value = "execution(* com.ldp.controller.*Controller.*(..))")
    public Object method03(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 獲取開始時間
        long start = System.currentTimeMillis();

        System.out.println("方法參數:" + Arrays.toString(proceedingJoinPoint.getArgs()));
        //讓代理方法執行
        Object result = proceedingJoinPoint.proceed();

        // 獲取結束執行時間
        long end = System.currentTimeMillis();

        System.out.println("方法返回:" + result);
        System.out.println("方法執行時間:" + (end - start) + " millisecond");
        return result;
    }

}
View Code

測試與上面一樣略!

完美!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM