AOP全称Aspect Oriented Programming(⾯向切⾯编程),它和面向对象编程(OOP)是不同的,OOP是一种以对象为核心的编程思想,AOP是一种以切面为核心的编程思想。
切面可以特指某一特定的问题,如日志记录、事务管理、安全性和错误处理等。
而AOP通过将横切关注点从应用程序的核心逻辑中分离出来,减少了代码冗余和复杂性。简单的来说,AOP就是一种思想,对某一类事情的集中处理。
AOP是一种思想,那Spring AOP就是一种实现的方式,当然不是唯一的实现方式,还有AspectJ、CGLIB等。
当要进行网站用户是否登录判断,如图:
如果业务模块中只有一个业务,会有人说,这不是多此一举吗?是的,是多此一举,但是一个业务模块中往往会有着许多许多的业务,那么就可以看出来AOP的强大了,大大的降低代码的耦合性,并且会很好的维护。
直接介绍概念不免略显空洞,但Spring AOP使用起来却很简单,我们可以先使用一下,然后在介绍那令人头大的概念。
在创建项目时,无法自动引入Spring AOP依赖,只能手动复制到pom.xml文件中,如果你有代码可以直接复制,没有的话,也可以从Maven仓库中复制,直接搜索找到对应版本即可。
也可以复制我的:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
切面类UserAspect.java:
package com.example.springaopdemo.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect //告诉spring切面类
@Component
@Slf4j
public class UserAspect {
//切点
@Pointcut("execution(* com.example.springaopdemo.controller.UserController.*(..))")
public void Pointcut(){
}
//前置通知
@Before("Pointcut()")
public void BeforeAdvice(){
log.info("执行了前置通知");
}
//后置通知
@After("Pointcut()")
public void AfterAdvice(){
log.info("执行了后置通知");
}
//环绕通知
@Around("Pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("进入环绕通知");
Object object = null;
object = joinPoint.proceed();
log.info("退出环绕通知");
return object;
}
}
业务类UserController.java:
package com.example.springaopdemo.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
@Slf4j
public class UserController {
@RequestMapping("/sayHi")
public String sayHi(){
log.info("执行了sayHi方法 ");
return "hi";
}
@RequestMapping("/sayHello")
public String sayHello(){
log.info("执行了sayHello方法 ");
return "hello";
}
}
参考业务类ArticleController.java:
package com.example.springaopdemo.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/art")
@Slf4j
public class ArticleController {
@RequestMapping("/showArt")
public String showArt(){
log.info("执行了showArt方法");
return "100篇";
}
}
访问user/sayHi:
访问user/sayHello:
访问art/showArt:
上面就是Spring AOP的简单使用,可以看到访问不同接口的结果不同,为什么前两个接口又可以调用切口类中的方法呢?那些注解的作用是什么?什么又是切口类?调用顺序为什么又是这样?下面慢慢介绍。
注解:
- @Aspect:告诉Spring这是一个切口类。
- @Pointcut:定义一个切点。里面的参数后面介绍。
- @Before:前置通知。目标方法前执行。
- @After:后置通知。目标方法后执行。
- @Around:环绕通知。在⽬标⽅法的前后都会被执⾏. 后⾯的表达式表⽰对哪些⽅法进⾏增强。
分为四部:
- 切点(Pointcut)。
- 连接点(Join Point)。
- 通知(Advice)。
- 切面(Aspect)。
其中切面中包括切点和通知。
切点包括连接点。
切点又称切入点,里面定义了一组规则,告诉程序哪些方法需要进行功能增强。
使用@Pointcut注解,注解具有切点表达式,可以匹配访问修饰符,类,方法,参数等。
execution() 是最常⽤的切点表达式, ⽤来匹配⽅法, 语法为:
execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)
访问修饰符可以写public,也可以省略,也可以使用 * 替代,* 表示不做限制,也可以替代包,类,方法,参数,都表示任意的意思。
但是替代参数时,只能替代一个任意参数,而 . . 可以替代任意参数。
替代包时,也只能替代一层包,而 . . 可以替代任意层包。
这样就可以分析上面这段代码了,限制任意访问修饰符(省略了),* 任意返回类型,然后com.example.springaopdemo.controller包下的UserController类中的任意方法,任意参数可以调用这个切点。
这也就是,UserController类中的接口调用时,为和会打印了那些通知日志,而ArticleController类中的接口调用时没有的原因。
满⾜切点表达式规则的⽅法, 就是连接点。也就是可以被AOP控制的⽅法
以程序举例, 所有 com.example.springaopdemo.controller.UserController 路径下的⽅法, 都是连接点。
连接点是满足切点表达式的方法,而切点可以看作所有连接点的集合。
比如:学校中,教数学的老师们,教数学就是切点表达式,教数学的老师都是一个连接点,教数学的所有老师就构成了切点。
通知就是具体要做的工作,要统一解决的问题。在连接点处执行的代码逻辑,通知分为:
- 前置通知(Before):连接点方法执行前执行,一般没有返回值。
- 后置通知(After):连接点方法执行后执行,一般没有返回值。
- 环绕通知(Around):可以同时在方法调用前、调用后和返回时执行。有返回值。
- 返回通知(AfterReturning):在方法返回时执行。
- 异常通知(AfterThrowing):在方法抛出异常时执行。
所以上面接口调用时,打印的日志才是那种形式。
在AOP⾯向切⾯编程当中, 我们把这部分重复的代码逻辑抽取出来单独定义, 这部分代码就是通知的内容。
切面是一个普通的Java类,它包含了一个或多个通知方法。
切面通过使用Spring AOP的代理机制来将通知方法与目标对象的方法绑定在一起。
当目标对象的方法被调用时,相关的通知方法也会被执行。
切⾯(Aspect) = 切点(Pointcut) + 通知(Advice)
当一个项目中定义了多个切面类,并且多个多个切面类的多个切入点都匹配到同一个目标方法,那么多个切面类的通知方法执行顺序是什么样的呢?
创建两个新的切面类:
UserAspect2.java:
package com.example.springaopdemo.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect //告诉spring切面类
@Component
@Slf4j
public class UserAspect2 {
//切点
@Pointcut("execution(* com.example.springaopdemo.controller.UserController.*(..))")
public void Pointcut(){
}
@Before("Pointcut()")
public void BeforeAdvice(){
log.info("执行了前置通知->UserAspect2");
}
@After("Pointcut()")
public void AfterAdvice(){
log.info("执行了后置通知->UserAspect2");
}
}
UserAspect3.java:
package com.example.springaopdemo.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect //告诉spring切面类
@Component
@Slf4j
public class UserAspect3 {
//切点
@Pointcut("execution(* com.example.springaopdemo.controller.UserController.*(..))")
public void Pointcut(){
}
@Before("Pointcut()")
public void BeforeAdvice(){
log.info("执行了前置通知->UserAspect3");
}
@After("Pointcut()")
public void AfterAdvice(){
log.info("执行了后置通知->UserAspect3");
}
}
@Before 通知:字⺟排名靠前的先执⾏。
@After 通知:字⺟排名靠前的后执⾏。
这样有时并无法满足我们的需要,是否可以更好的控制执行顺序?
那么,就需要用到**@Order**注解。
再次调用刚才的接口:
根据结果看出,@Order 注解标识的切⾯类, 执⾏顺序如下:
@Before 通知:数字越⼩先执⾏。
@After 通知:数字越⼤先执⾏。