主頁 > 後端開發 > AOP:面向切片編程

AOP:面向切片編程

2021-09-07 06:40:35 後端開發

AOP:面向切片編程

簡介

AOP解決的問題:將核心業務代碼與外圍業務(日志記錄、權限校驗、例外處理、事務控制)代碼分離出來,提高模塊化,降低代碼耦合度,使職責更單一,

AOP應用場景:

日志記錄、權限校驗、例外處理、事務控制等

相關概念

img

joinPoint:連接點,在spring中只支持方法連接點,連接點指的是可以使用advice(增強)的地方,例如一個類中有5個方法,那么這5個方法,那么這5個方法都可以是連接點,

pointcut:切點,可理解為實實在在的連接點,即切入advice(增強)的點,例如一個類中有5個方法,其中有3個方法(連接點)需要織入advice(增強),那么這3個需要織入advice的連接點就是切點,

advice:增強,實際中想要添加的功能,如日志、權限校驗,

before:前置增強,目標方法執行前之前執行,

after:后置增強,目標方法執行后執行,

around:環繞增強,在目標方法執行時執行,可控制目標方法是否執行,

after throwing:例外增強,目標方法拋出例外時執行,

weaving:織入,即對方法的增強,將切面的代碼織入(應用)到目標函式的程序,

introduction advice:引入增強,即對類的增強,

advisor:切面,由切點和增強相結合而成,定義增強應用到哪些切點上,

AOP的兩種實作:基于代理的AOP實作&&Aspectj Spring AOP

Spring aop

基于代理(jdk動態代理、cglib動態代理)實作的aop

Spring aop使用了兩種代理機制,一種是jdk動態代理,另一種是cglib動態代理,

Jdk動態代理只支持介面代理,cglib支持類的代理,

編程式實作

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>springboot-practice-2021-9-5</artifactId>
<version>1.0-SNAPSHOT</version>
  
  
<properties>
    <spring.version>4.1.7.RELEASE</spring.version>
</properties>
  
<dependencies>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.2.0</version>
    </dependency>
  
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>

</dependencies>
  
</project>
編程式

前置增強 before advice

前置增強:實作MethodBeforeAdvice介面,執行目標方法前執行before方法

import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;

public class UserBeforeAdvice implements MethodBeforeAdvice {

    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("Before");
    }

}

后置增強 after advice

后置增強:實作AfterReturningAdvice介面,執行目標方法后執行afterReturning方法

import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;

public class UserAfterAdvice implements AfterReturningAdvice {
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("After");
    }
}

環繞增強 around advice

環繞增強:實作MethodInterceptor介面,執行目標方法前后執行invoke方法

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class UserAroundAdvice implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before");
        Object result = invocation.proceed();
        System.out.println("After");
        return result;
    }
}

用戶服務介面類:

public interface UserService { 
    void queryAll();
}

用戶服務介面實作類:

public class UserServiceImpl implements UserService {
    public void queryAll() {
        System.out.println("查詢全部用戶并回傳");
    }
}

測驗類

import com.liuermeng.aop.advice.UserAfterAdvice;
import com.liuermeng.aop.advice.UserAroundAdvice;
import com.liuermeng.aop.advice.UserBeforeAdvice;
import com.liuermeng.aop.service.IUserService;
import com.liuermeng.aop.service.impl.UserServiceImpl;
import org.springframework.aop.framework.ProxyFactory;

public class Test {
    public static void main(String[] args) {
        /**
         * 測驗前置增強和后置增強
         */
        ProxyFactory proxyFactory = new ProxyFactory();//創建代理工廠
        proxyFactory.setTarget(new UserServiceImpl());//射入目標類物件
        proxyFactory.addAdvice(new UserBeforeAdvice());//添加前置增強
        proxyFactory.addAdvice(new UserAfterAdvice());//添加后置增強
        IUserService userService = (IUserService) proxyFactory.getProxy();//從代理工廠獲取代理
        userService.queryAll();//呼叫代理的方法

        /**
         * 測驗環繞增強
         */
        ProxyFactory proxyFactory2 = new ProxyFactory();//創建代理工廠
        proxyFactory2.setTarget(new UserServiceImpl());//射入目標類物件
        proxyFactory2.addAdvice(new UserAroundAdvice());//添加環繞增強
        IUserService userService2 = (IUserService) proxyFactory2.getProxy();//從代理工廠獲取代理
        userService2.queryAll();//呼叫代理的方法

    }
}

結果

Before
查詢全部用戶并回傳
After
Before
查詢全部用戶并回傳
After
宣告式(基于xml)
環繞增強 around advice

spring-aop.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: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
      http://www.springframework.org/schema/context/spring-context.xsd">

    <!--掃描指定包-->
    <context:component-scan base-package="com.liuermeng.aop"/>

    <!--配置代理-->
    <bean id="userServiceProxy" >
        <!--需要代理的介面-->
        <property name="interfaces" value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/com.liuermeng.aop.service.IUserService"/>
        <!--介面實作類-->
        <property name="target" ref="userServiceImpl"/>
        <!--攔截器名稱(即增強類名稱)-->
        <property name="interceptorNames">
            <list>
                <value>userAroundAdvice</value>
            </list>
        </property>

    </bean>

</beans>

其他的代碼除了測驗類以外與上面相同,另外在aroundAdvice類上面加上@Component標簽

測驗環繞增強代碼:

