1、AOP 简介
1. 什么是 AOP?
AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的一个重要特性,它提供了一种将横切关注点(如日志、事务管理、权限检测、异常处理、限流等)从业务逻辑代码中分离出来的机制,从而提高代码的可重用性、可维护性和模块化程度。
AOP 将应用程序分为核心业务和非核心的公共功能,AOP的关注点是系统中非核心的公共功能;AOP 可以通过预编译或者运行期动态代理的方式,为横跨多个对象(没有继承关系..)的业务逻辑添加统一的功能;
2. AOP 作用
-
提高代码的可重用性和可维护性:通过将横切关注点与业务逻辑代码分离,使得业务逻辑代码更加简洁,便于理解和维护。
-
实现模块之间的解耦:AOP可以将不同模块之间的依赖关系降低到最低,使得系统更加灵活和可扩展。将核心业务和非核心业务的公共功能解耦。
-
增强代码的安全性:通过AOP,可以在不修改原有代码的情况下,对方法进行权限控制、性能监控等操作。对一些方解析功能增强。
-
提高代码的复用性:通过切面编程,可以将多处使用的公共逻辑(如日志记录、事务管理等)封装在切面中,避免在多个地方重复编写相同的代码,提高代码的复用性。
3. AOP 核心概念
-
连接点(Joinpoint):程序执行过程中的某个点,如方法调用、异常抛出等。
-
在SpringAOP中,理解为方法的执行。
-
-
切入点(Pointcut):一组连接点的集合,用于定义哪些连接点将被切面增强。
-
在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法,用切入点表达式匹配连接点。
-
切入点表达式(execution):匹配连接点的式子。
-
-
通知(Advice):在切入点指定的连接点处执行的代码,包括前置通知、后置通知、环绕通知等。
-
切面(Aspect):横切关注点的模块化,是通知、引入和切入点的组合。
2、Advice 类型
-
前置通知(Before Advice)
-
定义:在目标方法执行之前执行的通知。
-
特点:前置通知不会影响连接点的执行,除非它抛出一个异常,这样后面的连接点就不会执行。
-
-
后置通知(After Returning Advice)
-
定义:在目标方法成功执行之后执行的通知。
-
特点:如果目标方法通过抛出异常退出,则不会执行此类型的通知。
-
-
异常通知(After Throwing Advice)
-
定义:在目标方法通过抛出异常退出时执行的通知。
-
特点:专门用于处理异常情况,可以记录异常信息或进行异常处理。
-
-
最终通知(After (finally)Advice)
-
定义:无论目标方法通过何种方式退出(正常返回或异常退出),该通知都会执行。
-
特点:类似于Java中的finally块,无论目标方法执行结果如何,最终通知都会被执行。
-
-
环绕通知(Around Advice)
-
定义:环绕通知是最强大的通知类型,它将目标方法封装起来,可以在方法调用之前和之后自定义行为,甚至可以完全控制是否调用目标方法。
-
特点:环绕通知需要提供一个带有
ProceedingJoinPoint
参数的方法,可以执行目标方法 ProceedingJoinPoint.proceed();。
-
知识点1:@After
名称 | @After |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行 |
知识点2:@AfterReturning
名称 | @AfterReturning |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行 |
知识点3:@AfterThrowing
名称 | @AfterThrowing |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行 |
知识点4:@Around
名称 | @Around |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行 |
知识点4:@Before
名称 | @Before |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行 |
3、AOP 入门案例
1. 思路分析
1.引入依赖(pom.xml)
2.定义连接点
3.定义通知(通知(日志、权限校验、事务))
4.定义切入点
5.绑定切入点与通知关系(切面)
2. 实现步骤
-
开启注解格式AOP功能
@Configuration @ComponentScan /* aspectj自动代理 proxy-target-class=true: 修改创建代理对象的方式 - 完全采用CGCLIB动态代理 expose-proxy: 能够重新获取当前类型的代理对象 <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true" /> */ @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) public class SpringConfig { }
-
引入依赖
<!-- Spring 整合 AspectJ 框架 - AOP框架 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.1.1</version> </dependency>
-
定义连接点
public class UserServiceImpl implements UserService { @Override public void add() { System.out.println("核心业务 - 添加用户"); } @Override public int update(int id) { System.out.println("核心业务 - 更新用户"); return 1; } }
-
定义切入点
//对service包下的任意子包下的任意类下的任意方法 任意参数进行添加功能 @Pointcut("execution(* com.advice.service..*.*(..))") public void advice(){ }
-
定义通知以及 绑定切入点与通知关系
@Before("advice()") public void before(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println(methodName + "准备执行"); //方法参数值列表 //Object[] args = joinPoint.getArgs(); } @AfterReturning(pointcut = "advice()",returning = "returnVal") public void afterReturning(JoinPoint joinPoint,Object returnVal){ System.out.println(joinPoint.getSignature().getName() +"执行结束,返回值:"+returnVal); } @AfterThrowing(pointcut = "advice()",throwing = "e") public void afterThrowing(JoinPoint joinPoint,Exception e){ System.out.println(joinPoint.getSignature().getName() + "执行异常!"); } @After("advice()") public void after(JoinPoint joinPoint){ System.out.println("after:不管连接点是否执行成功,都会执行"); } @Around("advice()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("前置环绕通知"); Object result = proceedingJoinPoint.proceed(); System.out.println("后置环绕通知"); return result; }
-
将通知类配给容器并标识其为切面类
@Component @Aspect public class AopAdvice { ...... ... ...通知 }
-
运行程序
public static void main(String[] args) { ApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class); // 代理对象 - 默认使用JDK动态代理 UserService userService = ioc.getBean(UserServiceImpl.class); userService.update(100); }
执行结果:
知识点1:@EnableAspectJAutoProxy
名称 | @EnableAspectJAutoProxy |
---|---|
类型 | 配置类注解 |
位置 | 配置类定义上方 |
作用 | 开启注解格式AOP功能 |
知识点2:@Aspect
名称 | @Aspect |
---|---|
类型 | 类注解 |
位置 | 切面类定义上方 |
作用 | 设置当前类为AOP切面类 |
知识点3:@Pointcut
名称 | @Pointcut |
---|---|
类型 | 方法注解 |
位置 | 切入点方法定义上方 |
作用 | 设置切入点方法 |
属性 | value(默认):切入点表达式 |
4、切入点表达式
1. 通配符
我们使用通配符描述切入点,主要的目的就是简化之前的配置,具体都有哪些通配符可以使用?
-
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现execution(public * com.user.*.UserService.find*(*))
匹配com.user包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
-
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写execution(public User com..UserService.findById(..))
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
-
+
:专用于匹配子类类型execution(* *..*Service+.*(..))
2. 书写技巧
-
所有代码按照标准规范开发,否则以下技巧全部失效
-
描述切入点通==常描述接口==,而不描述实现类,如果描述到实现类,就出现紧耦合了
-
访问控制修饰符针对接口开发均采用public描述(==可省略访问控制修饰符描述==)
-
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
-
==包名==书写==尽量不使用..匹配==,效率过低,常用*做单个包描述匹配,或精准匹配
-
==接口名/类名==书写名称与模块相关的==采用*匹配==,例如UserService书写成*Service,绑定业务层接口名
-
==方法名==书写以==动词==进行==精准匹配==,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
-
参数规则较为复杂,根据业务方法灵活调整
-
通常==不使用异常==作为==匹配==规则