面向切面编程

面向切面编程【AOP,Aspect Oriented Programming】:通过预编译方式和运行期间动态代理实现程序功能的统一维护的技术。AOP 是 Spring 框架中的一个重要内容,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

在 Spring 中,依赖注入管理和配置应用对象,有助于应用对象之间的解耦。而面向切面编程可以实现横切关注点与它们所影响的对象之间的解耦。

横切关注点:散布在应用中多处的功能,可以被提取出来集中处理。

面向切面编程所要解决的问题是:将横切关注点与应用的业务逻辑相分离。

AOP 常见的场景:日志、声明式事务、安全和缓存。

使用面向切面编程时,在一个地方定义通用功能,然后通过声明的方式定义这个通用功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,该类被称为切面。

好处

  1. 每个关注点集中在一个地方,而不是分散在多处代码中;
  2. 模块更简洁,主要的代码只关注业务逻辑代码;

1、专业术语

通知(Advice)

切面的工作被称为通知通知定义了切面是什么以及何时使用。Spring 含有 5 中类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知;
  • 后置通知(After):在目标方法被调用之后调用通知,此时不关心方法的输出是什么;
  • 返回通知(After-returning):在目标方法成功执行后调用通知;
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  • 环绕通知(Around):在目标方法调用之前和调用之后均调用通知;

连接点(Join point)

连接点是应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。简单理解:一个方法即为一个连接点,只是在这个连接点调用通知的时间点可以自定义。

切点(Pointcut)

切点是一定数量的连接点;切点定义所要织入通知的一个或多个连接点。Spring 基于动态代理,只支持方法连接点。切点指明目标方法,当目标方法调用执行时,会调用对应的通知。

切面(Aspect)

切面是通知和切点的结合。通知和切点共同定义了切面的功能、在何时和何地完成功能。

引入(Introduction)

向现有的类添加新的方法或属性称为引入。

织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。目标对象的生命周期里有多个点可以织入切面:

  • 编译期:切面在目标类编译时被织入;需要使用特殊的编译器,比如:AspectJ 的织入编译器。
  • 类加载器:切面在目标类加载到 JVM 时被织入;
  • 运行期:切面在应用运行在某个时刻被织入;一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。

2、Spring的AOP实现

Spring 提供了 4 种类型的 AOP 支持:

  • 基于代理的经典 Spring AOP;
  • 纯 POJO 切面;
  • @AspectJ 注解驱动的切面;
  • 注入式 AspectJ 切面(适用于 Spring 各版本);

前三种都是 Spring AOP 实现的变体,Spring AOP 构建在动态代理基础之上,因此,Spring 对 AOP 的支持局限于 方法拦截。

Spring 通知是用标准的 Java 类编写的。定义通知所应用的切点通常使用注解或在 Spring XML 配置文件中编写

Spring 在运行时通知对象。Spring 的切面由包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。直到装配需要被代理的 Bean 时,Spring 才会创建代理对象

2.1 AspectJ 指示器

前面讲到,切点作用是用于定位,在所在位置执行时调用切面的通知。在 Spring AOP 中,要使用 AspectJ 的切点表达式来定义切点;而在 Spring AOP 所支持的 AspectJ 切点指示器有:

指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法;
@args() 限制连接点匹配参数由指定注解标注的执行方法;
execution() 用于匹配是连接点的执行方法;
this() 限制连接点匹配 AOP 代理的 Bean 引用为指定类型的类;
target() 限制连接点匹配目标对象为指定类型的类;
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解;
within() 限制连接点匹配指定的类型;
@within() 限制连接点匹配指定注解所标注的类型;
@annotation() 限定匹配带有指定注解的连接点;
@bean() 限定连接点匹配指定 ID 的 Bean;

上述指示器中,除了 execution 之外,均用于匹配连接点;而 execution 指示器是执行通知:当括号内的连接点对应的方法被调用时,execution 会执行对应的通知操作

2.2 使用注解创建切面

2.2.1 创建切面类

AspectJ 5 引入了重要的特性:使用注解创建切面。要在类中使用注解创建切面,首先,必须要导入对应的 jar包( aspectjweaver.jar ),下面,我们来创建一个切面类:

  1. import org.aspectj.lang.annotation.Aspect;
  2. @Aspect
  3. public class AspectClass {
  4. }

使用注解 AspectJ 表明,这个类不仅仅是一个简单的 Java 类,还是一个切面。但是前面说过,一个完整的切面应该包含切点和通知

2.2.2 定义通知方法