import com.liuermeng.aop.service.IUserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {

    public static void main(String[] args) {
        // 獲取spring context
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("aop2.xml");
        // 從context中獲取id為userServiceProxy的代理物件
        IUserService userServiceProxy = (IUserService) applicationContext.getBean("userServiceProxy");
        // 呼叫代理的方法
        userServiceProxy.queryAll();

    }
}

結果

Before
查詢全部用戶并回傳
After
拋出增強 throws advice

拋出增強:實作ThrowsAdvice介面,當執行目標方法出現例外會執行拋出增強中的afterThrowing方法,

/**
 * 拋出增強
 */
@Component
public class UserThrowAdvice implements ThrowsAdvice {
 
    public void afterThrowing(Method method, Object[] args, Object target, Exception e) {
        System.out.println("Throw exception:");
        System.out.println("Target class name:" + target.getClass().getName());
        System.out.println("Method name: " + method.getName());
        System.out.println("Exception message:" + e.getMessage());
    }
}

.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: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
      http://www.springframework.org/schema/context/spring-context.xsd">

    <!--掃描指定包-->
    <context:component-scan base-package="com.liuermeng.aop"/>

    <!--配置代理-->
    <bean id="userServiceProxy" >
        <!--需要代理的介面-->
        <property name="interfaces" value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/com.liuermeng.aop.service.IUserService"/>
        <!--介面實作類-->
        <property name="target" ref="userServiceImpl"/>
        <!--攔截器名稱(即增強類名稱)-->
        <property name="interceptorNames">
            <list>
<!--                <value>userAroundAdvice</value>-->
                <value>userThrowAdvice</value>
            </list>
        </property>

    </bean>

</beans>

測驗代碼同上

結果

查詢全部用戶并回傳
Throw exception:
Target class name:com.liuermeng.aop.service.impl.UserServiceImpl
Method name: queryAll
Exception message:Error哈哈哈哈
Exception in thread "main" java.lang.RuntimeException: Error哈哈哈哈
	at com.liuermeng.aop.service.impl.UserServiceImpl.queryAll(UserServiceImpl.java:10)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
	at org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor.invoke(ThrowsAdviceInterceptor.java:125)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
	at com.sun.proxy.$Proxy6.queryAll(Unknown Source)
	at com.liuermeng.aop.Test.main(Test.java:15)
切面 advisor

advisor(切面)封裝了advice(增強)和pointcut(切點)

在UserService介面中添加兩個方法query、save.

UserService代碼如下:

public interface IUserService { 

    void queryAll(); 

    void query(); 

    void save();

}

UserServiceImpl代碼如下:

@Component

public class UserServiceImpl implements UserService { 

    @Override

    public void queryAll() {

        System.out.println("查詢全部用戶并回傳");

    }
 

    @Override

    public void query() {

        System.out.println("根據條件查詢用戶");

    }
 

    @Override

    public void save() {

        System.out.println("新增用戶");

    }
 

}

.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: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
      http://www.springframework.org/schema/context/spring-context.xsd">

    <!--掃描指定包-->
    <context:component-scan base-package="com.liuermeng.aop"/>

    <!-- 配置一個切面 -->
    <bean id="userServiceAdvisor" >
        <!-- 增強 -->
        <property name="advice" ref="userAroundAdvice"/>
        <!-- 切點 -->
        <property name="pattern" value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/com.liuermeng.aop.service.impl.UserServiceImpl.query.*"/>

    </bean>

    <!--配置代理-->
    <bean id="userServiceProxy" >
        <!--需要代理的介面-->
        <property name="interfaces" value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/com.liuermeng.aop.service.IUserService"/>
        <!--介面實作類-->
        <property name="target" ref="userServiceImpl"/>
        <!-- qiemian -->
        <property name="interceptorNames" value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/userServiceAdvisor"/>
        <!-- 代理目標類 -->
        <property name="proxyTargetClass" value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/true"/>

    </bean>

</beans>

測驗代碼:

public class Test {
 

    public static void main(String[] args) {

        // 獲取spring context

        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop.xml");

        // 從context中獲取id為userServiceProxy的代理物件

        UserService userServiceProxy = (UserService) applicationContext.getBean("userServiceProxy");

        // 呼叫代理的方法

        userServiceProxy.queryAll();

        userServiceProxy.query();

        userServiceProxy.save();

 

    }

}

結果如下:

Before
查詢全部用戶并回傳
After
Before
根據條件查詢用戶
After
新增用戶

可以看出UserService中的queryAll、query方法被攔截,執行這兩個方法前后執行了環繞增強代碼,而save方法沒有被攔截,

基于Aspect Spring AOP開發

Spring AOP 與ApectJ 的目的一致,都是為了統一處理橫切業務,但與AspectJ不同的是,Spring AOP 并不嘗試提供完整的AOP功能(即使它完全可以實作),Spring AOP 更注重的是與Spring IOC容器的結合,并結合該優勢來解決橫切業務的問題,因此在AOP的功能完善方面,相對來說AspectJ具有更大的優勢,同時,Spring注意到AspectJ在AOP的實作方式上依賴于特殊編譯器(ajc編譯器),因此Spring很機智回避了這點,轉向采用動態代理技術的實作原理來構建Spring AOP的內部機制(動態織入),這是與AspectJ(靜態織入)最根本的區別,在AspectJ 1.5后,引入@Aspect形式的注解風格的開發,Spring也非常快地跟進了這種方式,因此Spring 2.0后便使用了與AspectJ一樣的注解,請注意,Spring 只是使用了與 AspectJ 5 一樣的注解,但仍然沒有使用 AspectJ 的編譯器,底層依是動態代理技術的實作,因此并不依賴于 AspectJ 的編譯器,下面我們先通過一個簡單的案例來演示Spring AOP的入門程式

