https://www.bilibili.com/video/av47952931 p56~65
AOP概述 Aspect Oriented Programming 面向切面编程
作用及优势 作用:
优势:
Spring中的AOP 相关术语 Joinpoint 连接点 类中可以被增强的方法
Pointcut 切入点 类中实际增强的方法
Advice 通知/增强 切入点上扩展的功能
前置增强:在方法之前执行
后置增强:在方法正常执行之后执行
最终增强:在最后执行,无论是否有异常
环绕增强:在之前和之后执行
异常增强:方法出现异常时执行
Introduction 引介 一种特殊的增强,在不修改类代码的前提下,可以在运行期间为类动态地添加一些方法或Field
Target 目标对象 要增强的类
Weaving 织入 把增强应用到目标对象的过程
Proxy 代理 一个类被AOP织入增强后,就产生一个结果代理类
Aspect 切面 切入点 + 通知/引介
Spring基于XML的AOP配置 配置步骤
配置增强Bean
使用aop:config标签表明开始AOP的配置
使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识 ref属性:是指定通知类bean的Id
在aop:aspect标签的内部使用对应标签来配置通知的类型
aop:before:表示配置前置通知(实例中让printLog方法在切入点方法前执行) method属性:用于指定类中哪个方法是前置通知 pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 切入点表达式的写法: 关键字:execution(表达式) 表达式: 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表) 标准的表达式写法: public void com.itheima.service.impl.AccountServiceImpl.saveAccount() 访问修饰符可以省略 void com.itheima.service.impl.AccountServiceImpl.saveAccount() 返回值可以使用通配符,表示任意返回值 * com.itheima.service.impl.AccountServiceImpl.saveAccount() 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*. * *.*.*.*.AccountServiceImpl.saveAccount()) 包名可以使用..表示当前包及其子包 * *..AccountServiceImpl.saveAccount() 类名和方法名都可以使用*来实现通配 * *..*.*() 参数列表: 可以直接写数据类型: 基本类型直接写名称 int 引用类型写包名.类名的方式 java.lang.String 可以使用通配符表示任意类型,但是必须有参数 可以使用..表示有无参数均可,有参数可以是任意类型 全通配写法: * *..*.*(..) 实际开发中切入点表达式的通常写法: 切到业务层实现类下的所有方法 * com.itheima.service.impl.*.*(..)
示例 xml配置 在Spring Framework Documentation的Core中搜索xmlns:aop,导入约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop ="http://www.springframework.org/schema/aop" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <bean id ="accountService" class ="com.itheima.service.impl.AccountServiceImpl" > </bean > <bean id ="logger" class ="com.itheima.utils.Logger" > </bean > <aop:config > <aop:aspect id ="logAdvice" ref ="logger" > <aop:before method ="printLog" pointcut ="execution(* com.itheima.service.impl.*.*(..))" > </aop:before > </aop:aspect > </aop:config > </beans >
业务层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface IAccountService { void saveAccount () ; void updateAccount (int i) ; int deleteAccount () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class AccountServiceImpl implements IAccountService { public void saveAccount () { System.out.println("执行了保存" ); } public void updateAccount (int i) { System.out.println("执行了更新" +i); } public int deleteAccount () { System.out.println("执行了删除" ); return 0 ; } }
通知 1 2 3 4 5 6 7 8 9 10 public class Logger { public void printLog () { System.out.println("Logger类中的pringLog方法开始记录日志了。。。" ); } }
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class AOPTest { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml" ); IAccountService as = (IAccountService)ac.getBean("accountService" ); as.saveAccount(); as.updateAccount(1 ); as.deleteAccount(); } }
配置切入点表达式 id属性用于指定表达式的唯一标识
expression属性用于指定表达式内容
该标签写在aop:aspect标签内部只能当前切面使用
还可以写在aop:aspect外面,此时就变成了所有切面可用
注意:必须放在切面之前,否则会报错(还不提醒怎么错的)
4种常用通知类型 继续在刚才的实例上加其它通知类型
使用切入点表达式简化配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <aop:config > <aop:pointcut id ="pt1" expression ="execution(* com.itheima.service.impl.*.*(..))" > </aop:pointcut > <aop:aspect id ="logAdvice" ref ="logger" > <aop:before method ="beforePrintLog" pointcut-ref ="pt1" > </aop:before > <aop:after-returning method ="afterReturningPrintLog" pointcut-ref ="pt1" > </aop:after-returning > <aop:after-throwing method ="afterThrowingPrintLog" pointcut-ref ="pt1" > </aop:after-throwing > <aop:after method ="afterPrintLog" pointcut-ref ="pt1" > </aop:after > </aop:aspect > </aop:config >
切入点方法无异常时输出
1 2 3 4 前置通知Logger类中的beforePrintLog方法开始记录日志了... 执行了保存 后置通知Logger类中的afterReturningPrintLog方法开始记录日志了... 最终通知Logger类中的afterPrintLog方法开始记录日志了...
切入点方法有异常时输出
1 2 3 4 5 6 前置通知Logger类中的beforePrintLog方法开始记录日志了... 执行了保存 异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了... 最终通知Logger类中的afterPrintLog方法开始记录日志了... Exception in thread "main" java.lang.ArithmeticException: / by zero ...
后置和异常只会执行其中一个
环绕通知 下面这样配置环绕通知是不行的
1 2 3 public void aroundPringLog () { System.out.println("Logger类中的afterThrowingPrintLog方法开始记录日志了..." ); }
这样配置后,切入点方法不执行了,只执行通知方法
分析:对比动态代理中,发现其中的环绕通知有明确的切入点调用,而这样没有
解决:使用Spring的ProceedingJoinPoint接口。该接口有一个proceed()方法,在程序执行时,Spring会提供该接口的实现类供我们使用
这样可以控制增强方法何时执行,前置、后置、异常、最终都可以实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Logger { public Object aroundPringLog (ProceedingJoinPoint pjp) { Object rtValue = null ; try { Object[] args = pjp.getArgs(); System.out.println("Logger类中的aroundPringLog方法开始记录日志了...前置" ); rtValue = pjp.proceed(args); System.out.println("Logger类中的aroundPringLog方法开始记录日志了...后置" ); return rtValue; }catch (Throwable t){ System.out.println("Logger类中的aroundPringLog方法开始记录日志了...异常" ); throw new RuntimeException(t); }finally { System.out.println("Logger类中的aroundPringLog方法开始记录日志了...最终" ); } } }
1 2 <aop:around method ="aroundPringLog" pointcut-ref ="pt1" > </aop:around >
Spring基于注解的AOP配置 示例 xml配置 约束比基于xml的多了context
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop ="http://www.springframework.org/schema/aop" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <context:component-scan base-package ="com.itheima" > </context:component-scan > <aop:aspectj-autoproxy > </aop:aspectj-autoproxy > </beans >
Logger配置(4种常用类型)![建议别用,有bug] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Component("logger") @Aspect public class Logger { @Pointcut("execution(* com.itheima.service.impl.*.*(..))") private void pt1 () {} @Before("pt1()") public void beforePrintLog () { System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了..." ); } @AfterReturning("pt1()") public void afterReturningPrintLog () { System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了..." ); } @AfterThrowing("pt1()") public void afterThrowingPrintLog () { System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了..." ); } @After("pt1()") public void afterPrintLog () { System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了..." ); } }
运行输出:
1 2 3 4 前置通知Logger类中的beforePrintLog方法开始记录日志了... 执行了保存 最终通知Logger类中的afterPrintLog方法开始记录日志了... 后置通知Logger类中的afterReturningPrintLog方法开始记录日志了...
发现最终在后置之前执行了(异常也一样)
没有办法,因为Spring基于注解的AOP中,调用顺序确实有问题,实际开发中应该慎重
Logger配置(环绕通知) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Component("logger") @Aspect public class Logger { @Pointcut("execution(* com.itheima.service.impl.*.*(..))") private void pt1 () {} @Around("pt1()") public Object aroundPringLog (ProceedingJoinPoint pjp) { Object rtValue = null ; try { Object[] args = pjp.getArgs(); System.out.println("Logger类中的aroundPringLog方法开始记录日志了...前置" ); rtValue = pjp.proceed(args); System.out.println("Logger类中的aroundPringLog方法开始记录日志了...后置" ); return rtValue; }catch (Throwable t){ System.out.println("Logger类中的aroundPringLog方法开始记录日志了...异常" ); throw new RuntimeException(t); }finally { System.out.println("Logger类中的aroundPringLog方法开始记录日志了...最终" ); } } }
此时就没有顺序问题了
1 2 3 4 Logger类中的aroundPringLog方法开始记录日志了...前置 执行了保存 Logger类中的aroundPringLog方法开始记录日志了...后置 Logger类中的aroundPringLog方法开始记录日志了...最终