在切面类中,5 种通知类型分别对应 5 种注解,使用注解标注方法定义通知方法,这些注解分别是:

注解 通知
@After 目标方法返回或抛出异常后调用
@AfterReturning 目标方法返回后调用
@AfterThrowing 目标方法抛出异常后调用
@Around 将目标方法封装起来
@Before 目标方法调用之前执行

使用注解定义通知

  1. import org.aspectj.lang.annotation.After;
  2. import org.aspectj.lang.annotation.Aspect;
  3. import org.aspectj.lang.annotation.Before;
  4. @Aspect
  5. public class AspectClass {
  6. @Before()
  7. public void before(){
  8. System.out.println("前---置通知");
  9. }
  10. @After()
  11. public void before(){
  12. System.out.println("后---置通知");
  13. }
  14. @Around()
  15. public Object around(ProceedingJoinPoint jp){
  16. System.out.println("环绕通知---前");
  17. Object proceed=null;
  18. try {
  19. Object[] args = jp.getArgs(); //获取传入目标方法的参数
  20. proceed = jp.proceed(); //调用执行目标方法
  21. } catch (Throwable throwable) {
  22. throwable.printStackTrace();
  23. }
  24. System.out.println("环绕通知---后");
  25. return proceed;
  26. }
  27. }

其中,环绕通知的方法定义不同。环绕通知方法中需要传入一个参数 ProceedingJoinPoint 接口,该接口控制目标方法:

  • getArgs() 获取目标方法的形参;
  • 通过 proceed() 执行目标方法,会返回目标方法的返回值;如果不调用此方法,会阻塞目标方法的调用;也可以多次调用
2.2.3 编写切点表达式

在注解内放入切点表达式。所谓切点表达式,是使用指示器匹配连接点。切点表达式中需要使用 execution 指示器(如下图)。

不同的指示器之间可以使用逻辑运算(and、or、not)拼接一起使用

下面,我们为切面添加切点,当执行器内指定的目标方法执行时会调用对应的通知方法。

  1. import org.aspectj.lang.annotation.After;
  2. import org.aspectj.lang.annotation.Aspect;
  3. import org.aspectj.lang.annotation.Before;
  4. @Aspect
  5. public class AspectClass {
  6. @Before("execution(* 包名.类名.目标方法())")
  7. public void before(){
  8. System.out.println("前---置通知");
  9. }
  10. @After("execution(* 包名.类名.目标方法())")
  11. public void after(){
  12. System.out.println("后---置通知");
  13. }
  14. @Around("execution(* 包名.类名.目标方法())")
  15. public Object around(ProceedingJoinPoint jp){
  16. System.out.println("环绕通知---前");
  17. Object proceed=null;
  18. try {
  19. proceed = jp.proceed();
  20. } catch (Throwable throwable) {
  21. throwable.printStackTrace();
  22. }
  23. System.out.println("环绕通知---后");
  24. return proceed;
  25. }
  26. }

还有一种方式可以简化编写切点。在上面这个例子中,每个通知中都使用切点表达式来匹配连接点,这样做很繁琐。使用 @Poingcut 注解标注,为一个方法编写切点,然后在通知注解中引用切点方法即可

  1. import org.aspectj.lang.ProceedingJoinPoint;
  2. import org.aspectj.lang.annotation.*;
  3. @Aspect
  4. public class AspectClass {
  5. //切点方法
  6. @Pointcut("execution(* 包名.类名.目标方法())")
  7. public void work() {
  8. }
  9. @Before("work()")//引用切点方法
  10. public void before() {
  11. System.out.println("前---置通知");
  12. }
  13. @After("work()")//引用切点方法
  14. public void after() {
  15. System.out.println("后---置通知");
  16. }
  17. @Around("work()")//引用切点方法
  18. public Object around(ProceedingJoinPoint jp) throws Throwable {
  19. System.out.println("环绕通知---前");
  20. Object proceed = null;
  21. Object[] args = jp.getArgs();
  22. jp.proceed();
  23. System.out.println("环绕通知---后");
  24. return proceed;
  25. }
  26. }
2.2.4 启动自动代理

