Spring通知类型详解

Spring通知类型详解

1、Spring5大通知类型

1)前置通知

在目标方法执行之前执行执行的通知。

前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息

注意,如果接收JoinPoint,必须保证其为通知方法的第一个参数,否则报错。

模块依赖:

<dependencies>
        <!-- 配置Spring-context依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>


        <!-- 配置aspectJ依赖 -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>


        <!-- 配置Spring整合JUnit依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>

        <!-- 配置JUnit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

配置方式:

<?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.itheima"/>

    <!--
        proxy-target-class属性值决定是基于接口的还是基于类的代理被创建
        为true则是基于类的代理将起作用(需要cglib库)
        为false或者省略这个属性,则标准的JDK 基于接口的代理将起作用。
        proxy-target-class在spring事务、aop、缓存这几块都有设置,其作用都是一样的。
		高版本的Spring会自动根据实际情况选择选用jdk还是cglib动态代理,所以一般情况下不需要配置。
    -->
    <aop:config proxy-target-class="false">

        <!-- 配置切点,单独配置切点时需要为切点配置唯一id,以便切面引用 -->
        <aop:pointcut id="pc01" expression="execution(* com.itheima.service.impl.UserServiceImpl.*(..))"></aop:pointcut>

        <!-- 配置切面,ref属性用于引用切面对象 -->
        <aop:aspect ref="firstAspect">
            <aop:before method="before" pointcut-ref="pc01"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

目标类父接口代码:

package com.itheima.service;

import org.springframework.stereotype.Service;

/**
 * UserService:目标类接口
 */
public interface UserService {

    public void addUser(String name);

    public void updateUser();

    public void deleteUser();

    public void query();
}

目标类代码:

package com.itheima.service.impl;

import com.itheima.service.UserService;
import org.springframework.stereotype.Service;

/**
 * UserServiceImpl:目标类
 */
@Service("userService")
public class UserServiceImpl implements UserService {
    public void addUser(String name) {
        System.out.println("增加用户。。");
    }

    public void updateUser() {
        System.out.println("修改用户。。");
    }

    public void deleteUser() {
        System.out.println("删除用户。。");
    }

    public void query() {
        System.out.println("查询用户。。");
    }
}

切面类代码:

package com.itheima.aop;

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

/**
 * FirstAspect:切面类代码
 */
@Component("firstAspect")
public class FirstAspect {

    /**
     * 前置增强
     * @param jp 切点
     */
    public void before(JoinPoint jp){
        // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
        Class clz = jp.getTarget().getClass();
        // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
        Signature signature = jp.getSignature();
        String name = signature.getName();
        System.out.println("1 -- before...["+clz+"]...["+name+"]...");
    }

}

测试类代码:

package com.itheima.test;

import com.itheima.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class) //Spring整合junit测试
@ContextConfiguration("classpath:applicationContext.xml")   //指定配置文件位置
public class AOPTest {


    //要使用到userService,使用Autowired自动注入
    @Autowired
    private UserService userService;

    @Test
    public void test01(){
        userService.addUser("itheima.hz");

    }
}

执行结果:

1 -- before...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...
增加用户。。

2)环绕通知

在目标方法执行之前和之后都可以执行额外代码的通知。

在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置

要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint

环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。

环绕通知有控制目标方法是否执行、有控制是否返回值、有改变返回值的能力。

环绕通知虽然有这样的能力,但一定要慎用,不是技术上不可行,而是要小心不要破坏了软件分层的“高内聚 低耦合”的目标。

配置方式:

<aop:around method="around" pointcut-ref="pc01"></aop:before>

目标类父接口代码:

//代码不变,略

目标类代码:

//代码不变,略

切面类代码:

    /**
     *  环绕增强
     * @param pjp   正在执行的切点
     * @return      目标方法的返回值
     * @throws Throwable
     */
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("2 -- around before...");
        //通过ProceedingJoinPoint对象的proceed()方法调用目标方法,必须显示调用
        //该方法的返回值即为目标方法的返回值
        Object result = pjp.proceed();
        System.out.println("2 -- around after...");
        return result;
    }