基于注解

簡單案例

pom中增加了Spring與junit相關的依賴

aspect注解定義的切面類UserAdvisor

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

@Component
@Aspect
public class UserAdvisor {
    /**
     * 環繞通知
     * @param joinPoint 可用于執行切點的類
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
    public Object arount(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("環繞通知前....");
        Object obj= (Object) joinPoint.proceed();
        System.out.println("環繞通知后....");
        return obj;
    }

    /**
     * 前置通知
     */
    @Before("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
    public void before() {
        System.out.println("前置通知");
    }

    /**
     * 后置通知
     * @param returnVal
     */
    @AfterReturning(value = "https://www.cnblogs.com/liuermeng/archive/2021/09/06/execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))",returning = "returnVal")
    public void adterReturn(Object returnVal){
        System.out.println("后置通知...."+returnVal);
    }

    /**
     * 最終通知
     */
    @After("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
    public void after() {
        System.out.println("最終通知");
    }
    /**
     * 拋出通知
     * @param e
     */
    @AfterThrowing(value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))",throwing = "e")
    public void afterThrowable(Throwable e){
        System.out.println("出現例外:msg="+e.getMessage());
    }

}

IUserService

public interface IUserService {
    int queryAll();
    void query();

    void save();
}

實作類

import com.liuermeng.aop.service.IUserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements IUserService {
    public int queryAll() {
        System.out.println("查詢全部用戶并回傳");
//        throw new RuntimeException("Error哈哈哈哈");
        return 666;
    }
    public void query() {

        System.out.println("根據條件查詢用戶");

    }

    public void save() {

        System.out.println("新增用戶");

    }
}

組態檔.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: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
      http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:component-scan base-package="com.liuermeng.aop"/>
    <!-- 啟動@aspectj的自動代理支持-->
    <aop:aspectj-autoproxy />
</beans>

測驗類

import com.liuermeng.aop.service.IUserService;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:aop3.xml")
public class Test {
    @Autowired
    IUserService userService ;
    @org.junit.Test
    public void test(){
        userService.queryAll();

    }
}

結果

環繞通知前....
前置通知
查詢全部用戶并回傳
環繞通知后....
最終通知
后置通知....666

利用Spring2.0引入的aspect注解開發功能定義aspect類即UserAdvisor,在該aspect類中,撰寫了5種注解型別的通知函式,分別是前置通知@Before、后置通知@AfterReturning、環繞通知@Around、例外通知@AfterThrowing、最終通知@After,這5種通知與AspectJ的通知型別幾乎是一樣的,并在注解通知上使用execution關鍵字定義的切點運算式,即指明該通知要應用的目標函式,當只有一個execution引數時,value屬性可以省略,當含兩個以上的引數,value必須注明,如存在回傳值時,當然除了把切點運算式直接傳遞給通知注解型別外,還可以使用@pointcut來定義切點匹配運算式,這個與AspectJ使用關鍵字pointcut是一樣的,后面分析,目標類和aspect類定義完成后,最后需要在xml組態檔中進行配置,注意,使用Spring AOP 的aspectJ功能時,需要使用以下代碼啟動aspect的注解功能支持:

<aop:aspectj-autoproxy />
定義切入點運算式

在上面的案例中是采用直接在注解中使用“execution”定義運算式作為值傳遞給通知型別的,如下:

