Java – SpringAOP – 基于注解的AOP
简介
AOP 面向切面编程,通俗的讲,就是把一个实体类方法中的一些非业务代码抽取出来,并封装到一个类中,这个类就是切面类。切面类可以通过代理,监听实体类的执行,来执行切面类的前置,后置,异常,finally 的方法。
通过注解实现切面编程
如上图表示,如果我们要在每个方法执行时,都需要加上【日志】代码,那么这些【日志】代码,被称为【非核心业务代码】,如果这些代码都嵌到核心方法中,会非常麻烦,不利于维护和阅读,更不利于核心业务的开发。
通过AOP切面,我们可以把非核心业务代码抽取出来,封装在一个类中,并把它们按照核心业务代码执行前,和执行后进行区分,可以分为四片阶段的代码,这些代码叫通知。
上图中展示的是前置通知和后置通知。
实现AOP(使用JDK动态代理)
添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.24</version>
</dependency>
创建实体类接口
Spring一旦使用JDK动态代理的模式,在底层会自动把实体类从ioc中排除(即即使用在实体类中定义了 @Component
也不能通过 getBean
获得)
原因是:代理的目的就是在实体类的外面再封装一层类,ioc直接控制外层类。如果使用了代码,而ioc依然可以通过getBean
获取到实体类,那么用于代理的外层包装类就没有意义了,所以Spring在这方面进行了限制。如下图为代理模式下的实体类与外层类的关系。
// 创建一个运算的接口
public interface Calculator {
// 定义一个加法
int add(int x, int y);
// 定义一个减法
int min(int x, int y);
// 定义一个乘法
int sub(int x, int y);
// 定义一个除法
int div(int x, int y);
}
创建实现类
实现类为实现接口方法的类,也是实体类
// 使用注解定义实体类为 ioc Bean 类,归由ioc管理
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int x, int y) {
int result = x + y;
System.out.println("内部方法add");
return result;
}
@Override
public int min(int x, int y) {
int result = x - y;
System.out.println("内部方法min");
return result;
}
@Override
public int sub(int x, int y) {
int result = x * y;
System.out.println("内部方法sub");
return result;
}
@Override
public int div(int x, int y) {
int result = x / y;
System.out.println("内部方法div");
return result;
}
}
JDK代理模式只能通过接口来获取实体类,不能直接 new
实体类(上面说过,开启了代理就不能直接控制实体类,且Spring也不允许)。
创建Bean配置
使用注解的AOP,必须要配置xml或注解配置文件。
package cn.unsoft.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
// 定义为配置项
@Configuration
// 扫描包
@ComponentScan(basePackages = "cn.unsoft")
// 开启注册的AOP切面功能
// 相当于 xml 中的 <aop:aspectj-autoproxy>
@EnableAspectJAutoProxy
public class SpringConfig {
}
创建切面类
切面类,就是用于存放那些在实体类中抽取出来的非核心业务代码的类,它会随着实体类的执行,按照通知的周期来跟随执行。
@Component
@Aspect
@Order(99)
public class LoggerAspect {
/**
* 定义复用切入点表达式
* 通过设定一个空参空方法体的方法,并声明切入点表达式
* * cn.unsoft.spring.aop.annoitation.*.*(..))
* 是指,匹配所有返回类型的cn.unsoft.spring.aop.annoitation包下的所有类的所有方法
* 并且不考虑参数类型
*/
@Pointcut("execution(* cn.unsoft.spring.aop.annoitation.*.*(..))")
public void PointCut() {
}
/**
* 定义一个前置通知方法
*
* @Before:指定这个方法是用在前置通知上 execution() : 通用格式,用于定位需要前置通知的方法,因为方法名可能会重名,所以方法名需要全类名
* 考虑到重载方法的定义,重载方法只和参数有关,与形参无关,所以只要定义参数类型即可完整定位方法。
* 前置通知方法,会在实体方法执行前执行。
*/
@Before("execution(public int cn.unsoft.spring.aop.annoitation.Calculator.add(int,int))")
public void BeforeMethod(JoinPoint joinPoint) {
System.out.println("前置通知方法");
Signature signature = joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
System.out.println("方法名是" + signature.getName());
System.out.println("参数值为" + Arrays.toString(args));
}
/**
* 要使用复用切入点表达式,只要传入注解该表达式的方法名即可
*/
@Before("PointCut()")
public void AfterMethod() {
}
@After("PointCut()")
public void After(){}
@AfterReturning(value = "PointCut()", returning = "result")
public void AfterReturningMethod(JoinPoint joinPoint, Object result) {
System.out.println(result);
}
// 异常切面需要指定接收 throwing 的参数名
@AfterThrowing(value = "PointCut()", throwing = "e")
public void AfterThrowingMethod(JoinPoint joinPoint, Exception(或Throwing) e) {
System.out.println("异常抛出:" + e);
}
@Around("PointCut()")
public Object AroundMethod(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("此处相当于@Before注解中的执行通知方法");
/**
* ProceedingJoinPoint 是一个带执行的切入点对象,它可以控制实体类方法何时执行
* joinPoint.proceed() 则是执行实体类方法后得出的结果
* joinPoint.proceed() 可以看作是实体类方法全部代码的调用方法,并把执行结果返回
*/
result = joinPoint.proceed();
System.out.println("此处相当于@AfterReturning注解中的执行通知方法");
} catch (Throwable e) {
System.out.println("此处相当于@AfterThrowing注解中的执行通知方法");
}finally {
System.out.println("此处相当于@After注解中的执行通知方法");
}
return result;
}
}
注意:JoinPoint
接收的是来自实体类方法中的各种信息,包括方法名、接收参数等信息。
通过IoC取得实体类
IoC不能直获取实体类,因为实体类已经被代理封装,因此只能通过它实现的接口进行获取。
// 获取ioc
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annoication.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.add(1,1);
==>> 输出结果
前置通知方法
内部方法add
切入点表达式
所谓切入点指的是实体类中被抽取的非核心业务代码后空闲出来的位置,就称切入点,它是用于切入通知的。
切入点定义在上面我们知道,可以通过【@Before()
】进行定义
@Before("execution(public int cn.unsoft.spring.aop.annoitation.Calculator.add(int,int))")
切入点注解还包括以下几种
前置通知:使用@Before
注解标识,在被代理的目标方法前执行
返回通知:使用@AfterReturning
注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
异常通知:使用@AfterThrowing
注解标识,在被代理的目标方法异常结束后执行(死于非命)
后置通知:使用@After
注解标识,在被代理的目标finally方法最终结束后执行(盖棺定论)
环绕通知:使用@Around
注解标识,使用try...catch...finally
结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
切入点通知详解
@Before
@Before
代指在实体类方法执行前会执行的通知方法。
@After
@After
代指实体类方法执行完成后会执行的通知方法,不管实体类方法是否出现运行异常,@After
通知方法都会被执行
@AfterReturning
@AfterReturning
存在于实体类方法中的数据被返回后执行的通知方法,如果实体类方法在执行中出现异常,那么@AfterReturning
通知方法将不会被执行
@AfterReturning
注解可以接收来自实体类方法中返回的数据值,获取方法如下
@AfterReturning(value = "PointCut()",returning = "result")
public void AfterReturningMethod(JoinPoint joinPoint, Object result){
System.out.println(result);
}
代码中的 returning = "result"
是指获取实体类中返回的数据,并把它存放在一个变量中,而这个变量的名称为 result
,变量名可以自定义
我们可以在通知方法中接收这个 result
变量,但是接收的形参必须与returning
定义的名称一致。
@AfterThrowing
@AfterThrowing
存在于实体类方法中执行出现异常时执行的通知方法,如果实体类方法执行后不出现异常,@AfterThrowing
通知方法是不会被执行的。
@AfterThrowing
注解可以接收来自实体类方法中抛出的异常值,获取方法如下:
@AfterThrowing(value = "PointCut()", throwing = "e")
public void AfterThrowingMethod(JoinPoint joinPoint, Exception e) {
System.out.println("异常抛出:" + e);
}
代码中的 throwing = "e"
是指获取实体类中返回的异常,并把它存放在一个变量中,而这个变量的名称为 e
,变量名可以自定义
我们可以在通知方法中接收这个 e
变量,但是接收的形参必须与throwing
定义的名称一致。
@Around
@Around
环绕通知实际上是整合了上面4个通知的一个总体,它会提供一个可执行切入点,开发者可以自定义实体类方法何时执行,出现异常该怎么处理等等。
@Around
类似于JDK动态代理模式:
@Around("PointCut()")
public Object AroundMethod(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("此处相当于@Before注解中的执行通知方法");
/**
* ProceedingJoinPoint 是一个带执行的切入点对象,它可以控制实体类方法何时执行
* joinPoint.proceed() 则是执行实体类方法后得出的结果
* joinPoint.proceed() 可以看作是实体类方法全部代码的调用方法,并把执行结果返回
*/
result = joinPoint.proceed();
System.out.println("此处相当于@AfterReturning注解中的执行通知方法");
} catch (Throwable e) {
System.out.println("此处相当于@AfterThrowing注解中的执行通知方法");
}finally {
System.out.println("此处相当于@After注解中的执行通知方法");
}
return result;
}
实体类中有可能出现返回值,那么 @Around
中必须返回,否则会报错。
表达式语法
1.用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
例如:*.Hello匹配cn.Hello,不匹配cn.unsoft.Hello
2.在包名的部分,使用“*..”表示包名任意、包的层次深度任意
3.在类名的部分,类名部分整体用*号代替,表示类名任意
4.在类名的部分,可以使用*号代替类名的一部分
例如:*Service匹配所有名称以Service结尾的类或接口
5.在方法名部分,可以使用*号表示方法名任意
6.在方法名部分,可以使用*号代替方法名的一部分
例如:*Operation匹配所有方法名以Operation结尾的方法
7.在方法参数列表部分,使用(..)表示参数列表任意
8.在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
9.在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
10.切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
11.在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
例如:execution(public int ..Service.*(.., int)) 正确
例如:execution(* int ..Service.*(.., int)) 错误
复用表达式
在定义切入点表达式时,可以预选设定一个表达式,其它地方可以直接使用该表达式
/**
* 定义复用切入点表达式
* 通过设定一个空参空方法体的方法,并声明切入点表达式
* * cn.unsoft.spring.aop.annoitation.*.*(..))
* 是指,匹配所有返回类型的cn.unsoft.spring.aop.annoitation包下的所有类的所有方法
* 并且不考虑参数类型
*/
@Pointcut("execution(* cn.unsoft.spring.aop.annoitation.*.*(..))")
public void PointCut(){}
/**
* 要使用复用切入点表达式,只要传入注解该表达式的方法名即可
*/
@Before("PointCut()")
public void AfterMethod(){
}
切面类优先级
一个实体类方法可以被多个切面类同时发生通知执行,那么同一个实体类方法中,不同的切面类优先级,通过 @Order()
在切面类中注解控制。
@Order()
注解默认值是 Integer.Max_Value = 2746483647
,值越小,优先级越高。
@Component
@Aspect
// 设置切面类的优先级,值越小,优先级越高,在通知同一个实体类方法时更先执行
@Order(99)
public class LoggerAspect { }
参考项目
下载基本的参考项目
https://www.tzming.com/wp-content/uploads/2023/filesdown/SpringAnnoAOP.rar
共有 0 条评论