一、简介
AOP是什么?
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
简单来说,AOP是一种编程思想,不通过修改源代码方式,在主干功能里面添加新功能(减少耦合)。
备注:在公司修改原有的代码是大忌
主要意图
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
二、AOP中重要的概念:
通知:需要增强的具体功能(方法)
接入点:所有【可以增强】的方法(但是不一定会增强,根据需要添加)
切入点:接入点中被增强的方法就叫切入点
切面:通知、切入点所在的类
可以通过下面两张图片帮助理解
save方法和update方法是接入点,表示这些方法【可以增强】
切入点:接入点update()方法需要增强,代表的切入点为pt()
或者可以这么理解:绑定update()和前置通知方法method()
(增强:即使用了通知,调用了通知的方法,不过根据通知的类型不同,执行的位置不一样;)
@Before为前置通知,@Before(“pt()”)表示在切入点pt()之前 执行了method();
——上面设置了切入点pt()指定的方法为update();
因此,执行顺序为 update()——method();
三、快速入门(注解):
案例:
目标:在不修改下方daobao接口的两个方法的前提下:每次执行前输出当前系统的时间
1、导入依赖
<dependencies>
<!--里面包含了Spring-aopjar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--aspectjweaver是spring的切入点表达式需要用的包(AspectJ库)-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
2、明确接入点dao接口的方法
(不用操作)
dao接口
public interface BookDao {
public void save();
public void update();
}
bookdaoimpl接口实现类
@Repository(上传到spring容器,用于注入和调用)
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
// 目标:System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
public void update(){
System.out.println("book dao update ...");
}
}
3、创建一个切面(通知类)
并且添加两个注解
@Component//注册到spring容器中去
@Aspect//告知Spring容器这是一个切面的通知类,设置当前类为切面类
4、设置(谁是)切入点
@Pointcut(“execution(void com.itheima.dao.BookDao.update())”)
@Pointcut(“execution(方法的返回值类型 +相对路径.方法名)”)
备注:切入点名称为pt()
5、设置通知的类型(使用切入点的位置)
备注1:通知的类型共5种类型,后面讲
备注2:method方法就是增强的功能
步骤3-5相关代码
//通知类必须配置成Spring管理的bean
@Component//注册到spring容器中去
@Aspect//告知Spring容器这是一个切面的通知类,设置当前类为切面类
public class MyAdvice {
//设置切入点,要求配置在方法上方
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//设置在切入点pt()的前面运行当前操作(前置通知)
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
6、SpringConfig配置类:开启注解开发AOP功能的驱动支持
@EnableAspectJAutoProxy//告诉SpringConfig 开启注解开发AOP功能
7、开始测试
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
System.out.println(bookDao);
System.out.println(bookDao.getClass());
}
}
四、切入点表达式
切入点表达式:有两种描述方式(通常描述接口)
描述Dao接口
@Pointcut(“execution(void com.itheima.dao.BookDao.update())”)
private void pt(){}
描述DaoImpl实现类 (强耦合)
@Pointcut(“execution(void com.itheima.dao.impl.BookDaoImpl.update())”)
private void pt(){}
切入点表达式标准格式∶
动作关键字(访问修饰符 返回值﹑包名.类/接口名.方法名(参数)异常名 )
execution (
publicUser com.itheima.service.UserService.findById (int) )
动作关键字∶描述切入点的行为动作,例如execution表示执行到指定切入点
访问修饰符:public , private等,可以省略(正常都不写)
返回值
包名
类/接口名方法名参数
异常名︰方法定义中抛出指定异常,可以省略
使用通配符设置多个切入点
* 1个占位符/(至少一个)
.. 0-任意个占位符 ——尽量不使用
切入点表达式书写技巧(规范)
-
描述切入点通常描述接口,而不描述实现类
-
访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
——省略public、private -
接口名/类名书写名称与模块相关的采用匹配,例如UserService书写成Service,绑定业务层接口名
——(接口前使用*,*Service) -
方法名书写以动词进行精准匹配,名词采用*匹配,例如getByld书写成getBy*,selectAll书写成selectAll——(方法后使用* , getBy*)
-
参数规则较为复杂,根据业务方法灵活调整
-
通常不使用异常作为匹配规则
上面5点大致总结如下:execution(* com.itheima.*.*Service.find*(..) ) -
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
-
包名书写尽量不使用…匹配,效率过低,常用*做单个包描述匹配,或精准匹配
五、AOP通知类型
前置通知——通知方法在切入点(方法前)运行
后置通知——通知方法在切入点(方法后)运行
环绕通知(重点)——前后都运行
返回后通知(了解):返回后通知,在原始方法执行完毕后运行,且原始方法执行过程中未出现异常现象
抛出异常后通知(了解):抛出异常后通知,在原始方法执行过程中出现异常后运行
前置通知和后置通知
环绕通知(重点)
(一般使用这种通知可以完成其他四种通知的作用)
@Around
- 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
- 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
- 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,必须设定为Object类型
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象
下面是环绕通知模板(没有任何的增强操作)
@Around("切入点方法名")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//表示对原始操作的调用
Object ret = pjp.proceed();
return ret;
}
案例(环绕通知):测试业务层接口的万次执行效率
需求∶任意业务层接口执行均可显示其执行效率(执行时长)
分析∶
①:业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率②∶通知类型选择前后均可以增强的类型——环绕通知
实现如下
@Component
@Aspect
public class ProjectAdvice {
//匹配业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//设置环绕通知,在原始操作的运行前后记录执行时间
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
//获取执行的签名对象
Signature signature = pjp.getSignature();//该对象封装了此次执行过程
String className = signature.getDeclaringTypeName();//获取此次的运行的方法的类型
String methodName = signature.getName();//获取此次的运行的方法名
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
}
附:ProceedingJoinPoint对象
通过ProceedingJoinPoint对象 获取本次执行的方法的信息
作用:用于通配符写入的多个不同切入点的区分(区分每次执行的方法)
- 通过ProceedingJoinPoint对象 获取执行的签名对象
- 通过该对象的方法即可获取相应的信息
//1、获取执行的签名对象
Signature signature = pjp.getSignature();//该对象封装了此次执行过程
//2、通过该对象的方法即可获取相应的信息
String className = signature.getDeclaringTypeName();//获取此次的运行的方法的类型
String methodName = signature.getName();//获取此次的运行的方法名
六、AOP通知获取数据
1、获取切入点方法的参数
注:(5种通知都可以获取到原始操作的参数)
1.1 JoinPoint :
适用于前置、后置、返回后、抛出异常后通知
Object[] args = jp.getArgs();//args 就是原始操作的参数的数组
1.2 ProceedJointPoint :适用于环绕通知
(ProceedJoinPoint是oinPoint的子接口,因此也可以调用父接口的方法)
同上Object[] args = pjp.getArgs();
作用:可以对获取的参数进行修改
例:如果用传入的参数有问题(比如:输入的类型错误),你可以通过对次args参数数组的值进行修改,(变成默认值)然后通过运行,而不会使程序报错,
2、获取切入点方法返回值(了解)
得保证运行后有存在返回值,只有下面两种通知能用
2.1 环绕通知:
就是上方的 Object ret = pjp.proceed();
2.2 返回后通知
将切入点pt()的返回值(如果有),放入到形参的ret中去
//设置返回后通知获取原始方法的返回值,要求returning属性值必须与方法形参名相同
@AfterReturning(value = "pt()",returning = "ret")//如果原始方法中有返回值,就将该返回值放到形参ret中去
public void afterReturning(JoinPoint jp,Object ret) {
System.out.println("afterReturning advice ..."+ret);
}
3、获取切入点方法运行异常信息(了解)
3.1环绕通知
3.2 抛出异常后通知(了解)
将切入点pt()的异常值(如果有),放入到形参的t中去
//设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
案例(数据处理):百度网盘密码数据兼容处理
需求:对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理
专栏文章:
Spring框架(一):概述及简单使用(基于XML方式)