AOP 面向切面编程
1. AOP简介
- AOP(Aspect oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
- OOP(Object oriented Programming)面向对象编程
- 作用: 在不惊动原始设计的基础上为其进行
功能增强
- spring理念:无入侵式编程
- SpringAOP的本质:
代理模式
2. AOP核心概念
连接点 (JoinPoint ):
- 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
- 在SpringAOP中,理解为方法的执行。连接点可以被切入点匹配。
切入点 ( Pointcut ) :
- 匹配连接点的式子。意思是“根据什么规则去匹配”
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
通知 ( Advice ) :
- 在切入点处执行的操作,也就是共性功能在SpringAOP中,功能最终以方法的形式呈现
- 意思是,如果切入点匹配成功后,应该执行什么方法
通知类:
- 定义通知的类
切面( Aspect ) :
- 描述通知与切入点的对应关系
织入:
- 执行代理的过程称为织入
通俗的来讲,连接点 就是一个需要
被增强
的方法,而 切入点 是一套规则
,目的是找到
对应的连接点。一旦连接点找到以后,就会根据设置执行
通知。
3. AOP工作流程
- 引入依赖,在配置类中加入启动注解
- 读取所有切面配置中的切入点
- 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
- 匹配失败,创建对象
- 匹配成功,创建原始对象(目标对象)的
代理对象
- 获取bean执行方法
- 匹配失败:获取bean,调用方法并执行,完成操作
- 匹配成功:获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
目标对象(Target ):
原始能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
代理(Proxy )
目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
4. 入门案例
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
接口
package link.xiaomo.dao;
public interface BookDao {
public void update();
public int save();
}
实现类
package link.xiaomo.dao;
public class BookDaoImpl implements BookDao {
public void update(){
System.out.println("book dao update ...");
}
public void save(){
System.out.println("book dao save ...");
}
}
通知类
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void link.xiaomo.dao.BookDao.save())")
private void ptx(){}
@Pointcut("execution(void link.xiaomo.dao.BookDao.update())")
private void pt(){}
// @Around表示环绕原方法增强
@Around("ptx()")
public Object method(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
// 表示对原始操作的调用
Object obj = pjp.proceed();
System.out.println("around after advice ...");
return obj;
}
// @Before表示在原方法之前增强
@Before("pt()")
public void method(){
System.out.println("around before advice ...");
}
}
spring里的任意配置类
@Configuration
@EnableAspectJAutoProxy
public class SpringConfig {
}
5. AOP切入表达式
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
5.1. 切入点表达式标准格式
- 格式:动作关键字 ( 访问修饰符 返回值 全限定名.方法名( 参数 ) 异常名 )
execution (public User link.xiaomo.service.UserService.findById (int ) )
- 动作关键字: 描述切入点的行为动作。例如,**execution **表示
执行
到指定切入点 - 访问修饰符: public,private等,可以省略
- 返回值
- 全限定名:可以是类或接口的全限定名,指包名+类/接口名
- 方法名
- 参数
- 异常名: 方法定义中抛出指定异常,可以省略
5.2. 通配符
可以使用通配符描述切入点,快速描述
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution (public * link.xiaomo.*.UserService.find* (*) )
匹配link.xiaomo包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution (public User link..UserService.findById(..))
匹配link包下的任意包中的UserService类或接口中所有名称为findByld的方法
+
:专用于匹配子类类型
execution(**..*Service+.*(..))
匹配任意以Service结尾的类或接口的子类/子接口中的所有方法
5.3. 书写技巧
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点
通常描述接口
,而不描述实现类(尽量降低藕合) - 访问控制修饰符针对接口开发均采用public描述(
可省略访问控制修饰符描述
) - 返回值类型对于 增删改 类使用 精准类型 加速匹配,对于 **查询 **类 使用*通配 快速描述(比如查询匹配 getBy*)
包名
书写尽量不使用..匹配
,效率过低,常用做单个包描述匹配,或精准匹配接口名
/类名书写名称与模块相关的采用\*匹配
,例如UserService书写成*Service,绑定业务层接口名方法名
书写以动词
进行精准匹配
,名词采用*匹配,例如getByld书写成getBy*,selectAll书写成selectAll- 参数规则较为复杂,根据业务方法灵活调整
- 通常
不使用异常
作为匹配规则,表达式中一般省略异常部分(因为有全局异常处理)
6. 通知获取数据
在以下代码例子中,我们可以在注释的位置填上适当的代码,实现不同的功能
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void link.xiaomo..*Service(..))")
private void pt(){}
@Around("pt()")
public Object method(ProceedingJoinPoint pjp) throws Throwable {
// 具体的逻辑
return obj;
}
}
6.1. 获取实际连接点的方法名
因为我们的一个切入点可能匹配到多个连接点,如果我们想要区分当前的切入点具体是哪个,可以通过以下的方法获取切入点方法的名字,以及所在类的名字。
需要注意,如果通知方法的形参有ProceedingJoinPoint或者JoinPoint,则它们必须放在形参的第一位,否则会报错。
- 以下内容填充在上面代码的注释部分
// 通过ProceedingJoinPoint对象获取签名对象
Signature signature = pjp.getSignature();
// 切入点的类名
String className = signature.getDeclaringTypeName();
// 切入点方法的名字
String methodName = signature.getName();
6.2. 获取切入点数据
我们可以通过ProceedingJoinPoint对象,不仅可以做到上面一个案例中的获取方法和类数据,我们还也可以获取和修改方法的参数、返回值
通过这种方式,我们可以做到在不修改原方法的情况下,实现批量的参数合法性校验,举例而言,比如对以getXXX开头的方法,如果参数是空字符串,就替换成null
获取到的方法,和设置方法的参数类型,都是Obejct数组。
- 获取参数
Object[] args = pjp.getArgs();
- 修改原方法的参数(设置一个新的参数传给连接点)
// 自定义一个新参数
Object[] newArgs = new Object[]{1,2,3};
// 把新参数传给切入点
pjp.process(newArgs);
- 获取返回值
- 在@AfterReturning中获取返回值,需要使用如下方法
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(JoinPoint jp,String ret) {
System.out.println("afterReturning advice ..."+ret);
}
6.3. 通知类型
注解 | 说明 |
---|---|
@Before | 前置通知,在连接点方法前调用 |
@Around | 环绕通知,它将覆盖原有方法,可以想象成前置+原方法+后置 |
@After | 后置通知,在连接点方法后调用 类比finally |
@AfterReturning | 返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常 |
@AfterThrowing | 异常通知,当连接点方法异常时调用 |
环绕通知比较常用:
@Component
@Aspect //声明该bean是一个切面bean 找到切入点+添加通知(增强动作)
@Slf4j
@Order(1)
public class RuntimeAroundAspect {
@Around("link.xiaomo.tlias.aspectj.ZhiFuAspect.pt()")
public Object around2Time(ProceedingJoinPoint joinPoint) {
//前置通知
log.info("前置通知位置:从我这走 买路财,放行");
//方法执行 放行
Object result = null;
try {
result = joinPoint.proceed();
//后置通知
log.info("后置通知位置:放行走完了");
return result;
} catch (Throwable e) {
//异常通知
log.info("异常通知位置:"+ e.getMessage());
return Result.error("有异常");
} finally {
//最终通知
log.info("终于走完了.....");
}
}
}
6.4. (单个切面类中)五大通知执行顺序
不同版本的Spring是有一定差异的,使用时候要注意
- Spring 4
- 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕返回 ==> 环绕最终 ==> @After ==> @AfterReturning
- 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕异常 ==> 环绕最终 ==> @After ==> @AfterThrowing
- Spring 5.28
- 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterReturning ==> @After ==> 环绕返回 ==> 环绕最终
- 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterThrowing ==> @After ==> 环绕异常 ==> 环绕最终
6.5. 多个切面类的执行顺序
1)默认情况下:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
2)@Order可指定执行顺序
@Component
@Aspect
@Order(1) //切面类的执行顺序
public class 某切面类 {
}
@Order(1) 注意作用在AOP切面执行顺序上!
- 注意区分
这些过滤器、拦截器、切面并不是我们在代码中手动调用的,所以需要大家在脑海中强行构建他们的执行顺序。
AOP切面位置:
6.6. 案例:对所有参数去前后空格
@Around("XXXPt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
for (int i = @; i < args.length; i++) {
//判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
Object ret = pjp.proceed(args);
return ret;
}
Comments NOTHING