前言:在开发完成的项目中,通常在某些已开发完成的功能中进行一些改造(例如:在所有增加功能的方法中新增操作员功能的实现)如果这个时候把每个新增功能的方法中都添加相应的代码,显然工作量较大,而且在以后的功能变更中由于耦合性太高的缘故是使操作变得繁琐,进而使用Aop能解决在方法增强的同时保证耦合性降低。
一.AOP的简介
- AOP( Aspect Oriented Programming):面向切面编程,它与OOP(Object Oriented Programming)面向对象编程比较类似,都是一种编程范式。
- AOP的作用:在无入侵式的情况下,对原始设计的基础上为其进行功能增强(简单的说就是在不改变方法源代码的基础上对方法进行功能增强。
二.AOP的专业术语
名称 | 说明 |
---|---|
连接点(JoinPoint) | 程序执行过程中的某个特定的点,比如某个方法被调用的时候或者处理异常的时候。在Spring AOP中一个连接点总是代表一个方法的执行,其实AOP拦截的方法就是一个连接点。 |
切入点(Pointcut) | 匹配连接点的断言。通知和切入点表达式关联,并与切入点匹配的任何连接点上运行。切入点表达式如何跟连接点匹配是AOP的核心,Spring默认使用Aspectj作为切入点语法。说白了就是切面指定一个方法被AOP拦截到的时候要执行的代码。 |
通知(Advice) | 在切面的某个连接点上执行的动作,通知类型包括“before”、“after”、“around”等。许多AOP框架都是以拦截器作为通知的模型,并维护一个以连接点为中心的拦截器链,Spring也不例外。总之就是AOP对拦截点的处理通过通知来执行,所以说通知是指一个方法被AOP拦截到的时候要执行的代码。 |
目标(Target) | 被一个或多个切面所通知的对象,也称作被通知对象。由于Spring AOP是通过运行时代理实现的,所以这个对象永远时被代理对象。说白了就是所以的对象在AOP中都会产生一个代理类,AOP整个过程都是针对代理类进行处理 |
切面(Aspect) | 一个关注点的模块化,这个关注点可能会横切多个对象。事务管理就是一个关于横切关注点很好的例子,在Spring中我们可以通过XML或者注解来实现对程序的切面。 |
织入(Weaving) | 把切面连接到其它应用程序类型或者对象上,并创建一个被通知对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成, Spring和其它纯AOP框架一样,在运行时完成织入,说白了就是把切面跟对象关联并创建该对象的代理对象的过程。 |
代理(Proxy) | AOP框架创建的对象,用来实现切面(包括通知方法执行等功能),在Spring中AOP可以是JDK动态代理或Cglib代理。 |
三. Advice的类型
通知类型 | 简介 | 注解 |
---|---|---|
前置通知(Before advice) | 在某个连接点(Join point)之前执行的通知,但这个通知不能阻止连接点的执行(除非它抛出一个异常)。 | @Before |
返回后通知(After returning advice) | 在某个连接点(Join point)正常完成后执行的通知。例如,一个方法没有抛出任何异常正常返回。 | @AfterReturning |
*抛出异常后通知(After throwing advice) | 在方法抛出异常后执行的通知。 | @AfterThrowing |
后置通知(After finally advice) | 当某个连接点(Join point)退出的时候执行的通知(不论是正常返回还是发生异常退出)。 | @After |
*环绕通知(Around advice) | 包围一个连接点(Join point)的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。 | @Around |
四.快速上手(基于SpringBoot项目,注解开发)
在maven项目里面导入相关坐标
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
该坐标为切入点表达式依赖坐标,Aop在Boot依赖中进行了实现,无需手动导入。
在Controller层里面,我们定义一个方法用于测试(注意AOP的测试在测试环境里面并不方便,这 里为了方便,放到Controller层)
package com.itheima.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/sin")
public void run(){
System.out.println("主方法执行.......");
}
}
这里,我们定义的是一个被需要增强的方法。
定义一个切面
package com.itheima.Aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAop {
@Pointcut("execution(void com.itheima.Controller.UserController.run())")
private void put(){}
@Before("put()")
public void before(){
System.out.println("这是前置通知");
}
@After("put()")
public void after(){
System.out.println("这是后置通知");
}
@Around("put()")
public void around(ProceedingJoinPoint point) throws Throwable {
System.out.println("环绕前置通知");
Object proceed = point.proceed();
System.out.println("环绕后置通知");
}
}
在这里,先对前,后,环绕通知进行测试。 显示结果如下
我们设置的方法,都执行了,前置和环绕前置在主方法执行前,环绕后置和后置在执行主方法后。
下面对异常通知进行测试,只有程序出现异常时,才会触发的通知(这里在程序中设计一个有异常的问题)
package com.itheima.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/sin")
public void run() throws Exception{
int a = 1;
int b = 0;
int c = a/b;
}
}
切面如下
package com.itheima.Aop;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAop {
@Pointcut("execution(void com.itheima.Controller.UserController.run())")
private void put(){}
@AfterThrowing("put()")
public void AfterThrowing(){
System.out.println("这是异常通知");
}
}
测试结果如下
在这里,我们的异常必须选择抛出才能够触发,如果选择捕获,就无法触发异常通知。
剩下一个返回通知,这里小编不在进行测试(有兴趣的可以按照同样的思路进行测试)。
五.环绕通知
在上面的测试中,前后通知一目了然,关于环绕通知中的代码
@Around("put()")
public void around(ProceedingJoinPoint point) throws Throwable {
System.out.println("环绕前置通知");
Object proceed = point.proceed();
System.out.println("环绕后置通知");
}
有一行代码如下
Object proceed = point.proceed();
在这里小编解释一下,通过point这个参数中的proceed()方法,我们是为了标识需要被加强的方法,当然它也可以拿到被增强方法的返回值。(想象一下,如果这里我们将这行代码省略,会出现什么问题?无法确定另外两行代码是在主方法执行的那个位置,有兴趣的可以自行测试)。
会发现只执行了两个通知,并未执行主方法。
六.切点表达式
在上面的测试中,我们切面里面出现了如下的代码
@Pointcut("execution(void com.itheima.Controller.UserController.run())")
private void put(){}
这里小编简述一下@Pointcut就是定义切点的一个注解,在后面的括号内部,我们需要填写的就是需要被增强方法的路径,当然关于表达式的定义还有很多种,这里小编不进行详解,可以自行学习。
例如:
@Pointcut("execution(void com.itheima.Controller.UserController.*)")
private void put(){}
它表示的就是在UserController下的所有方法,都要被被增强。
当然,当我们遇到需要被增强的方法不在一个包下,分布很乱,没有任何公共特点的时候,在使用上面的思路去增强就比较困难,这里小编提供一种方案,自定义注解标识。
首先,我们需要只定义一个注解
package com.itheima.Annotate;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Identifying {
}
在切面类里面的切点表达式,我们需要换种写法
@Pointcut("@annotation(com.itheima.Annotate.Identifying)")
private void put(){}
这里,需要知道的是后面括号内填写的时你自定义注解的路径,这样以来,你只需要在被需要增强的方法上加上该注解,就会被自动识别并进行加强。
package com.itheima.Controller;
import com.itheima.Annotate.Identifying;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/sin")
@Identifying
public void run() throws Exception{
int a = 1;
int b = 0;
int c = a/b;
}
}
注意观察与之前相比的变化。