文章目录
AOP概述
- AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理。
- AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
- 面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样, 会使主业务逻辑变的混杂不清。例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑—转账。
1)AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。是一种通过动态代理实现程序功能扩展和统一维护的一种技术。
2)利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
3)AOP编程操作的主要对象是切面(aspect),而切面用于模块化横切关注点(公共功能)。
AOP术语
- 横切关注点 :
- 从每个方法中抽取出来的同一类非核心业务
- 切面(Aspect):
- 封装横切关注点信息的类
- 通知(Advice):
- 存放横切关注点的方法
- 目标(Target):
- 被通知的对象
- 代理(Proxy):
- 先目标对象应用通知之后创建的代理对象
- 连接点(Joinpoint):
- 横切关注点在程序代码中的具体位置
- 切入点(pointcut):
- 切入点指声明的一个或多个连接点的集合
Spring使用AOP的步骤
1.引入依赖
<!--spirng-aspects的jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
2.创建Spring的配置文件,扫描包和开启AspectJ注解支持
<?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.zyd"/>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
3.创建切面类,编写通知方法
package com.zyd.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* @author : ZYD
* @date : 2022/10/24 19:21
* @description
*/
@Component
@Aspect
public class LoggingAspect {
//切面表达式下文会详细介绍
//通知(advice)
@Before(value = "execution(public int com.zyd.service.*.*(..))")
public void beFore() {
System.out.println("前置通知执行了");
}
通知(advice)
@After(value = "execution(public int com.zyd.service.*.*(..))")
public void after() {
System.out.println("后置通知执行了");
}
}
4.创建目标类
package com.zyd.service;
import org.springframework.stereotype.Service;
/**
* @author : ZYD
* @date : 2022/10/24 19:26
* @description
*/
@Service
public class Calculator {
public int add(int a, int b) {
System.out.println("目标方法执行了");
return a + b;
}
}
5.创建测试类,查看AOP效果
package com.zyd;
import com.zyd.service.Calculator;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author : ZYD
* @date : 2022/10/24 19:28
* @description
*/
public class AopTest {
ApplicationContext ioc = new ClassPathXmlApplicationContext("SpringAop.xml");
@Test
public void test01() {
Calculator calculator = ioc.getBean(Calculator.class);
int add = calculator.add(1, 2);
}
}
切入点表达式
1.作用
- 通过表达式的方式定位一个或多个具体的连接点
2.语法格式
- execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))
public int com.zyd.service.Calculator.add(int,int)) 目标方法是:com.zyd.service.Calculator中的add方法
public int com.zyd.service.Calculator.*(int,int)) 目标方法是com.zyd.service.Calculator中的所有
public int Calculator.*(int,int)) 目标方法是和切面同包的Calculator下的所有方法
public int *.*(int,int)) 目标方法是任何包任何类中的任何方法
public int Calculator.*(int,int) 目标方法是和切面同包的Calculator下所有的方法,参数必须是int和int
public int Calculator.*(String,int,int) 和切面同包的Calculator下所有的方法,参数必须是String、int和int
public int Calculator.*(..) 和切面同包的Calculator下所有的方法,参数必须是任意
public int Calculator.*(String,..) 切面同包的Calculator下所有的方法,参数必须是以String开始,后面无所谓
public int Calculator.*(..,double) 和切面同包的Calculator下所有的方法,参数必须是以double结尾,前面无所谓
public int Calculator.add(..) 和切面同包的Calculator下所有的add方法,参数任意,返回值是int
public * *.Calculator.*(..)
* Calculator.add(..) 和切面同包的Calculator下所有的add方法,参数任意,返回值是任意
* *..*(..) 所有类的所有方法,参数任意,返回值任意
//在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来
Calculator中的所有add方法和所有sub方法 (参数任意。返回值任意)
execution(* Calculator.add(..)) || execution(* Calculator.sub(..))
所有类的所有方法,排除Calculator中的add方法以外的其他所有
!execution(* Calculator.add(..))
Calculator中除add方法以外的其他所有
execution(* Calculator.*(..)) && !execution( * Calculator.add(..))
通知细节
1.前置通知
使用@Before注解标识
在方法执行之前执行的通知。
@Before(value = "execution(* *..*(..))")
public void beFore(JoinPoint joinPoint) {
//获取方法名
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName);
//获取参数
Object[] args = joinPoint.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("前置通知执行了");
}
2.后置通知
使用@After注解标识
后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候。
@After(value = "execution(public int com.zyd.service.*.*(..))")
public void after(JoinPoint joinPoint) {
//获取方法名
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName);
//获取参数
Object[] args = joinPoint.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("后置通知执行了");
}
3.返回通知
使用@AfterReturning注解标识(连接点方法抛异常不执行)
无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
- 在返回通知中,通过@AfterReturning注解的returning属性获取连接点的返回值。该属性的值即为用来传入返回值的参数名称
- 必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值
- 原始的切点表达式需要出现在pointcut属性中
//返回通知
@AfterReturning(pointcut = "execution(public int com.zyd.service.*.*(..))",returning = "result")
public void returningAdvice(JoinPoint joinPoint , Object result) {
//获取方法名
String methodName = joinPoint.getSignature().getName();
//获取参数
Object[] args = joinPoint.getArgs();
System.out.println("Logging: The method " + methodName + " returns " + result);
System.out.println("返回通知");
}
4.异常通知
使用@AfterThrowing注解标识
在异常通知中,通过@AfterThrowing注解的throwing属性获取异常信息。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行。
//异常通知
@AfterThrowing(pointcut = "execution(* *..*(..))",throwing = "e")
public void throwingAdvice(JoinPoint joinPoint,Throwable e){
//获取方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("Logging: The method "+methodName+" occurs "+e);
}
5.环绕通知
通过@Around注解标识
环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed()的返回值,否则会出现异常
//环绕通知
@Around("execution(* *..*(..))")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
//获取方法名
String methodName = proceedingJoinPoint.getSignature().getName();
//获取参数
Object[] args = proceedingJoinPoint.getArgs();
Object result=null;
try {
//前置通知
System.out.println("★Logging: The method "+methodName+" begins with "+ Arrays.toString(args));
//执行目标方法
result = proceedingJoinPoint.proceed();
//返回通知
System.out.println("★Logging: The method "+methodName+" returns "+result);
} catch (Throwable throwable) {
//异常通知
System.out.println("★Logging: The method "+methodName+" occurs "+throwable);
throwable.printStackTrace();
}finally {
//后置通知
System.out.println("★Logging: The method "+methodName+" ends");
}
return result;
}
重(chong)用切入点表达式
@Pointcut(value = "execution(* *..*(..))")
public void pointcut() {}
//实现重用
@Before(value = "pointcut()")
public void beFore(JoinPoint joinPoint) {
//获取方法名
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName);
//获取参数
Object[] args = joinPoint.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("前置通知执行了");
}
设置切面的优先级
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
切面的优先级可以通过实现Ordered接口或利用@Order注解指定。
实现Ordered接口,getOrder()方法的返回值越小,优先级越高。
若使用@Order注解,通过注解的value属性指定优先级,值越小优先级越高。
基于XML的方式实现AOP
除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 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.zyd"/>-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!--配置计算器实现类-->
<bean id="calculator" class="com.zyd.service.Calculator"></bean>
<!--配置切面类-->
<bean id="loggingAspect" class="com.zyd.aop.LoggingAspect"></bean>
<!--AOP配置-->
<aop:config>
<!--配置切入点表达式-->
<aop:pointcut id="pointCut"
expression="execution(* com.zyd.service.Calculator.*(..))"/>
<!--配置切面-->
<aop:aspect ref="loggingAspect">
<!--前置通知-->
<aop:before method="beFore" pointcut-ref="pointCut"></aop:before>
<!--返回通知-->
<aop:after-returning method="returningAdvice" pointcut-ref="pointCut" returning="result"></aop:after-returning>
<!--异常通知-->
<aop:after-throwing method="throwingAdvice" pointcut-ref="pointCut" throwing="e"></aop:after-throwing>
<!--后置通知-->
<aop:after method="after" pointcut-ref="pointCut"></aop:after>
<!--环绕通知-->
<aop:around method="aroundAdvice" pointcut-ref="pointCut"></aop:around>
</aop:aspect>
</aop:config>
</beans>