 /**
     * 前置通知
     */
    @Before("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
    public void before() {
        System.out.println("前置通知");
    }

除了上述方式外,還可采用與ApectJ中使用pointcut關鍵字類似的方式定義切入點運算式如下,使用@Pointcut注解:

/**
 * 使用Pointcut定義切點
 */
@Pointcut("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
public void myPointcut(){}
 /**
  * 前置通知
  */
@Before("myPointcut()")
public void before() {
    System.out.println("前置通知");
}
切入點指示符

為了方法通知應用到相應過濾的目標方法上,SpringAOP提供了匹配運算式,這些運算式也叫切入點指示符,在前面的案例中,它們已多次出現,

通配符
在定義匹配運算式時,通配符幾乎隨處可見,如*、.. 、+ ,它們的含義如下:

.. :匹配方法定義中的任意數量的引數,此外還匹配類定義中的任意數量包

//任意回傳值,任意名稱,任意引數的公共方法
execution(public * *(..))
//匹配com.zejian.dao包及其子包中所有類中的所有方法
within(com.zejian.dao..*)

+ :匹配給定類的任意子類

//匹配實作了DaoUser介面的所有子類的方法
within(com.zejian.dao.DaoUser+)

* :匹配任意數量的字符

//匹配com.zejian.service包及其子包中所有類的所有方法
within(com.zejian.service..*)
//匹配以set開頭,引數為int型別,任意回傳值的方法
execution(* set*(int))

型別簽名運算式
為了方便型別(如介面、類名、包名)過濾方法,Spring AOP 提供了within關鍵字,其語法格式如下:

within(<type name>)

type name 則使用包名或者類名替換即可,來點案例吧,

//匹配com.zejian.dao包及其子包中所有類中的所有方法
@Pointcut("within(com.zejian.dao..*)")

//匹配UserDaoImpl類中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl)")

//匹配UserDaoImpl類及其子類中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl+)")

//匹配所有實作UserDao介面的類的所有方法
@Pointcut("within(com.zejian.dao.UserDao+)")

方法簽名運算式
如果想根據方法簽名進行過濾,關鍵字execution可以幫到我們,語法運算式如下

//scope :方法作用域,如public,private,protect
//returnt-type:方法回傳值型別
//fully-qualified-class-name:方法所在類的完全限定名稱
//parameters 方法引數
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))

對于給定的作用域、回傳值型別、完全限定類名以及引數匹配的方法將會應用切點函式指定的通知,這里給出模型案例:

//匹配UserDaoImpl類中的所有方法
@Pointcut("execution(* com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl類中的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl類中的所有公共方法并且回傳值為int型別
@Pointcut("execution(public int com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl類中第一個引數為int型別的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(int , ..))")

其他指示符
bean:Spring AOP擴展的,AspectJ沒有對于指示符,用于匹配特定名稱的Bean物件的執行方法;

//匹配名稱中帶有后綴Service的Bean,
@Pointcut("bean(*Service)")
private void myPointcut1(){}

this :用于匹配當前AOP代理物件型別的執行方法;請注意是AOP代理物件的型別匹配,這樣就可能包括引入介面也型別匹配

//匹配了任意實作了UserDao介面的代理物件的方法進行過濾
@Pointcut("this(com.zejian.spring.springAop.dao.UserDao)")
private void myPointcut2(){}

target :用于匹配當前目標物件型別的執行方法;

//匹配了任意實作了UserDao介面的目標物件的方法進行過濾
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao)")
private void myPointcut3(){}

@within:用于匹配所以持有指定注解型別內的方法;請注意與within是有區別的, within是用于匹配指定型別內的方法執行;

//匹配使用了MarkerAnnotation注解的類(注意是類)
@Pointcut("@within(com.zejian.spring.annotation.MarkerAnnotation)")
private void myPointcut4(){}

@annotation(com.zejian.spring.MarkerMethodAnnotation) : 根據所應用的注解進行方法過濾

//匹配使用了MarkerAnnotation注解的方法(注意是方法)
@Pointcut("@annotation(com.zejian.spring.annotation.MarkerAnnotation)")
private void myPointcut5(){}

ok~,關于運算式指示符就介紹到這,我們主要關心前面幾個常用的即可,不常用過印象即可,這里最后說明一點,切點指示符可以使用運算子語法進行運算式的混編,如and、or、not(或者&&、||、!),如下一個簡單例子:

//匹配了任意實作了UserDao介面的目標物件的方法并且該介面不在com.zejian.dao包及其子包下
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao) !within(com.zejian.dao..*)")
private void myPointcut6(){}
//匹配了任意實作了UserDao介面的目標物件的方法并且該方法名稱為addUser
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao)&&execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut7(){}
通知函式以及傳遞引數

在Spring中與AspectJ一樣,通知主要分5種型別,分別是前置通知、后置通知、例外通知、最終通知以及環繞通知,下面分別介紹,

  • 前置通知@Before

    前置通知通過@Before注解進行標注,并可直接傳入切點運算式的值,該通知在目標函式執行前執行,注意JoinPoint,是Spring提供的靜態變數,通過joinPoint 引數,可以獲取目標物件的資訊,如類名稱,方法引數,方法名稱等,該引數是可選的,

/**
 * 前置通知
 */
@Before("myPointcut()")
public void before(JoinPoint joinPoint) {
    System.out.println("前置通知");
}
  • 后置通知@AfterReturning

    通過@AfterReturning注解進行標注,該函式在目標函式執行完成后執行,并可以獲取到目標函式最終的回傳值returnVal,當目標函式沒有回傳值時,returnVal將回傳null,必須通過returning = “returnVal”注明引數的名稱而且必須與通知函式的引數名稱相同,請注意,在任何通知中這些引數都是可選的,需要使用時直接填寫即可,不需要使用時,可以完成不用宣告出來,如下