到此为止,一个切面就创建好了。但是,如果没有启用自动代理功能,这个切面只能被当做一个 Bean,AspectJ 注解也不会被解析。在 JavaConfig 配置类和 XML 配置文件启动自动代理功能

  1. import org.springframework.context.annotation.Bean;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.context.annotation.EnableAspectJAutoProxy;
  4. @Configuration
  5. //(1)启动 AspectJ 自动代理
  6. @EnableAspectJAutoProxy
  7. public class WorkAspectConfig {
  8. //(2)声明切面类的bean
  9. @Bean
  10. public AspectClass getAspectClass(){
  11. return new AspectClass();
  12. }
  13. }
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xmlns:aop="http://www.springframework.org/schema/aop"
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/aop/spring-context.xsd
  10. http://www.springframework.org/schema/aop
  11. http://www.springframework.org/schema/aop/spring-aop.xsd">
  12. <aop:aspectj-autoproxy/><!--(1)启用 AspectJ 自动代理-->
  13. <bean class="AspectClass"/><!--(2)声明切面类的bean-->
  14. </beans>
2.2.4 处理通知中的参数

在此之前,我们在讲到前置通知和后置通知的方法定义中都是没有参数,除了环绕通知,可以通过 ProceedJoinPoint 接口获取目标方法的参数和传递返回值。但,如果前置通知和后置通知的方法中定义了参数,那么,切面如何访问和使用目标方法中的参数呢?

  1. @Before("execution(* 包名.类名.目标方法名(Type...)) && args(name,...)")
  2. public void work(Type name,...){
  3. }

