1.面向切面编程AOP入门案例(注解版)
步骤:
①导入aop相关坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.4</version>
</dependency>
说明:spring-context坐标依赖spring-aop坐标
②定义dao接口与实现类
public interface BookDao{
public void save();
public void update();
}
@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 ...");
}
}
③定义通知类,制作通知
public class MyAdvice {
public void before(){
System.out.println(System.currentTimeMillis());
}
}
④定义切入点
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
}
说明:切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑
⑤绑定切入点与通知的关系,指定通知添加到原始连接点的具体执行位置
这里就是制作切面,切面是用来描述通知和切入点之间的关系
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//制作切面,这里通知和切入点的关系是before,通知在切入点之前运行
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
注:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行
⑥定义通知类受spring容器管理,并定义当前类为切面类
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
注:@Component将MyAdvice定义为Spring加载的Bean;@Aspect将MyAdvice定义为切面类
⑦开启Spring对AOP注解驱动支持@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.itheima")
//注解开启AOP
@EnableAspectJAutoProxy
public class SpringConfig {
}
2.AOP工作流程
2.1具体步骤
1.Spring容器启动
2.读取所有切面配置中的切入点
注: 这里只会读取已绑定的切入点,如上图中ptx()并未被绑定就不会读取
3.初始化bean,判定bean对应的通知类中的方法是否匹配到任意切入点
- 匹配失败,创建对象
- 匹配成功,创建原始对象(目标对象)的代理对象
- 匹配成功说明需要对其进行增强
- 当需要对目标对象进行功能增强时,采用的技术是动态代理,所以这里会创建一个代理对象,容器中管理的就是代理对象,而不是原始对象。
- 在运行的时候,执行的是代理对象的方法,在该方法中会对原始方法进行功能增强
4.获取bean执行方法
- 获取bean,调用方法并执行,完成操作
- 根据代理对象的运行模式运行原始方法与增强的内容,完成操作
2.2对于容器中代理对象的理解
- 匹配成功
- 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
- 匹配失败
- 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。
- 要通过字节码文件验证已有对象是否是代理对象,不能直接打印对象,因为这里代理对象重写了原始对象的toString方法
2.3AOP核心概念
- 目标对象(Target)原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
- 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
小结:SpringAOP的本质是代理模式
3.AOP切入点表达式
3.1描述方式
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
描述方式一:执行com.itheima.dao包下的BookDao接口中的无参数update()
execution(void com.itheima.dao.BookDao.update())
描述方式二:不建议,紧耦合。执行com.itheima.dao.impl包下的BookDaoImpl实现类中的无参数update()
execution(void com.itheima.dao.impl.BookDaoImpl.update())
3.2切入点表达式
3.2.1标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(public User com.itheima.service.UserService.findById(int))
- 动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
- 访问修饰符,还可以是public,private等,可以省略
- 返回值,写返回值类型
- 包名,多级包使用点连接
- 类/接口名称
- 方法名
- 参数,直接写参数的类型,多个类型用逗号隔开
- 异常名,方法定义中抛出指定异常,可以省略
3.2.2通配符描述切入点,快速描述
- * :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.itheima.*.UserService.find*(*))//这里必须要有参
- .. :0个或多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(public User com..UserService.findById(..))
- 匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
- + :专用于匹配子类类型
execution(* *..*Service+.*(..))
3.2.3书写技巧
对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让我们用用:
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就紧耦合了,而Spring的思想在尽量降低紧耦合
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述返回值
- 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy*,selectAll书写成selectAll
- 参数规则较为复杂,根据业务方法灵活调整
-
通常不使用异常作为匹配规则
4.AOP通知类型
- AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
- AOP通知共分为以下5种类型
前置通知@Before 目标方法之前
后置通知@After 目标方法之后
环绕通知(重点)@Around 目标方法前后都有
返回后通知(了解)@AfterReturning 抛异常时无法运行
抛出异常后通知(了解)@AfterThrowing 抛异常才能运行
@Around(重点,常用)
作用:设置当前通知方法与切入点之间的绑定关系,当通知方法在原始切入点方法前后运行
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ...");
Object ret = pjp.proceed();
System.out.println("arround after advice ...");
return ret;
}
@Around注意事项(要掌握)
- 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,并通过proceed()得到返回值,实现原始方法调用前后同时添加通知
注:可以实现隔离,即判断对象是否和原始对象相同,从而达到获取权限的目的
- 通知中如果未使用ProceedIngJoinPoint对原始方法进行调用将跳过原始方法的执行
- 对原始方法的调用可以不接收返回值,通知方法设置称void即可,如果接收返回值,必须设定为Object类型
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象
5.案例:测量业务层接口万次执行效率
5.1需求分析
- 需求:任意业务层接口执行均可显示其执行效率(执行时长)
分析:
①业务层功能:业务接口执行前后分别记录时间求差值得到执行效率
②通知类型选择前后均可增强的类型——环绕通知
5.2代码实现
@Component
@Aspect
public class ProjectAdvice {
//匹配业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//设置环绕通知,在原始操作的运行前后记录执行时间
@Around("ProjectAdvice.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");
}
}
补充:当前测试的接口执行效率仅仅是一个理论值,并不是一次完整的执行过程
6.AOP通知获取数据
-
获取切入点方法的参数
- JoinPoint:适用于前置,后置,返回后,抛出异常通知
- ProceedJointPoint:适用于环绕通知
-
获取切入点方法返回值
- 返回后通知
- 环绕通知
-
获取切入点方法运行异常信息
- 抛出异常后通知
- 环绕通知
6.1获取参数
①JointPoint对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数
@Before("pt()")
public void before(JointPoint jp){
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
}
②ProceedJointPoint:JointPoint的子类,@Around采用ProceedJointPoint来获取参数,其余通知类型采用JointPoint来获取参数
public Object around(ProceedingJointPoint pjp) threos Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();
return ret;
}
6.2获取返回值
只有返回后AfterReturing
和环绕Around
这两个通知类型可以获取
①抛出异常后通知可以获取切入点方法中的异常信息,使用形参可以接收对应的异常对象
@AfterReturning(Value = "pt()",returning = "ret")
public void afterReturning(String ret){
System.out.println("afterRetruning advice ..."+ ret);
}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = pjp.proceed(args);
//proceed方法的返回值ret,就是原始方法的返回值
System.out.println(ret);
return ret;
}
②一些小tips:
参数名
afterReturning方法形参类型建议为Object
参数类型可以写成String,为了能匹配更多的参数类型,建议写成Object类型
若参数列表中有JoinPoint,那么在获取原始方法参数时,必须放第一个
6.3获取异常
只有抛出异常后AfterThrowing
和环绕Around
这两个通知类型可以获取
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = null;
try{
ret = pjp.proceed(args);
}catch(Throwable throwable){
t.printStackTrace();
}
return ret;
}
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
@Around环绕通知获取异常只需将异常捕获,在catch方法中就可以获取到原始方法的异常信息
7.案例:百度网盘密码数据兼容处理
分析:
- 在业务方法执行前对之前对所有的输入输出参数进行格式处理——trim()
- 使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用
@Around("DataAdvice.servicePt()")
public Object trimString(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
//对原始参数的每一个参数进行操作
for(int i = 0; i < args.leng ; i++){
//如果是字符串数据
if(args[i].getClass().equals(String.class)){
//取出数据,trim()操作后,更新数据
args[i] = args[i].toString().trim();
}
}
return pjp.proceed(args);
}
AOP总结
- 概念:AOP面向切面编程,是一种编程范式
- 作用:在不惊动原始设计的基础上为方法进行功能增强
- 核心概念
- 代理(Proxy):SpringAOP的本质核心就是采用代理模式实现的
- 连接点(JoinPoint):在SpringAop中,理解为任意方法的执行
- 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
- 通知(Advice):若干个方法的共性,在切入点处执行,最终体现为一个方法
- 切面(Aspect):描述通知与欺辱点对应关系
- 目标对象(Target):被代理的原始对象称为目标对
- 切入点表达式标准格式,动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名 )
- excution(* com.hcx.service.*Service.*(..))
- 切入点表示描述统配符:
- 作用:用于快速描述,范围描述
- *:匹配任意符号(常用)
- …:匹配多个连续任意符号(常用)
- +:匹配子类类型
- 切入点表达式书写技巧
- 按标准规范开发
- 查询操作的返回值建议在使用 * 匹配
- 减少使用…去描述包
- 对接口进行描述,使用表示模块名,例如UserService的匹配描述为Service
- 方法名书写保留动词,例如get,使用表示名称,例如getById匹配描述为getBy
- 参数更卷实际情况灵活调整
- 通知类型
- 前置通知
- 后置通知
- 环绕通知(重点)
- 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
- 环绕通知可以隔离原始方法的调用执行
- 环绕通知返回设置为Objec类型
- 环绕通知中可以对原始方法调用过程中出现的异常进行处理
- 返回后通知
- 抛出异常后通知
- AOP通知获取数据
- 获取切入点方法的参数
- JoinPoint:适用于前置,后置,返回后,抛出异常后通知,设为方法发第一个形参
- ProceedJointPoint:适用于环绕通知
- 获取切入点方法返回值
- 返回后通知
- 环绕通知
- 获取切入点方法运行异常信息
- 抛出异常后通知
- 环绕通知