文章目录
一、代理模式
- 二十三种设计模式中的一种,属于结构型模式
- 它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类
间接调用
- 让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦
- 调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护
- 之前做的设计模式系列这里排上用场了:设计模式(六):结构型之代理模式
二、AOP概念及相关术语
1、概述
- AOP(Aspect Oriented Programming)是一种
设计思想
,是软件设计领域中的面向切面编程 - 它是面向对象编程的一种
补充和完善
- 它以通过预编译方式和运行期动态代理方式实现- 在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术
2、相关术语
2.1、横切关注点
- 分散在每个各个模块中解决同一样的问题,如
用户验证、日志管理、事务处理、数据缓存
都属于横切关注点 - 从每个方法中抽取出来的同一类非核心业务
- 在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强
- 这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点
2.2、通知(增强)
- 增强,通俗说,就是你想要增强的功能,比如 安全,事务,日志等
- 每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫
通知方法
- 前置通知:在被代理的目标方法
前
执行 - 返回通知:在被代理的目标方法
成功结束
后执行(代码异常则不执行) - 异常通知:在被代理的目标方法
异常结束
后执行 - 后置通知:在被代理的目标方法
最终结束
后执行(肯定会执行,类似finally) - 环绕通知:使用try…catch…finally结构围绕
整个
被代理的目标方法,包括上面四种通知对应的所有位置
2.3、切面
封装通知方法的类
2.4、目标
被代理的目标对象
2.5、代理
向目标对象应用通知之后创建的代理对象
2.6、连接点
- 这也是一个纯逻辑概念,不是语法定义的
- 把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是
连接点
- 通俗说,就是spring允许你使用通知的地方
2.7、切入点
- 定位连接点的方式
- 每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)
- 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句
- Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法
- 切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件
三、基于注解的AOP
1、切入点表达式语法
1.1、execute表达式(常用)
- 用
*号代替
“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
- 在包名的部分,一个*号只能代表包的层次结构中的一层,表示这一层是任意的- 例如:.Hello匹配com.Hello,不匹配com.xc.Hello*
- 在包名的部分,使用“
*..
”表示包名任意、包的层次深度任意 - 在类名的部分,类名部分整体用*号代替,表示类名任意
- 在类名的部分,可以使用*号代替类名的一部分- 例如:*Service匹配所有名称以Service结尾的类或接口
- 在方法名部分,可以使用*号表示方法名任意
- 在方法名部分,可以使用*号代替方法名的一部分- 例如:*Operation匹配所有方法名以Operation结尾的方法
- 在方法参数列表部分,使用
(..)
表示参数列表任意 - 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
- 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的- 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符- 例如:
execution(public int *..*Service.*(.., int))
正确- 例如:execution(* int *..*Service.*(.., int))
错误
1.2、within表达式
- 给定class的所有方法
表达式格式:包名.* 或者 包名..*
-within(com.xc.service.*)
:拦截包中任意方法,不包含子包中的方法-within(com.xc.service..*)
:拦截包或者子包中定义的方法- within与execution相比,粒度更大,仅能实现到
包
和接口
、类
级别 - 而execution可以精确到方法的返回值,参数个数、修饰符、参数类型等
1.3、this表达式
- 代理对象为指定的类型会被拦截
this(com.xc.service.AccountServiceImpl)
:AccountService接口、AccountServiceImpl实现类- 如果使用jdk动态代理生成的对象,不是AccountServiceImpl类型不会拦截- 如果使用cglib代理生成的是AccountServiceImpl的子类,会被拦截
1.4、target表达式
- 目标对象为指定的类型会被拦截
target(com.xc.service.AccountService)
- this作用于代理对象,target作用于目标对象
1.5、args 表达式
- 方法参数为指定的类型会被拦截
args(com.xc.UserModel)
:匹配只有一个参数,且类型为UserModelargs(type1,type2,typeN)
:匹配多个参数args(com.xc.UserModel,..)
:匹配任意多个参数
1.6、@target表达式
- 目标对象中包含指定注解,调用该目标对象的任意方法都会被拦截
@target(com.xc.MyAnnotation)
1.7、@within表达式
@within(com.xc.MyAnnotation)
- @target 和 @within 的不同点- 父类有注解,但子类没有注解的话,@within和@target是不会对子类生效的- 子类没有注解的情况下,只有没有被重写的有注解的父类的方法才能被@within匹配到- 如果父类无注解,子类有注解的话,
@target对父类所有方法生效,@within只对重载过的方法生效
1.8、@annotation表达式
- 方法上有指定注解会被拦截(注解作用在方法上面)
@annotation(com.xc.MyAnnotation)
1.9、@args表达式
- 方法参数上有指定注解会被拦截
@args(com.xc.Annotation1)
:匹配1个参数,且第1个参数所属的类中有Annotation1注解@args(com.xc.Annotation1,com.xc.Annotation2)
:匹配多个参数,且多个参数所属的类型上都有指定的注解@args(com.xc.Annotation1,..)
:匹配多个参数,且第一个参数所属的类中有Annotation1注解
2、准备工作
添加依赖
<!--spring context依赖--><!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.0.2</version></dependency><!--spring aop依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>6.0.2</version></dependency><!--spring aspects依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>6.0.2</version></dependency>
准备被代理的目标资源(计算器接口-加减乘除)
接口:
publicinterfaceCalculator{intadd(int i,int j);intsub(int i,int j);intmul(int i,int j);intdiv(int i,int j);}
实现类:
@ComponentpublicclassCalculatorImplimplementsCalculator{@Overridepublicintadd(int i,int j){int result = i + j;System.out.println("方法内部 result = "+ result);return result;}@Overridepublicintsub(int i,int j){int result = i - j;System.out.println("方法内部 result = "+ result);return result;}@Overridepublicintmul(int i,int j){int result = i * j;System.out.println("方法内部 result = "+ result);return result;}@Overridepublicintdiv(int i,int j){int result = i / j;System.out.println("方法内部 result = "+ result);return result;}}
3、创建切面类并配置
@Aspect//切面类@Component//ioc容器publicclassLogAspect{//设置切入点和通知类型//切入点表达式: execution(访问修饰符 增强方法返回类型 增强方法所在类全路径.方法名称(方法参数))//通知类型:// 前置 @Before(value="切入点表达式配置切入点")//@Before(value = "execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))")@Before(value ="execution(public int com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))")publicvoidbeforeMethod(JoinPoint joinPoint){String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();System.out.println("前置通知,方法名称:"+ methodName +",参数:"+Arrays.toString(args));}// 后置 @After()//@After(value = "com.xc.spring6.aop.annoaop.LogAspect.pointCut()")@After(value ="pointCut()")publicvoidafterMethod(JoinPoint joinPoint){String methodName = joinPoint.getSignature().getName();System.out.println("后置通知,方法名称:"+ methodName);}// 返回 @AfterReturning@AfterReturning(value ="execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))", returning ="result")publicvoidafterReturningMethod(JoinPoint joinPoint,Object result){String methodName = joinPoint.getSignature().getName();System.out.println("返回通知,方法名称:"+ methodName +",返回结果:"+ result);}// 异常 @AfterThrowing 获取到目标方法异常信息//目标方法出现异常,这个通知执行@AfterThrowing(value ="execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))", throwing ="ex")publicvoidafterThrowingMethod(JoinPoint joinPoint,Throwable ex){String methodName = joinPoint.getSignature().getName();System.out.println("异常通知,方法名称:"+ methodName +",异常信息:"+ ex);}// 环绕 @Around()@Around("execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))")publicObjectaroundMethod(ProceedingJoinPoint joinPoint){// 方法名、参数名String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();Object result =null;try{System.out.println("环绕通知 ==> 目标方法之前执行");//调用目标方法
result = joinPoint.proceed();System.out.println("环绕通知 ==> 目标方法返回值之后");}catch(Throwable throwable){
throwable.printStackTrace();System.out.println("环绕通知 ==> 目标方法出现异常执行");}finally{System.out.println("环绕通知 ==> 目标方法执行完毕执行");}return result;}//重用切入点表达式@Pointcut(value ="execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))")publicvoidpointCut(){}}
配置类:
@Configuration@ComponentScan(basePackages ="com.xc")@EnableAspectJAutoProxy(proxyTargetClass =true)publicclassAopConfig{}
4、测试
publicclassClient{publicstaticvoidmain(String[] args){ApplicationContext context =newAnnotationConfigApplicationContext(AopConfig.class);Calculator calculator = context.getBean(Calculator.class);int result = calculator.add(2,3);System.out.println("执行结果:"+result);}}
输出结果:
环绕通知 ==> 目标方法之前执行
前置通知,方法名称:add,参数:[2,3]
方法内部 result =5
返回通知,方法名称:add,返回结果:5
后置通知,方法名称:add
环绕通知 ==> 目标方法返回值之后
环绕通知 ==> 目标方法执行完毕执行
执行结果:5
5、切面的优先级
- 相同目标方法上同时存在
多个切面
时,切面的优先级控制切面的内外嵌套顺序 - 优先级高的切面:外面- 优先级低的切面:里面 - 使用@Order注解可以控制切面的优先级- @Order(较小的数):优先级高- @Order(较大的数):优先级低
两个切面的执行顺序
6、 Spring5.2.7.RELEASE之前版本顺序
新版顺序:环绕通知(后)放在最后执行,后置通知(最终通知)在返回通知后面
环绕通知 ==> 目标方法之前执行
前置通知,方法名称:add,参数:[2,1]
方法内部 result =3
环绕通知 ==> 目标方法返回值之后
后置通知,方法名称:add
返回通知,方法名称:add,返回结果:3
四、@Aspectj番外篇
- AspectJ:全称Eclipse AspectJ,可以单独使用,也可以整合到其它框架中
- 单独使用AspectJ时需要使用专门的编译器
ajc
- spring基于@AspectJ注解的方式,也只是使用一些注解,并没有依赖于 AspectJ 实现具体的功能
Spring AOP与单独使用@AspectJ区别
aspectj的类加载期织入的实现方式
- aspectj是AOP一种实现,主要原理是用
asm
做字节码替换来达到AOP的目的 - aspectj有3个时间点可以做织入-
编译期
(Compile-time weaving):把aspect类(aop的切面)和目标类(被aop的类)放在一起用ajc编译-编译期后
(Post-compile weaving):目标类可能已经被打成了一个jar包,这时候也可以用ajc命令将jar再织入一次-类加载期
(Load-time weaving):在jvm加载类的时候,做字节码替换
- 其中前2个时间点,我们可以理解为静态织入,因为在class文件生成后,就已经织入好了
- 类加载期织入,可以理解为“动态织入”(注意不同于java动态代理的“动态”),因为这个类替换是在jvm加载类的时候完成的
版权归原作者 冬天vs不冷 所有, 如有侵权,请联系我们删除。