AOP是什么?
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
简单来说,AOP是一种编程思想,不通过修改源代码方式,在主干功能里面添加新功能(减少耦合)。
备注:在公司修改原有的代码是大忌
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
通知:需要增强的具体功能(方法)
接入点:所有【可以增强】的方法(但是不一定会增强,根据需要添加)
切入点:接入点中被增强的方法就叫切入点
切面:通知、切入点所在的类
可以通过下面两张图片帮助理解
案例:
目标:在不修改下方daobao接口的两个方法的前提下:每次执行前输出当前系统的时间
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.10.RELEASEversion>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.4version>
dependency>
dependencies>
(不用操作)
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 ...");
}
}
并且添加两个注解
@Component//注册到spring容器中去
@Aspect//告知Spring容器这是一个切面的通知类,设置当前类为切面类
@Pointcut(“execution(void com.itheima.dao.BookDao.update())”)
@Pointcut(“execution(方法的返回值类型 +相对路径.方法名)”)
备注1:通知的类型共5种类型,后面讲
备注2:method方法就是增强的功能
//通知类必须配置成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());
}
}
@EnableAspectJAutoProxy//告诉SpringConfig 开启注解开发AOP功能
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*(..) )
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
包名书写尽量不使用…匹配,效率过低,常用*做单个包描述匹配,或精准匹配
前置通知——通知方法在切入点(方法前)运行
后置通知——通知方法在切入点(方法后)运行
环绕通知(重点)——前后都运行
返回后通知(了解):返回后通知,在原始方法执行完毕后运行,且原始方法执行过程中未出现异常现象
抛出异常后通知(了解):抛出异常后通知,在原始方法执行过程中出现异常后运行
(一般使用这种通知可以完成其他四种通知的作用)
@Around
@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对象 获取本次执行的方法的信息
作用:用于通配符写入的多个不同切入点的区分(区分每次执行的方法)
//1、获取执行的签名对象
Signature signature = pjp.getSignature();//该对象封装了此次执行过程
//2、通过该对象的方法即可获取相应的信息
String className = signature.getDeclaringTypeName();//获取此次的运行的方法的类型
String methodName = signature.getName();//获取此次的运行的方法名
注:(5种通知都可以获取到原始操作的参数)
1.1 JoinPoint :
适用于前置、后置、返回后、抛出异常后通知
Object[] args = jp.getArgs();//args 就是原始操作的参数的数组
1.2 ProceedJointPoint :适用于环绕通知
(ProceedJoinPoint是oinPoint的子接口,因此也可以调用父接口的方法)
同上Object[] args = pjp.getArgs();
作用:可以对获取的参数进行修改
例:如果用传入的参数有问题(比如:输入的类型错误),你可以通过对次args参数数组的值进行修改,(变成默认值)然后通过运行,而不会使程序报错,
得保证运行后有存在返回值,只有下面两种通知能用
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.1环绕通知
3.2 抛出异常后通知(了解)
将切入点pt()的异常值(如果有),放入到形参的t中去
//设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
需求:对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理
专栏文章:
Spring框架(一):概述及简单使用(基于XML方式)
Spring框架(二):注解开发及快速入门
Spring框架(三):Spring整合Mybaits、Junit
Spring框架(四):AOP面向切面编程
Spring框架(五):Spring事务简述(注解方式)
Spring框架(六):Spring注解简述