一、AOP简介
1.AOP的专业术语(百度百科):
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
2.到底什么是AOP?
例如:在一个系统中有用户、商品、支付三个管理模块,且三个模块都需要进行权限校验、日志记录、事务控制操作,那么我们正常的操作逻辑可能会如下图所示:
那么这样就有个问,有多少接口的话就会出现代码的重复。在开发过程中为注重可维护性这是不可容忍的。那么就可提出每个接口都来调用这个接口。这里有点切面的思想就该C位出道了,此时我们将方法注入到接口调用的某个地方(即切点)。如下图所示:
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理,例如权限校验、日志记录、事务控制等。
看了图解相比大家也对其有了点认识,下面我们用文字进行描述解说;
- AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如权限验证、日志记录、事务控制等。就是在不改变原有的逻辑的基础上,增加一些额外的功能。
- AOP也可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志记录功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能却毫无关系,对于其他类型的代码,例如安全验证、异常处理也都是如此,这种散布在各处的无关的代码被称为横切,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用,这在编程中是十分忌讳的一件事。
- AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性,这不就是我们编程过程。
3.AOP的相关概念
关键词解析:
Aspect(切面)
:通常是一个类(下面代码中的MyAopConfig
类),里面可以定义切入点和通知。JointPoint(连接点)
:程序执行过程中明确的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法(代码中加@AopLog
注解的方法),实际上连接点还可以是字段或者构造器。Advice(通知)
:AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)。Pointcut(切入点)
:就是带有通知的连接点(代码中的myCut()
),在程序中主要体现为书写切入点表达式weave(织入)
:将切面应用到目标对象并导致代理对象创建的过程(即将Aspect
和其他对象连接起来, 并创建Adviced object
的过程)。introduction(引入)
:在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。AOP Proxy
(AOP代理):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是cgLib代理,前者基于接口,后者基于子类。Target Object
(目标对象): 包含连接点的对象。也被称作被通知或被代理对象。
提示:
横切关注点:对哪些方法进行拦截,拦截后怎么处理(获取签名等等),这些关注点称之为横切关注点。
五个通知:@Before
前置通知(beforeAdvice) :在某连接点(JoinPoint)——核心代码(类或者方法)之前执行的通知。但这个通知不能阻止连接点前的执行。为啥不能阻止线程进入核心代码呢?因为@Before注解的方法入参不能传ProceedingJoinPoint,而只能传入JoinPoint。要知道从aop走到核心代码就是通过调用ProceedingJionPoint的proceed()方法。而JoinPoint没有这个方法。@After
后通知(afterAdvice) :当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。@AfterReturning
返回后通知(afterReturnAdvice) :在某连接点正常完成后执行的通知,不包括抛出异常的情况,也就是说方法无异常抛出执行完return时。@AfterThrowing
抛出异常后通知(afterThrowingAdvice) : 在方法抛出异常退出时执行的通知。@Around
环绕通知(aroundAdvice) :包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。这时aop的最重要的,最常用的注解。用这个注解的方法入参传的是ProceedingJionPoint proceedingJionPoint ,可以决定当前线程能否进入核心方法中,通过调用proceedingJionPoint.proceed();
提示
需要注意的一点是:@Before
前置通知(beforeAdvice)不能阻止连接点前的执行,那么这是什么原因导致的不能阻止线程进入核心代码呢?因为@Before
注解的方法入参不能传ProceedingJoinPoint
,而只能传入JoinPoint
。要知道从aop走到核心代码就是通过调用ProceedingJionPoint
的proceed()
方法。但是遗憾的是JoinPoint
没有这个方法。*
@Around
环绕通知(aroundAdvice)要是前端要获取返回值时,最后要有return
的结果,方法返回类型Object
类型,否则无法传回返回值。环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型 。
接下来我们开始肝代码:
确定横切哪种类型的方法有两种写法 :
1、execution
表达式 :execution(访问修饰符 返回值 包名.包名……类名.方法名(参数列表))
包名也可以使用匹配,数量代表包的层级,当前包可以使用…标识,例如 *…DemoController.hello()
类名和方法名也都可以使用匹配: ….*()
参数列表使用…可以标识有无参数均可,且参数可为任意类型。
全通配写法:* ….*(…)
通常情况下,切入点应当设置在业务层实现类下的所有方法:* com.example.aopdemo.aop..(…)。
2、 具体到哪个方法上 基于注解的形式进行拦截 要拦截哪个方法 就在哪个方法上加@AopLog
这个注解 “@annotation(com.example.aopdemo.aop.AopLog)
” 注解类的权限定类名就可以了,意思就是这个切面就找这个注解(@AopLog
) 找到了横切一下就ok了。
@annotation
匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。@annotation(com.example.aopdemo.aop.AopLog)
:指定AopLog注解方法的连接点。
(下面基于注解的形式)
- 引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--aop所用依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 自定义注解:
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AopLog {
}
- 定义一个切面类: 切面负责哪些方法可以被通知所拦截进行横切,要想把一个类变成切面类,需要两步:
① 在类上使用@Component
注解 把切面类注入到IOC
容器中
② 在类上使用@Aspect
注解 使之成为切面类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAopConfig {
/**
* 使用注解定义哪些方法需要被拦截
* 目前定义的是@annotation(com.example.aop.cut.AopLog注解的方法会被拦截
*/
@Pointcut("@annotation(com.example.aop.cut.AopLog)")
public void myCut() {
// 这个方法是空的 不用写任何方法体
}
@Before("myCut()") //此处是将通知和定义的切面相关联@Poincut是找哪些有注解的方法 要被横切的方法 (以下类推)
public void beforeAdvice(JoinPoint joinPoint) { // 参数可加可不加 看需求
Signature signature = joinPoint.getSignature(); //获取方法的签名 : 访问修饰符 返回值 类型 方法名 参数等
String methodMame = signature.getName();// 方法名
System.out.println("前置通知打印方法名称: " + methodMame);
Object[] args = joinPoint.getArgs(); //获取参数 放入列表 因为可能有多个
if (args != null) {
for (Object arg : args) {
System.out.println("前置通知打印参数列表" + arg);
}
}
}
@After("myCut()")
public void afterAdvice() {
System.out.println("后置通知方法被执行。。。");
}
@AfterReturning(pointcut = "myCut()", returning = "result") //意味返回的参数用何变量进行接收 传入
public void afterReturning(JoinPoint joinPoint, Object result) { // 返回通知就是方法执行完return后执行的
System.out.println("返回通知被执行。。。");
System.out.println("返回的参数:" + result);
}
@AfterThrowing(pointcut = "myCut()", throwing = "e") //e 是抛出的异常用何变量进行保存 传入
public void afterThrowingAdvice(JoinPoint joinPoint, Throwable e) {
System.out.println("异常通知被执行。。。");
System.out.println("异常信息" + e.getMessage());
}
@Around("myCut()") //这个方法灵活性很高的
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
System.out.println("环绕通知在目标方法执行之前的逻辑");
// 调用目标方法
Object result = null;
try {
result = joinPoint.proceed(joinPoint.getArgs());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知方法获取的返回值" + result);
return result;
}
- 调用接口方法:
import com.example.aop.cut.AopLog;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AopController {
@GetMapping("/hello")
@AopLog
public String hello(String name) {
System.out.println("hello 方法被执行了");
if ("zhangsan".equals(name)) {
throw new RuntimeException("张三没权限");
}
return "hello" + name;
}
@GetMapping("test")
public String test() {
System.out.println("测试没有横切的方法执行啦");
return "test";
}
}
- 运行截图
- 异常通知运行截图:
- 未进行AOP拦截运行截图:
总结:
其实在AOP
中切面就是与业务逻辑独立,但又垂直存在于业务逻辑的代码结构中的通用功能组合;切面与业务逻辑相交的点就是切点;连接点就是把业务逻辑离散化后的关键节点;切点属于连接点,是连接点的子集;Advice
(增强)就是切面在切点上要执行的功能增加的具体操作;在切点上可以把要完成增强操作的目标对象(Target
)连接到切面里,这个连接的方式就叫织入。