/**
* 后置通知,不需要引數時可以不提供
*/
@AfterReturning("myPointcut()")
public void AfterReturning(){
   System.out.println("我是后置通知...");
}
/**
* 后置通知
* returnVal,切點方法執行后的回傳值
*/
@AfterReturning(value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/myPointcut()",returning = "returnVal")
public void AfterReturning(JoinPoint joinPoint,Object returnVal){
   System.out.println("我是后置通知...returnVal+"+returnVal);
}
  • 例外通知 @AfterThrowing

    該通知只有在例外時才會被觸發,并由throwing來宣告一個接收例外資訊的變數,同樣例外通知也用于Joinpoint引數,需要時加上即可,如下:

/**
* 拋出通知
* @param e 拋出例外的資訊
*/
@AfterThrowing(value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/myPointcut()",throwing = "e")
public void afterThrowable(Throwable e){
  System.out.println("出現例外:msg="+e.getMessage());
}

  • 最終通知 @After

    該通知有點類似于finally代碼塊,只要應用了無論什么情況下都會執行,

/**
 * 無論什么情況下都會執行的方法
 * joinPoint 引數
 */
@After("myPointcut()")
public void after(JoinPoint joinPoint) {
    System.out.println("最終通知....");
}
  • 環繞通知@Around

    環繞通知既可以在目標方法前執行也可在目標方法之后執行,更重要的是環繞通知可以控制目標方法是否執行,但即使如此,我們應該盡量以最簡單的方式滿足需求,在僅需在目標方法前執行時,應該采用前置通知而非環繞通知,案例代碼如下第一個引數必須是ProceedingJoinPoint,通過該物件的proceed()方法來執行目標函式,proceed()的回傳值就是環繞通知的回傳值,同樣的,ProceedingJoinPoint物件也是可以獲取目標物件的資訊,如類名稱,方法引數,方法名稱等等,

@Around("myPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("我是環繞通知前....");
    //執行目標函式
    Object obj= (Object) joinPoint.proceed();
    System.out.println("我是環繞通知后....");
    return obj;
}
通知傳遞引數

在Spring AOP中,除了execution和bean指示符不能傳遞引數給通知方法,其他指示符都可以將匹配的方法相應引數或物件自動傳遞給通知方法,獲取到匹配的方法引數后通過”argNames”屬性指定引數名,如下,需要注意的是args(指示符)、argNames的引數名與before()方法中引數名 必須保持一致即param,

@Before(value="https://www.cnblogs.com/liuermeng/archive/2021/09/06/args(param)", argNames="param") //明確指定了    
public void before(int param) {    
    System.out.println("param:" + param);    
}  

當然也可以直接使用args指示符不帶argNames宣告引數,如下:

@Before("execution(public * com.zejian..*.addUser(..)) && args(userId,..)")  
public void before(int userId) {  
    //呼叫addUser的方法時如果與addUser的引數匹配則會傳遞進來會傳遞進來
    System.out.println("userId:" + userId);  
}  

args(userId,..)該運算式會保證只匹配那些至少接收一個引數而且傳入的型別必須與userId一致的方法,記住傳遞的引數可以簡單型別或者物件,而且只有引數和目標方法也匹配時才有會有值傳遞進來,

Aspect優先級

在不同的切面中,如果有多個通知需要在同一個切點函式指定的過濾目標方法上執行,那些在目標方法前執行(”進入”)的通知函式,最高優先級的通知將會先執行,在執行在目標方法后執行(“退出”)的通知函式,最高優先級會最后執行,而對于在同一個切面定義的通知函式將會根據在類中的宣告順序執行,如下:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AspectOne {
    /**
     * 使用Pointcut定義切點
     */
    @Pointcut("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
    public void myPointcut(){}
    /**
     * 前置通知
     */
    @Before("myPointcut()")
    public void beforeone(JoinPoint joinPoint) {
        System.out.println("前置通知1......");
    }
    @Before("myPointcut()")
    public void beforetwo(JoinPoint joinPoint) {
        System.out.println("前置通知2......");
    }

    /**
     * 后置通知
     * @param
     */
    @AfterReturning(value = "https://www.cnblogs.com/liuermeng/archive/2021/09/06/myPointcut()")
    public void adterReturnthree(){
        System.out.println("后置通知3....");
    }
    @AfterReturning(value = "https://www.cnblogs.com/liuermeng/archive/2021/09/06/myPointcut()")
    public void adterReturnfour(){
        System.out.println("后置通知4....");
    }
}

結果

前置通知1......
前置通知2......
查詢全部用戶并回傳
后置通知4....
后置通知3....

如果在不同的切面中定義多個通知回應同一個切點,進入時則優先級高的切面類中的通知函式優先執行,退出時則最后執行,如下定義AspectOne類和AspectTwo類并實作org.springframework.core.Ordered 介面,該介面用于控制切面類的優先級,同時重寫getOrder方法,定制回傳值,回傳值(int 型別)越小優先級越大,其中AspectOne回傳值為0,AspectTwo的回傳值為3,顯然AspectOne優先級高于AspectTwo,

package com.liuermeng.aop.advisor;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AspectOne implements Ordered {
    /**
     * 使用Pointcut定義切點
     */
    @Pointcut("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
    public void myPointcut(){}
    /**
     * 前置通知
     */
    @Before("myPointcut()")
    public void beforeone(JoinPoint joinPoint) {
        System.out.println("AspectOne前置通知1......");
    }
    @Before("myPointcut()")
    public void beforetwo(JoinPoint joinPoint) {
        System.out.println("AspectOne前置通知2......");
    }

    /**
     * 后置通知
     * @param
     */
    @AfterReturning(value = "https://www.cnblogs.com/liuermeng/archive/2021/09/06/myPointcut()")
    public void adterReturnthree(){
        System.out.println("AspectOne后置通知3....");
    }
    @AfterReturning(value = "https://www.cnblogs.com/liuermeng/archive/2021/09/06/myPointcut()")
    public void adterReturnfour(){
        System.out.println("AspectOne后置通知4....");
    }

    public int getOrder() {
        return 0;
    }
}
package com.liuermeng.aop.advisor;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AspectTow {
    /**
     * 使用Pointcut定義切點
     */
    @Pointcut("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
    public void myPointcut(){}
    /**
     * 前置通知
     */
    @Before("myPointcut()")
    public void beforeone(JoinPoint joinPoint) {
        System.out.println("AspectTwo前置通知1......");
    }
    @Before("myPointcut()")
    public void beforetwo(JoinPoint joinPoint) {
        System.out.println("AspectTwo前置通知2......");
    }

    /**
     * 后置通知
     * @param
     */
    @AfterReturning(value = "https://www.cnblogs.com/liuermeng/archive/2021/09/06/myPointcut()")
    public void adterReturnthree(){
        System.out.println("AspectTwo后置通知3....");
    }
    @AfterReturning(value = "https://www.cnblogs.com/liuermeng/archive/2021/09/06/myPointcut()")
    public void adterReturnfour(){
        System.out.println("AspectTwo后置通知4....");
    }

    public int getOrder() {
        return 3;
    }
}

結果

AspectOne前置通知1......
AspectOne前置通知2......
AspectTwo前置通知1......
AspectTwo前置通知2......
查詢全部用戶并回傳
AspectTwo后置通知4....
AspectTwo后置通知3....
AspectOne后置通知4....
AspectOne后置通知3....


基于XML

前面分析完基于注解支持的開發是日常應用中最常見的,即使如此我們還是有必要了解一下基于xml形式的Spring AOP開發,這里會以一個案例的形式對xml的開發形式進行簡要分析,定義一個切面類

package com.liuermeng.aop.advisor;

import org.aspectj.lang.ProceedingJoinPoint;

public class XmlAspect {
    public void before(){
        System.out.println("MyAspectXML====前置通知");
    }

    public void afterReturn(Object returnVal){
        System.out.println("后置通知-->回傳值:"+returnVal);
    }

    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("MyAspectXML=====環繞通知前");
        Object object= joinPoint.proceed();
        System.out.println("MyAspectXML=====環繞通知后");
        return object;
    }

    public void afterThrowing(Throwable throwable){
        System.out.println("MyAspectXML======例外通知:"+ throwable.getMessage());
    }

    public void after(){
        System.out.println("MyAspectXML=====最終通知..來了");
    }

}

.xml

<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 http://www.springframework.org/schema/context/spring-context.xsd">
    <bean name="userService" />
    <!-- 定義切面 -->
    <bean name="xmlAspect" />
    <!-- 配置AOP切面 -->
    <aop:config>
        <!-- 定義切點函式 -->
        <aop:pointcut id="pointCut" expression="execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))"/>
        <!-- 定義其他切點函式 -->
        <aop:pointcut id="pointCutSave" expression="execution(* com.liuermeng.aop.service.impl.UserServiceImpl.save(..))"/>
        <!-- 定義通知 order 定義優先級,值越小優先級越大-->
        <aop:aspect ref="xmlAspect" order="0">
            <!-- 定義通知
            method 指定通知方法名,必須與MyAspectXML中的相同
            pointcut 指定切點函式
            -->
            <aop:before method="before" pointcut-ref="pointCut"/>
            <!-- 后置通知  returning="returnVal" 定義回傳值 必須與類中宣告的名稱一樣-->
            <aop:after-returning method="afterReturn" pointcut-ref="pointCut"  returning="returnVal" />
            <!-- 環繞通知 -->
            <aop:around method="around" pointcut-ref="pointCut"  />
            <!--例外通知 throwing="throwable" 指定例外通知錯誤資訊變數,必須與類中宣告的名稱一樣-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointCut" throwing="throwable"/>
            <!--
                 method : 通知的方法(最終通知)
                 pointcut-ref : 通知應用到的切點方法
                -->
            <aop:after method="after" pointcut-ref="pointCut"/>
        </aop:aspect>
    </aop:config>

</beans>

Test

import com.liuermeng.aop.service.IUserService;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:aop.xml")
public class Test {
    @Autowired
    IUserService userService ;
    @org.junit.Test
    public void test(){
        userService.queryAll();

    }
}

結果

MyAspectXML====前置通知
MyAspectXML=====環繞通知前
查詢全部用戶并回傳
MyAspectXML=====最終通知..來了
MyAspectXML=====環繞通知后
后置通知-->回傳值:666


需要注意的是最終通知和后置通知的位置

簡單應用

模擬性能監控,計算方法執行耗時,

package com.liuermeng.aop.advisor;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
 * 性能監控切面
 */
@Aspect
@Component
public class MonitorAspect {
    @Around("execution(* com.liuermeng.aop.service.impl.UserServiceImpl.*(..))")
    public Object arount(ProceedingJoinPoint pjp) throws Throwable{
        // 目標類名稱
        String clazzName = pjp.getTarget().getClass().getName();
        // 目標類方法名稱
        String methodName = pjp.getSignature().getName();
        // 計時并呼叫目標函式
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long time = System.currentTimeMillis() - start;

        System.out.println("執行" + clazzName + "." + methodName + "方法耗時" + time + "毫秒");
        // 可在此處將監控資訊存盤
        return result;
    }
}
package com.liuermeng.aop.service;

public interface IUserService {
    int queryAll();
    void query();
    void save();
}
package com.liuermeng.aop.service.impl;

import com.liuermeng.aop.service.IUserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements IUserService {
    public int queryAll() {
        System.out.println("查詢全部用戶并回傳");
//        throw new RuntimeException("Error哈哈哈哈");
        return 666;
    }
    public void query() {

        System.out.println("根據條件查詢用戶");

    }

    public void save() {

        System.out.println("新增用戶");

    }
}

.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: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
      http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/aop
      http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 掃描指定包 -->
    <context:component-scan base-package="com.liuermeng.aop"/>
    <!--默認為false,使用JDK動態代理 設定為true,啟用cglib動態代理-->
    <aop:aspectj-autoproxy proxy-target-/>
</beans>

測驗

package com.liuermeng.aop.test;

import com.liuermeng.aop.service.IUserService;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:aop.xml")
public class Test {
    @Autowired
    IUserService userService;//
    @org.junit.Test
    public void test(){
        userService.queryAll();
        userService.query();
        userService.save();
    }
}

注意:由于開啟了cglib動態代理,在測驗類中可以不注入IUserService,而選擇直接注入UserServiceImpl,UserServiceImpl也可以不用實作介面IUserService

Spring AOP實作原理概要

前面的分析中,我們談到Spring AOP的實作原理是基于動態織入的動態代理技術,而AspectJ則是靜態織入,而動態代理技術又分為Java JDK動態代理和CGLIB動態代理,前者是基于反射技術的實作,后者是基于繼承的機制實作,下面通過一個簡單的例子來分析這兩種技術的代碼實作,

JDK動態代理

先看一個簡單的例子,宣告一個A類并實作ExInterface介面,利用JDK動態代理技術在execute()執行前后織入權限驗證和日志記錄,注意這里僅是演示代碼并不代表實際應用,

//自定義的介面類,JDK動態代理的實作必須有對應的介面類
public interface ExInterface {
    void execute();
}
//A類,實作了ExInterface介面類
public class A implements ExInterface {
    @Override
    public void execute() {
        System.out.println("執行A的execute方法...");
    }
}
package com.liuermeng.proxy.aop;

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

//代理類的實作
public class JDKProxy implements InvocationHandler {
    /**
     * 要被代理的目標物件
     */
    private A target;
    public JDKProxy(A target){
        this.target=target;
    }
    /**
     * 創建代理類
     * @return
     */
    /**
     * 引數含義:
     * ClassLoader:和被代理物件使用相同的類加載器,
     * Interfaces:和被代理物件具有相同的行為,實作相同的介面,
     * InvocationHandler:如何代理,
     */
    public ExInterface createProxy(){
        return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }
    /**
     * 呼叫被代理類(目標物件)的任意方法都會觸發invoke方法
     * @param proxy 代理類
     * @param method 被代理類的方法
     * @param args 被代理類的方法引數
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //過濾不需要該業務的方法
        if("execute".equals(method.getName())) {
            //呼叫前驗證權限
            AuthCheck.authCheck();
            //呼叫目標物件的方法
            Object result = method.invoke(target, args);
            //記錄日志資料
            Report.recordLog();
            return result;
        }else if("delete".equals(method.getName())){
            //.....
        }
        //如果不需要增強直接執行原方法
        return method.invoke(target,args);
    }

    //測驗驗證
    public static void main(String args[]){
        A a=new A();
        //創建JDK代理
        JDKProxy jdkProxy=new JDKProxy(a);
        //創建代理物件
        ExInterface proxy=jdkProxy.createProxy();
        //執行代理物件方法
        proxy.execute();
    }


}

結果

AuthCheck權限驗證
執行A的execute方法...
Report記錄日志并上傳

在A的execute方法里面并沒有呼叫任何權限和日志的代碼,也沒有直接操作a物件,相反地只是呼叫了proxy代理物件的方法,最終結果卻是預期的,這就是動態代理技術,是不是跟Spring AOP似曾相識?實際上動態代理的底層是通過反射技術來實作,只要拿到A類的class檔案和A類的實作介面,很自然就可以生成相同介面的代理類并呼叫a物件的方法了,關于底層反射技術的實作,暫且不過多討論,請注意實作java的動態代理是有先決條件的,該條件是目標物件必須帶介面,如A類的介面是ExInterface,通過ExInterface介面動態代理技術便可以創建與A型別別相同的代理物件,如下代碼演示了創建并呼叫時利用多型生成的proxy物件:

A a=new A();
 //創建JDK代理類
 JDKProxy jdkProxy=new JDKProxy(a);
 //創建代理物件,代理物件也實作了ExInterface,通過Proxy實作
 ExInterface proxy=jdkProxy.createProxy();
 //執行代理物件方法
 proxy.execute();

代理物件的創建是通過Proxy類達到的,Proxy類由Java JDK提供,利用Proxy#newProxyInstance方法便可以動態生成代理物件(proxy),底層通過反射實作的,該方法需要3個引數

/**

* @param loader 類加載器,一般傳遞目標物件(A類即被代理的物件)的類加載器
* @param interfaces 目標物件(A)的實作介面
* @param h 回呼處理句柄(后面會分析到)
  */
  public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

創建代理類proxy的代碼如下:

public ExInterface createProxy(){
   return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }

到此并沒結束,因為有介面還是遠遠不夠,代理類(Demo中的JDKProxy)還需要實作InvocationHandler介面,也是由JDK提供,代理類必須實作的并重寫invoke方法,完全可以把InvocationHandler看成一個回呼函式(Callback),Proxy方法創建代理物件proxy后,當呼叫execute方法(代理物件也實作ExInterface)時,將會回呼InvocationHandler#invoke方法,因此我們可以在invoke方法中來控制被代理物件(目標物件)的方法執行,從而在該方法前后動態增加其他需要執行的業務,Demo中的代碼很好體現了這點:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //過濾不需要該業務的方法
    if("execute".equals(method.getName())) {
        //呼叫前驗證權限(動態添加其他要執行業務)
        AuthCheck.authCheck();

      //呼叫目標物件的方法(執行A物件即被代理物件的execute方法)
      Object result = method.invoke(target, args);

      //記錄日志資料(動態添加其他要執行業務)
      Report.recordLog();

      return result;
}eles if("delete".equals(method.getName())){
 //.....
 	return method.invoke(target, args);
}
//如果不需要增強直接執行原方法
	return method.invoke(target,args);
}

invoke方法有三個引數:

  • Object proxy :生成的代理物件
  • Method method:目標物件的方法,通過反射呼叫
  • Object[] args:目標物件方法的引數

這就是Java JDK動態代理的代碼實作程序,小結一下,運用JDK動態代理,被代理類(目標物件,如A類),必須已有實作介面如(ExInterface),因為JDK提供的Proxy類將通過目標物件的類加載器ClassLoader和Interface,以及句柄(Callback)創建與A類擁有相同介面的代理物件proxy,該代理物件將擁有介面ExInterface中的所有方法,同時代理類必須實作一個類似回呼函式的InvocationHandler介面并重寫該介面中的invoke方法,當呼叫proxy的每個方法(如案例中的proxy#execute())時,invoke方法將被呼叫,利用該特性,可以在invoke方法中對目標物件(被代理物件如A)方法執行的前后動態添加其他外圍業務操作,此時無需觸及目標物件的任何代碼,也就實作了外圍業務的操作與目標物件(被代理物件如A)完全解耦合的目的,當然缺點也很明顯需要擁有介面,這也就有了后來的CGLIB動態代理了

CGLIB動態代理

通過CGLIB動態代理實作上述功能并不要求目標物件擁有介面類,實際上CGLIB動態代理是通過繼承的方式實作的,因此可以減少沒必要的介面,下面直接通過簡單案例協助理解(CGLIB是一個開源專案,github網址是:https://github.com/cglib/cglib),

先引入maven依賴

<dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>3.2.0</version>
</dependency>
//被代理的類即目標物件
public class A {
    public void execute(){
        System.out.println("執行A的execute方法...");
    }
}
package cglib1;


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

import java.lang.reflect.Method;

//代理類
public class CGLibProxy implements MethodInterceptor {

    /**
     * 被代理的目標類
     */
    private A target;

    public CGLibProxy(A target) {
        super();
        this.target = target;
    }

    /**
     * 創建代理物件
     * @return
     */
    public A createProxy(){
        // 使用CGLIB生成代理:
        // 1.宣告增強類實體,用于生產代理類
        Enhancer enhancer = new Enhancer();
        // 2.設定被代理類位元組碼,CGLIB根據位元組碼生成被代理類的子類
        enhancer.setSuperclass(target.getClass());
        // 3.設定回呼函式,即一個方法攔截
        enhancer.setCallback(this);
        // 4.創建代理:
        return (A) enhancer.create();
    }

    /**
     * 回呼函式
     * @param proxy 代理物件
     * @param method 委托類方法
     * @param args 方法引數
     * @param methodProxy 每個被代理的方法都對應一個MethodProxy物件,
     *                    methodProxy.invokeSuper方法最終呼叫委托類(目標類)的原始方法
     * @return
     * @throws Throwable
     */

    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //過濾不需要該業務的方法
        if("execute".equals(method.getName())) {
            //呼叫前驗證權限(動態添加其他要執行業務)
            AuthCheck.authCheck();

            //呼叫目標物件的方法(執行A物件即被代理物件的execute方法)
            Object result = methodProxy.invokeSuper(proxy, args);

            //記錄日志資料(動態添加其他要執行業務)
            Report.recordLog();

            return result;
        }else if("delete".equals(method.getName())){
            //.....
            return methodProxy.invokeSuper(proxy, args);
        }
        //如果不需要增強直接執行原方法
        return methodProxy.invokeSuper(proxy, args);

    }



    public static void main(String[] args) {
        A a=new A();
        CGLibProxy proxy=new CGLibProxy(a);
        A proxy1 = proxy.createProxy();
        proxy1.execute();
    }
}

結果

AuthCheck權限驗證
執行A的execute方法...
Report記錄日志并上傳

從代碼看被代理的類無需介面即可實作動態代理,而CGLibProxy代理類需要實作一個方法攔截器介面MethodInterceptor并重寫intercept方法,類似JDK動態代理的InvocationHandler介面,也是理解為回呼函式,同理每次呼叫代理物件的方法時,intercept方法都會被呼叫,利用該方法便可以在運行時對方法執行前后進行動態增強,關于代理物件創建則通過Enhancer類來設定的,Enhancer是一個用于產生代理物件的類,作用類似JDK的Proxy類,因為CGLib底層是通過繼承實作的動態代理,因此需要傳遞目標物件(如A)的Class,同時需要設定一個回呼函式對呼叫方法進行攔截并進行相應處理,最后通過create()創建目標物件(如A)的代理物件,運行結果與前面的JDK動態代理效果相同,

public A createProxy(){
    // 1.宣告增強類實體,用于生產代理類
    Enhancer enhancer = new Enhancer();
    // 2.設定被代理類位元組碼,CGLIB根據位元組碼生成被代理類的子類
    enhancer.setSuperclass(target.getClass());
    // 3.設定回呼函式,即一個方法攔截
    enhancer.setCallback(this);
    // 4.創建代理:
    return (A) enhancer.create();
  }

關于JDK代理技術 和 CGLIB代理技術到這就講完了,我們也應該明白 Spring AOP 確實是通過 CGLIB或者JDK代理 來動態地生成代理物件,這個代理物件指的就是 AOP 代理累,而 AOP 代理類的方法則通過在目標物件的切入點動態地織入增強處理,從而完成了對目標方法的增強,這里并沒有非常深入去分析這兩種技術,更傾向于向大家演示Spring AOP 底層實作的最簡化的模型代碼,Spring AOP內部已都實作了這兩種技術,Spring AOP 在使用時機上也進行自動化調整,當有介面時會自動選擇JDK動態代理技術,如果沒有則選擇CGLIB技術,當然Spring AOP的底層實作并沒有這么簡單,為更簡便生成代理物件,Spring AOP 內部實作了一個專注于生成代理物件的工廠類,這樣就避免了大量的手動編碼,這點也是十分人性化的,但最核心的還是動態代理技術,從性能上來說,Spring AOP 雖然無需特殊編譯器協助,但性能上并不優于AspectJ的靜態織入,這點了解一下即可,最后給出Spring AOP簡化的原理模型如下圖,
img

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/297992.html

標籤:其他

上一篇:劍指offer計劃6( 搜索與回溯演算法簡單版)---java

下一篇:Mybatis 整合 ehcache快取

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more