测试类代码:

//代码不变,略

执行结果:

1 -- before...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...
2 -- around before...
增加用户。。
2 -- around after...

3)后置增强

在目标方法正常执行完成之后执行的通知。该通知在方法正常返回(结束)后,才被执行。

在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个

配置方式:

<aop:after-returning method="afterReturn" pointcut-ref="pc01"/>

目标类父接口代码:

//代码不变,略

目标类代码:

//代码不变,略

切面类代码:

    /**
     *  后置增强
     * @param jp   正在执行的切点
     */
    public void afterReturn(JoinPoint jp){
        // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
        Class clz = jp.getTarget().getClass();
        // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
        Signature signature = jp.getSignature();
        String name = signature.getName();
        System.out.println("3 -- after-returning...["+clz+"]...["+name+"]...");
    }

测试类代码:

//代码不变,略

执行结果:

1 -- before...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...
2 -- around before...
增加用户。。
2 -- around after...
3 -- after-returning...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...

在后置通知中,还可以通过配置获取返回值

返回值添加进通知方法参数列表,但是一定要保证JoinPoint处在参数列表的第一位,否则抛异常

配置方式:

<aop:after-returning method="afterReturn" pointcut-ref="pc1" returning="msg"/>

目标类父接口代码:

//代码不变,略

目标类代码:

//代码不变,略

切面类代码:

    /**
     *  后置增强
     * @param jp	正在执行的切点
     * @param msg	目标方法返回值
     */
    public void afterReturn(JoinPoint jp, Object msg){
        // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
        Class clz = jp.getTarget().getClass();
        // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
        Signature signature = jp.getSignature();
        String name = signature.getName();
        System.out.println("3 -- after-returning...["+clz+"]...["+name+"]...["+ msg +"]...");
    }

测试类代码:

//代码不变,略

执行结果:

1 -- before...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...
2 -- around before...
增加用户。。
2 -- around after...
3 -- after-returning...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...[null]...

4)异常增强

在目标方法抛出异常时执行的通知

可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位

另外,还可以配置参数,让异常通知可以接收到目标方法抛出的异常对象

配置方式:

<aop:after-throwing method="afterThrow" pointcut-ref="pc01" throwing="t"/>

目标类父接口代码:

//代码不变,略

目标类代码:

//其他代码不变,略
public void addUser(String name) {
    System.out.println("增加用户。。");
    int i = 1 / 0;
}

切面类代码:

    /**
     *  异常增强
     * @param jp	正在执行的切点
     * @param t		目标方法执行过程中产生的异常对象 
     */
    public void afterThrow(JoinPoint jp, Throwable t){
        // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
        Class clz = jp.getTarget().getClass();
        // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
        Signature signature = jp.getSignature();
        String name = signature.getName();
        System.out.println("4 -- after-returning...["+clz+"]...["+name+"]..."+t.getMessage());
    }

测试类代码:

//代码不变,略

执行结果:

1 -- before...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...
2 -- around before...
增加用户。。
4 -- after-throwing...[class com.itheima.service.impl.UserServiceImpl]...[addUser].../ by zero

java.lang.ArithmeticException: / by zero

5)最终通知

是在目标方法执行之后执行的通知。

和后置通知不同之处在于,后置通知是在方法正常返回后执行的通知,如果方法没有正常返-例如抛出异常,则后置通知不会执行。

但是最终通知目标方法调用过后一定会执行,不管目标方法有没有正常的执行完成。

另外,后置通知可以通过配置得到返回值,而最终通知无法得到。

最终通知也可以额外接收一个JoinPoint参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数

配置方式:

<aop:after-returning method="after" pointcut-ref="pc01"/>

目标类父接口代码:

//代码不变,略

目标类代码:

//其他代码不变,略
public void addUser(String name) {
    System.out.println("增加用户。。");
    //int i = 1 / 0;
}

切面类代码:

/**
     *  最终增强
     * @param jp   正在执行的切点
     */
