目录
一、认识SpringAOP
1、AOP是什么?
2、AOP的功能
3、AOP的组成(重要)
二、SpringAOP的实现
1、添加Spring AOP框架支持
2、定义切面和切点
3、定义通知
3.1 完成代码实现
3.2 具体通知分析
4、小练习:使用AOP统计UserController每个方法的执行时间。
三、SpringAOP的实现原理(重点)
AOP是一个思想, Spring AOP 是⼀个框架,是对 AOP 思想的实现,它们的关系和IoC 与 DI 类似。OOP是面向对象编程。AOP:面向切面编程:对某一类事情的集中处理(也就是同一类的事务怎么处理)。
栗子:
比如CSDN:编辑博客,删除博客,写博客都要在用户登录的基础上实现,所以在都实现单个的功能之前,都要对用户登录信息进行验证,各自实现或者调用用户验证的方法,所以非常麻烦。那么抽取成一个公共方法也可以:但是当对公共方法的参数或者其他进行了修改,那么它的所有调用方都要进行修改。所以对于这种功能统⼀,且使⽤的地⽅较多的功能,就可以考虑 AOP来统⼀处理了。所以现在AOP就是:这是哪个功能都要实现的是相同的功能,我们可以将它理解为就是对一类事情的集中处理,现在只需要在某一处配置一下就好了,此时所有需要时判断用户登录的方法就可以全部实现用户登录验证了。
(1)统⼀⽇志记录
(2)统⼀⽅法执⾏时间统计:项目监控、监控项目请求流量、监控接口的响应时间,甚至每个方法的响应时间;
(3)统⼀的返回格式设置:http状态码、code(业务状态码:后端处理响应成功,不代表业务办理成功)、msg(业务处理失败,返回的信息)、data;
(4)统⼀的异常处理;
(5)事务的开启和提交等。
注意:使⽤ AOP 可以扩充多个对象的某个能⼒,所以 AOP 可以说是 OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。
比如我们上面举的例子:关于csdn的博客的整个过程就是一个切面:其中:写博客功能、删除博客、编辑博客都是连接点,它们三个都要实现一个用户登录功能的校验,这个用户登录功能就是一个切点,其中用户登录功能中的方法体中实现代码就是一个通知。
目标:我们就实现上图中的关于csdn的博客的这个AOP的实现。我们使用SpringAOP来实现AOP的功能,目的是拦截所有UserController方法,每次调用UerController中的任何一个方法的时候,都执行相应的通知事件。
SpringAOP的实现步骤如下:
(1)添加SpringAOP的框架支持;
(2)定义切面和切点;
(3)定义通知。
在 pom.xml 中添加如下配置:
org.springframework.boot
spring-boot-starter-aop
//表示是一个切面:CSDN
@Component
@Slf4j
//表示是一个切面:CSDN,要结合五大注解使用
@Aspect
public class LoginAspect {
//表示是一个切点:登录功能验证
@Pointcut("execution(* aop.controller.UserController.*(..))")
public void pointcut1(){
//方法体就是通知
}
}
总结 :
切点指的是具体要处理的某一类问题:用户登录权限的验证就是一个具体的问题,也就是一个切点。
(1)切面上要加@Aspect注解,结合五大注解使用:加@Component;
(2)切点: pointcut 方法为空方法,它不需要有方法体,此方法名就是起到⼀个“标识”的作用,标识下面的通知方法具体指的是哪个切点(因为切点可能有很多个)。
(3)一个@Aspect(切面)下可以有多个切点,@Pointcut("execution(* aop.controller.UserController.*(..))")。同时通知方法不是一定要全部存在的。
(4)切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)切点表达式说明:
(1)灰色表示可以省略,红色不能省略;
(2)修饰符是public等,其中*表示任意;
(3)返回值:*表示任意;
(4)包名:
com.example 表示固定包
com.example.*.service 表示example包下任意包下的固定目录service
com.example.. 表示example包下所有的子包(含自己)
com.example.*.service.. 表示example包下任意子包下固定目录service下的任意包。
(5)类
UserController 表示指定类
*Impl 表示以Impl结尾
User* 表示以User开头
* 表示任意
(6)方法名
writeBlog 固定方法
write* 表示以write开头
*Blog 表示以Blog结尾
* 表示任意
(7)参数
() 表示无参
(int)表示一个整型
(int,int)表示两个整型
(..)表示参数任意
(8)异常:throws一般不写
AspectJ ⽀持三种通配符
(1)* 表示匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
(2).. 表示匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
(3)+ 表示表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的所有⼦类包括本身。
比如:
execution(* com.cad.demo.User.*(..)) :匹配 User 类里的所有方法。
execution(* com.cad.demo.User+.*(..)) :匹配该类的子类包括该类的所有方法。
execution(* com.cad.*.*(..)) :匹配 com.cad 包下的所有类的所有方法。
execution(* com.cad..*.*(..)) :匹配 com.cad 包下、方法包下所有类的所有⽅法。
execution(* addUser(String, int)) :匹配 addUser 方法,且第⼀个参数类型是 String,第⼆个参数类型是 int。
通知定义的是被拦截的方法(具体的写博客、编辑博客和删除博客)具体要执行的业务(用户登录权限的校验)。
使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
(1)前置通知使用@Before:通知方法会在目标方法调用之前执行。
(2)后置通知使用@After:通知方法会在目标方法返回或者抛出异常后调用。
(3)返回之后通知使用@AfterReturning:通知方法会在目标方法返回后调用。
(4)抛异常后通知使用@AfterThrowing:通知方法会在目标方法抛出异常后调用。
(5)环绕通知使用@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
UserController文件
@Slf4j
@RequestMapping("/aop")
@RestController
public class UserController {
//模拟写博客
@RequestMapping("/writeBlog")
public String writeBlog(){
//模拟异常
// int a = 10/0;
log.info("write blog success...");
return "write blog success...";
}
//编辑博客
@RequestMapping("/editBlog")
public String editBlog(){
log.info("edit blog success...");
return "edit blog success...";
}
//删除博客
@RequestMapping("/deleteBlog")
public String deleteBlog(){
log.info("deleteBlog success...");
return "deleteBlog success...";
}
}
LoginAspect文件
@Component
@Slf4j
//表示是一个切面:CSDN,要结合五大注解使用
@Aspect
public class LoginAspect {
//表示是一个切点:登录功能验证
@Pointcut("execution(* aop.controller.UserController.*(..))")
public void pointcut1(){ }
//1、前置通知使用@Before:通知方法会在目标方法调用之前执行。
@Before("pointcut1()")
public void doBefore(){
log.info("do before...");
}
//2、后置函数@After
@After("pointcut1()")
public void doAfter(){
log.info("do After...");
}
//3、@AfterReturning 在return 之前通知
@AfterReturning("pointcut1()")
public void doAfterReturning(){
log.info("do doAfterReturning...");
}
//4、@doAfterThrowing 在return 之前通知
@AfterThrowing("pointcut1()")
public void doAfterThrowing(){
log.info("do doAfterThrowing...");
}
//5、@doAround 环绕方法
@Around("pointcut1()")
//环绕方法要写返回结果 //ProceedingJoinPoint joinPoint表示连接点
public Object doAround(ProceedingJoinPoint joinPoint){
Object object = null;
log.info("环绕通知执行之前执行的方法...");
//执行目标方法
try {
object = joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
log.info("环绕通知执行之后执行的方法...");
return object;
}
@Pointcut("execution(* aop.controller.UserController1.*(..))")
public void pointcut2(){ }
}
(1)@Before
@Before在具体的目标方法调用之前执行。
2、@After
@After在具体调用目标方法之后执行;
(3)@AfterReturing
使用@AfterReturing会在目标方法return之前通知,知道@AfterReturing在@After之前执行;
(4)@AfterThrowing
总结
@doAfterThrowing只有在异常的时候才会执行,正常返回不会执行。异常情况@doAfterThrowing执行之后,@doAfterReturning就不会执行了。正常返回的时候执行@doAfterReturning,出现异常就不会执行了。
正常情况:
异常情况:
(5)@Around(最常用)
注意:
先执行环绕通知,再执行前置通知,接着是目标方法,然后是doAfterReturing和do After后置通知。
- getSignature()) //获取修饰符+ 包名+组件名(类名) +方法名
- getSignature().getName()) //获取方法名
- getSignature().getDeclaringTypeName()) //获取包名+组件名(类名)
普通写法:
在UserController中的每个方法都按如下计算:缺点是所有的方法(包括writeBlog,editBlog方法)都要写一下,冗余性很高。
AOP的写法
/**
* 计算UserController中所有方法的Log的执行时间
*/
//表示是一个切点:登录功能验证
@Pointcut("execution(* aop.controller.UserController.*(..))")
public void pointcut2(){ }
//@doAround 环绕方法
@Around("pointcut2()")
//环绕方法要写返回结果
public Object doAround(ProceedingJoinPoint joinPoint){
Object object = null;
long start = System.currentTimeMillis();
//执行目标方法
try {
object = joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
//getSignature()) //获取修饰符+ 包名+组件名(类名) +方法名
log.info(joinPoint.getSignature().toLongString()+"耗时"+(System.currentTimeMillis()-start));
return object;
}
测试结果:
时间不同主要是cpu调度的偏差。
总结:
(1)SpringAOP是构建在动态代理的基础上,所以Spring对AOP的支持局限于方法级别的拦截。
(2)Spring AOP支持JDK Proxy和 CGLIB的方式实现动态代理;
(3)proxyTargetClass为false,目标实现了接口,AOP默认会基于JDK的方式生成代理类;
(4)proxyTargetClass为false,没有实现接口的类,AOP会基于CGLIB的方式生成代理类;
(5)proxyTargetClass为false为true,AOP会基于CGLIB的方式生成代理类。