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、五种通知的常见使用场景
| 通知类型 | 常见应用场景 |
| -------- | ------------------------------------ |
| 环绕通知 | 控制事务 权限控制 |
| 后置通知 | 记录日志(方法已经成功调用) |
| 异常通知 | 异常处理 控制事务 |
| 最终通知 | 记录日志(方法已经调用,但不一定成功) |