public void after(JoinPoint jp){
    // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
    Class clz = jp.getTarget().getClass();
    // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
    Signature signature = jp.getSignature();
    String name = signature.getName();
    System.out.println("5 -- after-returning...["+clz+"]...["+name+"]...");
}

测试类代码:

//代码不变,略

执行结果:

1 -- before...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...
2 -- around before...
增加用户。。
2 -- around after...
3 -- after-returning...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...[null]...
5 -- after-returning...[class com.itheima.service.impl.UserServiceImpl]...[addUser]...

完整源码

pom.xml

<?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>com.itheima</groupId>
    <artifactId>Spring_aop_advice</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- 配置Spring-context依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>

        <!-- 配置aspectJ依赖 -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>

        <!-- 配置Spring整合JUnit依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>

        <!-- 配置JUnit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
</project>

applicationContext.xml

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

    <!--
        proxy-target-class属性值决定是基于接口的还是基于类的代理被创建
        为true则是基于类的代理将起作用(需要cglib库)
        为false或者省略这个属性,则标准的JDK 基于接口的代理将起作用。
        proxy-target-class在spring事务、aop、缓存这几块都有设置,其作用都是一样的。
    -->
    <aop:config proxy-target-class="false">

        <!-- 配置切点,单独配置切点时需要为切点配置唯一id,以便切面引用 -->
        <aop:pointcut id="pc01" expression="execution(* com.itheima.service.impl.UserServiceImpl.*(..))"></aop:pointcut>

        <!-- 配置切面,ref属性用于引用切面对象 -->
        <aop:aspect ref="firstAspect">
            <aop:before method="before" pointcut-ref="pc01"></aop:before>
            <aop:around method="around" pointcut-ref="pc01"></aop:around>
            <aop:after-returning method="afterReturn" pointcut-ref="pc01"
                                 returning="msg" />
            <aop:after-throwing method="afterThrow" pointcut-ref="pc01" throwing="t"/>
            <aop:after-returning method="after" pointcut-ref="pc01"/>

        </aop:aspect>


    </aop:config>

</beans>

UserService.java

package com.itheima.service;

import org.springframework.stereotype.Service;

/**
 * UserService:目标类接口
 */
public interface UserService {

    public void addUser(String name);

    public void updateUser();

    public void deleteUser();

    public void query();
}

UserServiceImpl.java

package com.itheima.service.impl;

import com.itheima.service.UserService;
import org.springframework.stereotype.Service;

/**
 * UserServiceImpl:目标类
 */
@Service("userService")
public class UserServiceImpl implements UserService {
    public void addUser(String name) {

        System.out.println("增加用户。。");
        //int i = 1 / 0;
    }

    public void updateUser() {
        System.out.println("修改用户。。");
    }

    public void deleteUser() {
        System.out.println("删除用户。。");
    }

    public void query() {
        System.out.println("查询用户。。");
    }
}

FirstAspect.java

package com.itheima.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.springframework.stereotype.Component;

/**
 * FirstAspect:切面类代码
 */
@Component("firstAspect")
public class FirstAspect {

    /**
     * 前置增强
     * @param jp 切点
     */
    public void before(JoinPoint jp){
        // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
        Class clz = jp.getTarget().getClass();
        // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
        Signature signature = jp.getSignature();
        String name = signature.getName();
        System.out.println("1 -- before...["+clz+"]...["+name+"]...");
    }

