AOP是什么
概念分析
AOP,全称Aspect Oriented Programming,中文是“面向切面编程”,
是一种可以在不修改原来的核心代码的情况下给程序动态统一进行增强的一种技术。
SpringAOP: 批量对Spring容器中bean的方法做增强,并且这种增强不会与原来方法中的代码耦合。
简单例子分析
看完上面的概念,或许还会有一点模糊,那就通过一个简单的例子分析:
假如有以下2个方法:
public void method1() {
System.out.println("running method1");
}
public void method2() {
System.out.println("running method2");
}
现在我们有一个需求,就是在执行两个方法之前都输出一句hello world!
,该怎么办呢?
- 直接在他们前面加不就好了:
public void method1() {
System.out.println("hello world!");
System.out.println("running method1");
}
public void method2() {
System.out.println("hello world!");
System.out.println("running method2");
}
这确实可以,但是万一在实际生产中不是两个方法,而是200个、2000个或者更多的方法,难道真的要一个个加吗?
有什么方法可以解耦合化加在他们之前呢,就像这个样子:
这就是要用到我们的AOP了,它可以对原有方法进行增强:(这里做一个简单使用)
- 将原来方法所在的类交给IOC容器管理(
@Component
注解)
package com.example.demo.service;
import org.springframework.stereotype.Component;
@Component
public class AOPTest {
public void method1() {
//System.out.println("hello world!");
System.out.println("running method1");
}
public void method2() {
//System.out.println("hello world!");
System.out.println("running method2");
}
}
- 加入AOP(看不懂不要紧,后面会做详细解释,主要是看个效果)
@Component
@Aspect
public class AOPConfig {
@Pointcut("execution(* com.example.demo.service.*.*(..))")
void pt() {}
@Before("pt()")
void method() {
System.out.println("hello world");
}
}
- 创建测试类
@SpringBootTest
class Demo1ApplicationTests {
@Resource
AOPTest aopTest;
@Test
void aopTests() {
aopTest.method1();
aopTest.method2();
}
}
- 再运行一下两个方法,结果如下:
这个时候方法就完成增强了,从上面的代码看来,我们没有对原有代码进行任何修改,只是外部进行增强就达到了我们想要的效果,这就是AOP的强大之处。
AOP简单入门
通过以上例子,我们知道了AOP的作用,但是我们还是不知道怎么去用,所以接下来就该将怎么去用了。
Tips:作者是在SpringBoot环境下进行使用,因此讲的是SpringBoot整合,但SpringAOP为Spring特性,因此原生Spring也能使用,引入依赖与配置使用有所不同
1 引入依赖
在pom.xml
文件中引入AOP依赖
<!-- 引入aop支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2 创建AOP切面类
2.1 基本切面类创建方法(@Aspect)
方法很简单,只需要在对应的类上加上@Aspect
注解即可,注意需要将该类交给IOC容器管理,需要加上@Component
注解。
@Component
@Aspect
public class AOPConfig {
}
2.2 创建切点方法(@Pointcut)
我们需要知道哪些方法需要被增强,因此需要创建一个空方法加上@Pointcut
注解来指定切点(也就是指出什么方法需要被增强)
@Component
@Aspect
public class AOPConfig {
@Pointcut("execution(* com.example.demo.service.*.*(..))")
void pt() {}
}
但是怎么指定需要被增强的方法呢?
有几种方式:
1 使用切点表达式(execution)
切点表达式:像上面例子一样的表达形式,使用execution来进行标注,指定哪些需要增强的方法,注意是方法!!!!因此下面的格式说明也是方法!
格式:
execution([可见性]返回类型[声明类型].包名.类名.方法名(参数)[异常]),其中[]内的是可选的
上面所有部分都支持通配符的使用,如下:
- * : 匹配所有
- .. : 匹配多个包或多个参数
-
- : 表示类及其子类
- 运算符:&&、||、!
以上面例子的切点表达式为例子:
* com.example.demo.service.*.*(..)
即增强 任意返回值(*) com.example.demo.service包下 所有类(.*) 所有方法(.*) 任意参数(..)
的方法,这样是不是明白了?
那我要增强 com.example.demo.service包下所有类 、start开头的方法、无参数 任意返回值 的方法是啥?
* com.example.demo.service.*.start*()
是不是通俗易解?
2 使用注解(@annotation)
除了切点表达式,还可以使用自定义注解的方法来确定要增强的方法,方法如下:
- 自定义一个新的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) // 指定在方法上生效
public @interface Invoke {
}
- 在切点方法@Pointcut中指定该注解
@Component
@Aspect
public class AOPConfig {
@Pointcut("@annotation(Invoke)")
void pt() {}
}
- 在需要增强的方法上加上刚刚自定义的注解来增强
@Component
public class AOPTest {
@Invoke
public void method1() {
//System.out.println("hello world!");
System.out.println("running method1");
}
@Invoke
public void method2() {
//System.out.println("hello world!");
System.out.println("running method2");
}
}
2.3 在增强的方法指定通知类型
- 通知类型:即要在被增强函数执行的哪个时候执行(例子里用的是方法执行前执行,其实还有很多)
通知类型有以下几种类型:
- @Before:前置通知,在目标方法执行前执行
- @AfterReturning: 返回后通知,在目标方法执行后执行,如果出现异常不会执行
- @After:后置通知,在目标方法之后执行,无论是否出现异常都会执行
- @AfterThrowing:异常通知,在目标方法抛出异常后执行
- @Around:环绕通知,围绕着目标方法执行
为方便理解,可以用一个伪代码来理解:
public Object test() {
before();// @Before 前置通知
try {
Object ret = 目标方法();// 目标方法调用
afterReturing();// @AfterReturning 返回后通知
} catch (Throwable throwable) {
throwable.printStackTrace();
afterThrowing();// @AfterThrowing 异常通知通知
}finally {
after();// @After 后置通知
}
return ret;
}
这样子就应该明白各类通知的作用了。
在确定了通知类型后,只需在增强方法前加上对应注解即可完成增强
@Component
@Aspect
public class AOPConfig {
@Pointcut("execution(* com.example.demo.service.*.*(..))")
void pt() {}
@Before("pt()") //前置通知,增强pt函数注解指定的方法
void method() {
System.out.println("hello world");
}
}
- 环绕通知的理解:
心细的小伙伴看到上面伪代码好像没有环绕通知,其实环绕通知就是以上各类通知的灵活执行版本,围绕着被执行方法执行。示例代码如下:
@Around("pt()")
public void around(ProceedingJoinPoint pjp){
// @Before执行
try {
pjp.proceed();//目标方法执行
// @AfterReturning执行
} catch (Throwable throwable) {
throwable.printStackTrace();
// @AfterThrowing执行
}finally {
// @After执行
}
}
从中可见:
- 原方法执行为
ProceedingJoinPoint
参数的process
方法 - 只需要在该语句前后加上语句,就可以在指定位置进行增强
至此,简单的AOP入门已经完成,也就是知道如何去用了。
AOP进阶
1 核心概念的理解
这部分其实应该是一开始就讲的,但是我觉得还是太抽象了,因此将用法讲完了再讲可能会更好理解。
- Joinpoint(连接点):指那些可以被增强到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点,换句话说,就是交由IOC容器管理的类的所有方法都可以是连接点。
- Pointcut(切入点):指被增强的连接点(方法),就是上面入门中使用切点表达式或者注解所指定的方法
- Advice(通知/ 增强):指具体增强的代码,就是上面入门中通知分类各类注解下的方法
- Target(目标对象):被增强的对象就是目标对象
- Aspect(切面):是切入点和通知(引介)的结合
- Proxy (代理):一个类被 AOP 增强后,就产生一个结果代理类(动态代理知识点)
2 获取被增强方法的信息
非环绕通知获取(非Around)
我们可以在通知方法(也就是增强方法)中增加一个参数JoinPoint
来获取相关信息
- getArgs:获取参数
- getTarget:获取目标对象
除此之外,我们还可以通过getSignature
获取更详细的方法:
我们在增强方法中打个断点,使用debug方法去看一下JoinPoint参数有什么:
可以看到是一个MethodSignature
类型,我们取出来看看:
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
可以看见可以取到被增强方法的各种信息:
- 方法名:getName
- 方法对象: getMethod
- 返回类型:getReturnType
- ......
还有各种信息大家可以自己去挖掘一下。
除此之外,我们还可以通过注解的方式,获取返回值与抛出的错误对象:
- 获取返回对象(@AfterReturning与@After)
@AfterReturning(value = "pt()",returning = "obj")
void method(Object obj) {
// 注解中使用returning获取到了,作为参数传入使用
}
- 获取错误对象(@AfterThrowing)
@AfterThrowing(value = "pt()",throwing = "obj")
void method(Throwable obj) {
// 注解中使用throwing获取到了,作为参数传入使用
}
环绕通知获取
环绕通知也有JoinPoint参数,但是不是JoinPoint本身,而是继承于JoinPoint的ProceedingJoinPoint
方法,它暴露了process
方法可以执行被增强方法。
process方法执行就是被增强方法执行,这个方法的返回值就是被增强方法执行后的返回值,因此,环绕通知必须要有返回值进行返回(当被增强方法有返回值时),否则会造成数据丢失而出现问题!
因为继承于JoinPoint
其余方法使用与上述一致,不再进行讲述。
3 多切面的执行顺序问题
在实际应用中,我们可能配置了多个切面类来使用,但是他们的顺序变得不明确,执行顺序是什么?
- 默认按照类名执行
当我们需要自定义时,在切面类上加@Order
注解来进行排序。
eg:
@Component
@Aspect
@Order(2)
public class AAspect {
}
@Component
@Aspect
@Order(1)
public class BAspect {
}
默认是A -> B
的顺序,加了排序后顺序为B -> A
。
实例:项目完整的日志系统
@Slf4j
@Aspect
@Component
public class ApiLogAspect {
@Pointcut("execution(* cn.polister.controller..*.*(..))")
void pt() {}
@Around("pt()")
public Object ApiLogNote(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
// 打印入口日志
printInLog(pjp);
proceed = pjp.proceed();
} finally {
printOutLog(pjp);
return proceed;
}
}
private void printOutLog(ProceedingJoinPoint pjp) {
log.info("finish running");
}
private void printInLog(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
log.info("==========START=========");
log.info("time: {}", new Date());
log.info("from: {}", request.getRemoteHost());
log.info("method: {}", request.getMethod());
log.info("name: {}", signature.getMethod());
log.info("===========END==========");
}
}
上述就是一个完整记录了controller接收请求的日志系统,利用AOP日志化。
总结
AOP的实现原理为动态代理(以后会有文章专门写的吧......咕咕咕),为我们带来了解耦合化的批量处理解决方案,大大增强了我们开发时代码的灵活度,还是非常重要的知识点的!
参考&鸣谢
B站:Spring教程@三更草堂,三更老师厉害的!
碎碎念
现在是2024.1.26凌晨3点,为啥不睡觉在这写文章(填坑)?
这两天看到Liella!的5巡,有星光序曲,但是被改成11人曲了
START!!True dreams(OP1)、未来如风(ED1)也被改了
想起21年的时候5人星团多好啊,高三党唯一的慰藉,下课回家唯一的放松就是看看切片,22年策划不做人强塞了4个人让五角星破碎了,但想想也不是第一次了
19年时初三看SING的时候也是这样,本来7人团好好的,五周年的时候突然就加了3个人变得突然割裂,我是不是有什么神奇的魔咒啊,就看了两个团,两个都用加人的方式来刺痛我的心(笑
那就再听听5人Ver星光序曲慰藉一下,晚安~