简述
面向切面(方法)编程(Aspect Oriented Programming)就是在不改动原方法的基础上,为原方法添加新的功能。在需要为一批方法添加一段重复代码时,如日志记录,使用AOP思想可以大大减少重复代码量,提高开发效率,并且易于维护。而Spring事务管理,底层实现也是通过AOP来完成的。
QuickStart
我们先引入一段Demo,来看看Spring AOP的实际使用方法。
使用Spring框架中提供的AOP实现,需要先引入依赖。
<!--Spring Aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--Spring Aop END-->
接着我们编写一个AOP类,我们想记录方法开始执行的时间和结束执行的时间。
@Component
@Aspect //声明是一个切面类
@Slf4j
public class RecordTimeAspect {
@Around("execution(* com.itheima.service.DeptService.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
//执行原方法 - 业务方法
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.info("方法执行时间: {} 毫秒",end-begin);
return result;
}
}
正常调用方法后,能够在控制台看到输出
[http-nio-8080-exec-3] INFO com.itheima.aop.RecordTimeAspect-方法执行时间: 9 毫秒
这就是一个简单的AOP用例。
我们可以看到,面向切面编程具有代码无侵入、减少重复代码、提高开发效率、易于维护的特点。
Spring AOP核心概念
为确保对AOP有精准的理解,必须对下列核心术语进行明确界定:
核心概念 英文 解读 示例 目标对象 Target 指被AOP增强逻辑所应用的原始对象实例,其内部封装了系统的核心业务功能。 DeptServiceImpl类的实例对象。代理对象 Proxy 由AOP框架为目标对象所生成的、遵循代理设计模式的包装对象。客户端的交互实质上是与此代理对象进行,由代理对象负责截获方法调用,并依据切面配置决定增强逻辑的执行时机与目标方法的最终调用。 Spring框架为 DeptServiceImpl实例所创建的代理对象。连接点 JoinPoint 程序执行流程中一个可被识别的、明确的执行点。在Spring AOP的语境下,连接点通常指代方法的执行这一特定事件。 DeptService接口内任一方法的执行过程。切入点 Pointcut 一个用于甄别并匹配连接点的谓词表达式。它精确定义了增强逻辑(即通知)应当作用于何种方法的执行之上。 execution(* com.itheima.service.DeptService.*(..))通知 Advice 在由切入点所指定的特定连接点上被执行的附加逻辑,即被模块化的横切关注点代码。通知定义了“执行何种操作”及“于何时执行”。 recordTime方法体内部的性能计时逻辑。切面 Aspect 通知(Advice)与切入点(Pointcut)的结构化结合体。一个切面完整地描述了在何处(Pointcut)、何时(Advice类型)执行何种操作(Advice内容)。在实现层面,一个被 @Aspect注解的Java类即构成一个切面。RecordTimeAspect类。织入 Weaving 将切面所定义的增强逻辑集成至目标对象,并最终生成代理对象的构造过程。 Spring容器在初始化阶段,解析 @Aspect配置并为匹配的目标对象创建代理实例的过程。从根本上讲,AOP的实现机制根植于代理(Proxy)设计模式,对此点的认知是理解其工作原理的基础。
实现原理
Spring AOP的实现架构完全依赖于动态代理(Dynamic Proxy)机制。在容器启动过程中,Spring框架会扫描切面配置,并为所有匹配的目标对象生成代理实例。此代理实例在功能上对原始对象进行了增强。
代理策略的选择取决于目标对象的继承体系结构:
- JDK动态代理:若目标对象实现了一个或多个接口,Spring框架将默认采用Java开发工具包(JDK)内建的动态代理机制。此机制会生成一个实现了与目标对象相同接口集的代理类。
- CGLIB代理:反之,若目标对象并未实现任何接口,Spring框架将转而使用CGLIB(Code Generation Library)库来创建代理。CGLIB通过动态生成目标类的子类来实现代理功能。
一项重要的技术限制是,基于继承的CGLIB代理机制无法为被final关键字修饰的类或方法生成代理,此为该技术的固有局限。
通知类型&通知顺序
通知类型
通知分为几种类型,分别对应着不同的通知方法执行时间。
| 注解 | 描述 |
|---|---|
| @Around | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行,如果执行中原方法有异常,后续代码不会继续执行 |
| @Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
| @After | 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
| @AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
| @AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |
其中@Around环绕通知需要在方法体中自行调用ProceedingJoinPoint.proceed()来让原方法执行,并且返回值必须指定为Object,否则无法接收到返回值,其他通知则不需要。
切入点表达式抽取
如果在每一个注解里面指定切入点表达式,会显得比较繁琐,所以我们可以将重复的切入点表达式从中抽取出来,通过@PointCut注解。
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){}
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
通知顺序
不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序,前置通知:数字越小先执行; 后置通知:数字越小越后执行
切入点表达式
切入点表达式顾名思义,用于匹配哪些方法要作为切入点加入通知。
有两种形式
execution(……):根据方法的签名来匹配
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) @Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")其中,
?代表此项可以省略,*代表单个任意,..代表任意个任意,也可以用&&,||,!来组装复杂的切入点表达式。注意,参数类型如果是包装类或者对象,需要写全类名,包名类名也不建议省略以提升性能。描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
@annotation(……) :根据注解匹配
定义注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogOperation{ }在业务类上加上此注解
@Override @LogOperation public List<Dept> list() { List<Dept> deptList = deptMapper.list(); return deptList; }切面方法
@Slf4j @Component @Aspect public class MyAspect { //前置通知 @Before("@annotation(com.itheima.anno.LogOperation)") public void before(){ log.info("MyAspect6 -> before ..."); } }
AOP通知获取方法参数
获取方法入参
可以通过
JoinPoint/ProceedingJoinPoint对象中的getArgs方法获得一个Object[]数组,其中包含了所有入参。ProceedingJoinPoint中的proceed(Object[] args)方法可以传入一个Object[]数组,我们可以在AOP通知中对入参进行修改,再传入原方法中。获取方法返回值
仅
Around和AfterReturning可以获取到方法的返回值。Around方法中,proceedingJoinPoint.proceed()方法的返回值就是原方法的返回值AfterReturning则需要在方法形参和注解中声明用来接收返回值的形参@AfterReturning(value = "pt()",returning = "ret") public void doSometing(Object[] ret){ //dosometing... }注意!当方法形参中同时包含多个变量时,Joinpoint对象必须放在第一个形参,不然会报错。
获取抛出的异常
Around可以用try/catch包围住proceed方法来捕获抛出的异常。AfterThrowing和上面获取返回值的形似,需要在方法形参和注解中声明要接收返回值的型参。@AfterThrowing(value = "pt()" , throwing = "t"){ public void doSometing(Throwable t){ //dosometing... } }