不同之处在于:

  • Execution 指示器内的方法需要指定参数类型,这个参数类型与传入通知方法的参数类型匹配
  • 使用逻辑运算符 && (或 ||、!拼接一个 args 指示器,这个指示器内指定参数名,表明传递给目标方法的 Type 类型参数也会传递给通知方法。
  • args 指示器内的参数名称与通知方法的名称要一致。
2.2.5 添加新功能

上面已经讲解讲解了:如何为现有方法添加额外的功能。那现在我们需要为一个对象添加新的方法,如何实现呢?具体步骤如下:

  1. 创建一个新的接口,接口内定义了新功能的方法;并创建新接口的实现类;
  2. 创建一个新的切面类,该类内定义一个步骤1声明接口的静态属性,该属性使用注解 @DeclareParents 标注;
  3. 在配置文件中装配切面类的 Bean,以及新接口的实现类的 Bean;

这样就完成了为一个现有对象添加了新方法。注意:要使用新方法,对象需要强制转换为新接口类型。在这里需要重点了解的是:@DeclareParents 的使用

  1. //@DeclareParents 由三部分组成
  2. @DeclareParents(value="package.OldInterface+"
  3. defaultImpl= newClassImplNewInterface.class)
  4. public static NewInterface newInterface;
  • value :指定需要添加新功能的类,+表示 OldInterface 类的所有子类;
  • defaultImpl :指定添加了新功能的实现类;
  • @DeclareParents 注解标注的静态属性:指明了要引入新功能的接口;

2.3 XML 配置创建切面

在 Spring 的 XML 配置文件中, aop 命名空间提供了元素用来声明切面,如表:

AOP配置元素 用途
<aop:aspect-autoproxy> 启用 @AspectJ 注解驱动的切面
<aop:config> 顶层AOP配置元素。大多数aop元素必须在该元素内
<aop:aspect> 定义一个切面
<aop:advisor> 定义AOP通知器
<aop:after> 定义AOP后置通知
<aop:after-returning> 定义AOP返回通知(不管目标方法是否执行成功)
<aop:after-throwing> 定义AOP异常通知
<aop:around> 定义AOP环绕通知
<aop:before> 定义AOP前置通知
<aop:declare-parents> 以透明的方式为目标对象引入额外的接口
<aop:pointcut> 定义一个切点

已经了解了 XML 配置的基本使用元素。由于前面对切面的了解已经比较深入,现在了解如何使用 XML 配置 AOP,暂不深入过多。直接上例子:

1、原有代码,需要在现有代码中添加新功能。简称:目标对象、目标方法。

  1. package xml;
  2. public class Work {
  3. public String working(){
  4. System.out.println("工作ing");
  5. return "工作ing";
  6. }
  7. public void working(int time){
  8. System.out.println("工作时长:"+time);
  9. }
  10. }

2、要向目标方法添加的新功能类,并以此类添加切面

  1. package xml;
  2. import org.aspectj.lang.ProceedingJoinPoint;
  3. public class AspectClass {
  4. public void before() {
  5. System.out.println("前---置通知");
  6. }
  7. public void after() {
  8. System.out.println("后---置通知");
  9. }
  10. public Object around(ProceedingJoinPoint jp) throws Throwable {
  11. System.out.println("环绕通知---前");
  12. Object proceed = null;
  13. //(1)获取目标方法的参数
  14. Object[] args = jp.getArgs();
  15. //(2)调用目标方法,并获取返回值
  16. proceed = jp.proceed(args);
  17. System.out.println("环绕通知---后");
  18. return proceed;
  19. }
  20. public void afterWork(int time) {
  21. if (time > 0 && time < 8) {
  22. System.out.println("工作时长不够8小时");
  23. }else{
  24. System.out.println("工作时长:"+time);
  25. }
  26. }
  27. }

3、要向目标对象添加新的方法

  1. package xml;
  2. public interface OtherWork {
  3. void addWorkTime();
  4. }
  1. package xml;
  2. public class NightWork implements OtherWork {
  3. @Override
  4. public void addWorkTime() {
  5. System.out.println("加夜班");
  6. }
  7. }

4、编写 XML 配置文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xmlns:aop="http://www.springframework.org/schema/aop"
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/context/spring-context.xsd
  10. http://www.springframework.org/schema/aop
  11. http://www.springframework.org/schema/aop/spring-aop.xsd">
  12. <!--原有对象,需要添加新功能的目标方法-->
  13. <bean class="xml.Work" id="w"/>
  14. <!--创建Bean ,该Bean要作为切面类-->
  15. <bean class="xml.AspectClass" id="aspectClass"/>
  16. <!--对引入新方法的接口的实现类创建bean-->
  17. <bean class="xml.NightWork" id="nightWork"/>
  18. <!--AOP配置-->
  19. <aop:config>
  20. <!--定义一个切面-->
  21. <aop:aspect ref="aspectClass">
  22. <!--<aop:before method="before"
  23. pointcut="execution(* xml.Work.working())"/>-->
  24. <!--定义切点-->
  25. <aop:pointcut id="work_pointcut"
  26. expression="execution(* xml.Work.working())"/>
  27. <!--定义通知-->
  28. <aop:before method="before" pointcut-ref="work_pointcut"/>
  29. <aop:after method="after" pointcut-ref="work_pointcut"/>
  30. <aop:around method="around" pointcut-ref="work_pointcut"/>
  31. <!--定义带有参数的通知-->
  32. <aop:after method="afterWork"
  33. pointcut="execution(* xml.Work.working(int)) and args(time)"/>
  34. <!--向原有对象中添加新的方法-->
  35. <aop:declare-parents types-matching="xml.Work"
  36. implement-interface="xml.OtherWork"
  37. default-impl="xml.NightWork"/>
  38. </aop:aspect>
  39. </aop:config>
  40. </beans>

6、测试类

  1. package test;
  2. import xml.OtherWork;
  3. import xml.Work;
  4. import org.junit.Test;
  5. import org.junit.runner.RunWith;
  6. import org.springframework.context.ApplicationContext;
  7. import org.springframework.context.support.ClassPathXmlApplicationContext;
  8. import org.springframework.test.context.ContextConfiguration;
  9. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  10. @RunWith(SpringJUnit4ClassRunner.class)
  11. @ContextConfiguration(locations = "classpath:xml/AspectConfig.xml")
  12. public class WorkTest {
  13. @Test
  14. public void test(){
  15. //解析XML配置文件
  16. ApplicationContext app =new ClassPathXmlApplicationContext("xml/AspectConfig.xml");
  17. //测试目标方法的新功能
  18. Work work = (Work) app.getBean("w");
  19. work.working(6);
  20. //测试目标对象的新方法
  21. OtherWork nightWork = (OtherWork) work;
  22. nightWork.addWorkTime();
  23. }
  24. }
  25. //测试结果:
  26. // 工作时长:6
  27. // 工作时长不够8小时
  28. // 加夜班

到此为止,Spring AOP的基础学习完毕。

Spring基础(二)_面向切面(AOP)的更多相关文章

  1. Spring框架系列(五)--面向切面AOP

    背景: 当需要为多个不具有继承关系的对象引入一个公共行为,例如日志.权限验证.事务等功能时,如果使用OOP,需要为每个对象引入这些公共 行为.会产生大量重复代码,并且不利用维护.AOP就是为了解决这个 ...

  2. Spring(三)面向切面编程(AOP)

    在直系学长曾经的指导下,参考了直系学长的博客(https://www.cnblogs.com/WellHold/p/6655769.html)学习Spring的另一个核心概念--面向切片编程,即AOP ...

  3. Spring xml中进行面向切面的配置

    Spring xml中进行面向切面的配置 XML: <?xml version="1.0" encoding="UTF-8"?> <beans ...

  4. C#_02.12_基础二_.NET类型存储和变量

    C#_02.12_基础二_.NET类型存储和变量 一.核心一句:C#程序是一组类型声明(留待后面慢慢体会,现在不是很理解,不强说了) 二.数据类型: 1.预定义了16种数据类型: 其中13种简单数据类 ...

  5. Spring学习手札(二)面向切面编程AOP

    AOP理解 Aspect Oriented Program面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术. 但是,这种说法有些片面,因为在软件工程中,AOP的价值体现的并 ...

  6. Spring框架使用(控制反转,依赖注入,面向切面AOP)

    参见:http://blog.csdn.net/fei641327936/article/details/52015121 Mybatis: 实现IOC的轻量级的一个Bean的容器 Inversion ...

  7. Spring学习笔记:面向切面编程AOP(Aspect Oriented Programming)

    一.面向切面编程AOP 目标:让我们可以“专心做事”,避免繁杂重复的功能编码 原理:将复杂的需求分解出不同方面,将公共功能集中解决 *****所谓面向切面编程,是一种通过预编译方式和运行期动态代理实现 ...

  8. 解析Spring第三天(面向切面AOP)

    面向切面:AOP 在不修改源代码的基础上,对方法进行增强.AOP的底层原理就是代理技术(第一种:jdk的动态代理(编写程序必须要有接口).第二种:cglib代理技术(生成类的子类).如果编写的程序有借 ...

  9. Spring实战4:面向切面编程

    主要内容 面向切面编程的基本知识 为POJO创建切面 使用@AspectJ注解 为AspectJ的aspects注入依赖关系 在南方没有暖气的冬天,太冷了,非常想念北方有暖气的冬天.为了取暖,很多朋友 ...

随机推荐

  1. 【Python3爬虫】反反爬之解决前端反调试问题

    一.前言 在我们爬取某些网站的时候,会想要打开 DevTools 查看元素或者抓包分析,但按下 F12 的时候,却出现了下面这一幕: 此时网页暂停加载,也就没法运行代码了,直接中断掉了,难道这就能阻止 ...

  2. 啊哈!C语言课后参考答案上

    最近看到一本好评量很高的的C语言入门书,课本真的很好,入门的话.专业性没有那么强,但入门足够了!!好评!看着看着就想把这本书的题课后习题都写出来,最后就有了这个小结.可能有的不是最好,不那么专业,但主 ...

  3. 侠说java8-行为参数化(开山篇)

    啥是行为参数化 行为参数化的本质是不执行复杂的代码块,让逻辑清晰可用. 相信使用过js的你肯定知道,js是可以传递函数的,而在 java中也有类似的特性,那就是匿名函数. 理解:行为参数化是一种方法, ...

  4. HTTP请求中的GET-POST方式

    目录 一.前言部分(概念) 二.对比 GET 与 POST 二者最大的差异 GET 与 POST 请求本质上并无区别 深层了解:POST 请求产生两个数据包? 三.两种请求方式如何灵活使用? 四.常见 ...

  5. 51nod 1086背包问题V2 (完全背包模板题)

    1086 背包问题 V2 1 秒 131,072 KB 20 分 3 级题 题目描述 有N种物品,每种物品的数量为C1,C2......Cn.从中任选若干件放在容量为W的背包里,每种物品的体积为W1, ...

  6. docker发布.net core程序的坑

    docker发布遇到的两个问题 1:Could not resolve CoreCLR path. For more details, enable tracing by setting COREHO ...

  7. asp.net core 3.x 通用主机是如何承载asp.net core的-上

    一.前言 上一篇<asp.net core 3.x 通用主机原理及使用>扯了下3.x中的通用主机,刚好有哥们写了篇<.NET Core 3.1和WorkerServices构建Win ...

  8. Magicodes.IE之Excel模板导出教材订购表

    说明 本教程主要说明如果使用Magicodes.IE.Excel完成教材订购表的Excel模板导出. 要点 本教程使用Magicodes.IE.Excel来完成Excel模板导出 需要通过创建Dto来 ...

  9. 7个效果震憾的HTML5应用组件

    在HTML5的世界里,任何文本.图像都可以变得令人难以想象,很多HTML5应用也都已经随着浏览器的升级而变得运行飞速,而且兼容性也越来越好.下面为大家介绍7款效果震憾的HTML5应用组件,HTML5是 ...

  10. 数据结构与算法 Python语言实现 第一章练习

    说明:部分代码参考了Harrytsz的文章:https://blog.csdn.net/Harrytsz/article/details/86645857 巩固 R-1.1 编写一个Python函数 ...