作者简介:大家好,我是未央;
博客首页:未央.303
系列专栏:JavaEE进阶
每日一句:人的一生,可以有所作为的时机只有一次,那就是现在!!!
文章目录
前言
一、 AOP的前置知识
1.1 什么是AOP
1.2 AOP的作用
二、SpringAOP的实现
2.1 添加SpringAOP框架支持
2.2 定义切面(Aspect)
2.3 定义切点(Pointcut)
2.4 定义通知(Advice)
三、实例展示(计时器)
代码实现
总结
前言
今天我们将介绍有关Spring AOP统一功能处理的内容
一、 AOP的前置知识
1.1 什么是AOP
Aop是一种统一处理某一问题的思想,比如验证用户是否登录。
验证用户是否登录实例分析:
在为使用Aop的时候,我们需要验证的每个类(页面)都有调用验证方法,而使用了Aop后,我们只需要在某处把验证规则配置一下,就可以实现对需要验证的类的登录验证,不用每个类在重复调用验证方法了。
Aop由切面、切点、连接点、通知组成;
切面:
切面表示我们要统一处理的功能(类)——比如验证用户是否登录;
切点:
切点则是是否进行Aop拦截的规则(哪些页面不需要进行登录验证,哪些需要,这种规则)
连接点:
连接点则是具体到哪些页面需要进行拦截(哪些类需要调用登录验证方法)
通知:
通知则是验证用户是否登录的那个具体方法实现(代码细节)——》前置通知,后置通知 ;
而AOP是一种思想,而SpringAOP是这个框架对AOP思想的实现(类似IoC和DI) ;
1.2 AOP的作用
AOP由以下四部分组成:
- 切面(Aspect):定义AOP业务类型(表示当前AOP是做什么的)。
- 连接点(Join Point):有可能调用AOP的地方就叫做一个连接点。
- 切点(Pointcut):定义AOP拦截规则。
- 通知(Advice)【增强方法】:定义什么时候干什么事(代码的实现细节,前后顺序)。
通知(Advice):
定义的是被拦截的方法具体要执行的业务,比如用户登录权限验证方法就是具体要执行的业务。
通知的分类方法:
- a) 前置通知:在拦截的目标方法之前执行的通知(事件)
- b)后置通知:在拦截的目标方法之后执行的通知(事件)
- c)返回之后通知:在拦截的目标方法返回数据之后通知
- d)抛出异常之后的通知:在拦截的目标方法抛出异常之后执行的通知
- e)环绕通知:在拦截方法执行前后都执行的通知。
AOP整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例 :
二、SpringAOP的实现
2.1 添加SpringAOP框架支持
在我们的项目的pom.xml中添加支持
这个时候,我们就需要借助 中央仓库了https://mvnrepository.com/
注意:
Spring AOP 框架, 在创建新项目的时候,搜索不到。
Spring Boot 项目中,有没有内置 AOP 框架。这个时候,我们就需要借助 中央仓库了https://mvnrepository.com/
细节拓展: Spring AOP 依赖的版本号标签是可以省略的。
虽然 Spring AOP 没有 作为一个常用框架,导致我们引入框架的时候,需要借助 Maven 中央仓库来引入。
但是!Spring Boot 里面,其实有记录 Spring AOP 的 版本关联信息、
它可根据当前项目的环境,自动引入合适版本的 Spring AOP.
2.2 定义切面(Aspect)
定义方法步骤:
2.3 定义切点(Pointcut)
其中pointcut方法为空方法,其不需要方法体,此方法名就是起到一个“标识”的作用,标识下面的通知方法具体指的是哪个切点。
切点的语法组成:
AspectJ支持三种通配符:
- *:匹配任意字符,只匹配一个元素(包,类,或方法,方法参数)
- ..:匹配任意字符,可以匹配多个元素,在表示类时,必须和*联合使用
- +:表示按照类型匹配指定类的所有类,必须跟在类名后面,如com.cad.Car+,表示继承该类的所有子类包括本身;
表达式示例如下:
- execution(* com.example.demo.UserController. *(..)):匹配UserController类里的所有方法。
- execution(* com.example.demo.UserController+.*(..)):匹配UserController类的子类包括该类的所有方法
- execution(* com.example.demo.*.*(..)):匹配com.example.demo包下的所有类的所有方法
- execution(* com.example.demo..*.*(..)):匹配com.example.demo包下、子孙包下所有类的所有方法
- execution(* addUser(String,int)):匹配addUser方法,且第一个参数类型是String,第二个参数类型是int
2.4 定义通知(Advice)
切点和通知的关系:
Spring 切面类中,可以在方法上使用以下注解,设置方法为通知方法,在满足条件后会通知本方法进行调用:
- 前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
- 后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。
- 返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
- 抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
- 环绕通知使⽤ @Around:通知包裹了的⽅法(集合中的连接点),在被通知的⽅法收到通知之前和调⽤之后执⾏⾃定义的⾏为。
实现通知方法:在什么时机执行什么方法。
以前置方法为例,演示一下:
三、实例展示(计时器)
我们可以在 其值方法中 加一行代码,记录 开始时间。
然后,再在 后置方法中 记录 结束时间。
最后,两者相减,不就得到了 拦截到的方法的执行时间了嘛!
这样做,真的对吗? 是不对。这得看情况。
如果是在单线程的环境下(同一时刻,只有一个线程在访问该方法),使用上述方式,没有问题。
但是!
在多线程的情况下,有多个用户访问 会被拦截下来的方法,每一次访问,都会调用 前置方法。
这会导致, 前置方法记录的开始时间,会不停被刷新(覆盖),最终记录的是 最后一个线程访问的时间。
后置方法,也是同样的情况。
也就是说我们最终相减的情况:
哪一次的开始时间 减去 哪一次 结束时间,我们都是无从获知的!
而且,得出非常多,数量取决访问的线程有多少。那么,问题来了!
前面我不是说: AOP 可以统⼀⽅法执⾏时间的统计嘛。
但是,遇到问题了、
那么,我们该怎么做呢?.有的人可能会说:这是线程安全问题,加锁呗!
对不起,不行!这就是全局的问题,你加锁也解决不了问题。
但是!我们不是剩一个 环绕通知吗?
解决的办法,就在这里。下面,我们就来先了解一下 环绕通知。
环绕通知使用@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
环绕通知@Around:
形象来说:环绕通知,就是把 整个连接点(方法)包裹起来了,那我们就可以“为所欲为”了。
比如说:
我们执行的方法 是在当前通知里面去执行的,所以,我们就可以针对每一个方法去记录开始时间和结束时间。因为在每一次在执行目标方法(连接点)和 通知 的时候,它们是在一块的。给人的感觉就像是具有了 事务的原子性;
代码实现
LoginAop代码 :
package com.example.demo.aop; import com.sun.corba.se.impl.ior.OldJIDLObjectKeyTemplate; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; // 切面表示我们要统一出来的问题——比如验证用户是否登录(LoginAop类) // 切点则是是否进行Aop拦截的规则(哪些页面不需要进行登录验证,哪些需要,这种规则) // 连接点则是具体到哪些页面需要进行拦截(登录验证) // 通知则是验证用户是否登录的那个具体方法实现(代码细节)——》前置通知,后置通知 // 验证当前是否登录 的aop实现类 @Component @Aspect // 表示当前类是一个切面 public class LoginAop { // 定义切点 @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") public void pointcut() { // 标记切点 } // 前置通知 @Before("pointcut()") // 参数说明这个前置方法是针对与那个切点的 public void doBefore() { System.out.println("前置通知"); } // 后置通知 @After("pointcut()") public void doAfter() { System.out.println("后置通知"); } // 环绕通知 @Around("pointcut()") public Object doAround(ProceedingJoinPoint joinPoint) { StopWatch stopWatch = new StopWatch(); Object o = null; System.out.println("环绕通知开始执行"); try { stopWatch.start(); o = joinPoint.proceed(); stopWatch.stop(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("环绕通知结束执行"); System.out.println("执行花费的时间: " + stopWatch.getTotalTimeMillis() + "ms"); return o; } }
总结