    /**
     *  环绕增强
     * @param pjp   正在执行的切点
     * @return      目标方法的返回值
     * @throws Throwable
     */
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("2 -- around before...");
        //通过ProceedingJoinPoint对象的proceed()方法调用目标方法,必须显示调用
        //该方法的返回值即为目标方法的返回值
        Object result = pjp.proceed();
        System.out.println("2 -- around after...");
        return result;
    }

    /**
     *  后置增强
     * @param jp    正在执行的切点
     * @param msg   目标方法返回值
     */
    public void afterReturn(JoinPoint jp, Object msg){
        // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
        Class clz = jp.getTarget().getClass();
        // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
        Signature signature = jp.getSignature();
        String name = signature.getName();
        System.out.println("3 -- after-returning...["+clz+"]...["+name+"].." +
                ".["+ msg +"]...");
    }

    /**
     *  异常增强
     * @param jp	正在执行的切点
     * @param t		目标方法执行过程中产生的异常对象
     */
    public void afterThrow(JoinPoint jp, Throwable t){
        // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
        Class clz = jp.getTarget().getClass();
        // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
        Signature signature = jp.getSignature();
        String name = signature.getName();
        System.out.println("4 -- after-throwing...["+clz+"]...["+name+"]..."+t.getMessage());
    }

    /**
     *  最终增强
     * @param jp   正在执行的切点
     */
    public void after(JoinPoint jp){
        // 通过JoinPoint对象获取更多信息,getTarget可以获取当前目标类对象
        Class clz = jp.getTarget().getClass();
        // 通过JoinPoint对象获取更多信息,getSignature可以获当前切点的方法签名
        Signature signature = jp.getSignature();
        String name = signature.getName();
        System.out.println("5 -- after-returning...["+clz+"]...["+name+"]...");
    }
}

AOPTest.java

package com.itheima.test;

import com.itheima.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class) //Spring整合junit测试
@ContextConfiguration("classpath:applicationContext.xml")   //指定配置文件位置
public class AOPTest {


    //要使用到userService,使用Autowired自动注入
    @Autowired
    private UserService userService;

    @Test
    public void test01(){
        userService.addUser("itheima.hz");

    }
}

2、五种通知的执行顺序

1.在目标方法没有抛出异常的情况下

前置通知

环绕通知的调用目标方法之前的代码

目标方法

环绕通知的调用目标方法之后的代码

后置通知

最终通知

2.在目标方法抛出异常的情况下

前置通知

环绕通知的调用目标方法之前的代码

目标方法 抛出异常 异常通知

最终通知

3.如果存在多个切面

多切面执行时,采用了责任链设计模式。

切面的配置顺序决定了切面的执行顺序,多个切面执行的过程,类似于方法调用的过程,在环绕通知的proceed()执行时,去执行下一个切面(或如果没有下一个切面,则执行目标方法),从而达成了如下的执行过程:

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QCPz7zuV-1576764102167)(images\01.png)]

如果目标方法抛出异常:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-th7pMWlx-1576764102169)(images\02.png)]

3、五种通知的常见使用场景

通知类型常见应用场景
环绕通知控制事务 权限控制
后置通知记录日志(方法已经成功调用)
异常通知异常处理 控制事务
最终通知记录日志(方法已经调用,但不一定成功)

rService;

@Test
public void test01(){
    userService.addUser("itheima.hz");

}

}




## 2、五种通知的执行顺序

### 1.在目标方法没有抛出异常的情况下

前置通知

环绕通知的调用目标方法之前的代码

目标方法

环绕通知的调用目标方法之后的代码

后置通知

最终通知

### 2.在目标方法抛出异常的情况下

前置通知

环绕通知的调用目标方法之前的代码

目标方法 抛出异常 异常通知

最终通知

### 3.如果存在多个切面

多切面执行时,采用了责任链设计模式。

切面的配置顺序决定了切面的执行顺序,多个切面执行的过程,类似于方法调用的过程,在环绕通知的proceed()执行时,去执行下一个切面(或如果没有下一个切面,则执行目标方法),从而达成了如下的执行过程:

​	[外链图片转存中...(img-QCPz7zuV-1576764102167)]



如果目标方法抛出异常:

[外链图片转存中...(img-th7pMWlx-1576764102169)]



## 3、五种通知的常见使用场景

| 通知类型 | 常见应用场景                         |
| -------- | ------------------------------------ |
| 环绕通知 | 控制事务 权限控制                    |
| 后置通知 | 记录日志(方法已经成功调用)           |
| 异常通知 | 异常处理 控制事务                    |
| 最终通知 | 记录日志(方法已经调用,但不一定成功) |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值