内容概览
- 什么是面向切面的编程
- 通过切点来选择连接点
- 使用注解创建切面
- 在xml中声明切面
- 总结
1. 什么是面向切面的编程
前面的内容我们讨论和学习了Spring的依赖注入,依赖注入有助于应用对象之间的解耦。这部分内容我们主要了解Spring AOP(面向切面的编程)。那么什么是aop呢?我们先回顾一下OOP:Object Oriented Programming,OOP作为面向对象编程的模式,获得了巨大的成功,OOP的主要功能是数据封装、继承和多态。
AOP是一种新的编程方式,在软件开发过程中,最重要的就是业务,我们入职了解公司的老项目也肯定先是从业务开始了解的。比如在电商系统中,有很多和业务相关的主要组成部分,如用户,商品,订单等等。这些东西的状态和行为的正确性保证了系统的正常运行。不过除了这些东西,还有一些点值得我们关注,比如打日志、安全、事务处理等等,这些东西既和每一个组成成员密切相关,也完全可以在系统级的层面抽象出来,如果让组成成员对象在操作这些内容的时候也事必躬亲,那么可以想象系统会多么臃肿,修改起来多么麻烦,显然,让组成成员对象只关注业务的处理,而一些功能点内容让应用程序对象来处理,是更好的选择。
这些散布于项目应用多个地方的功能被称为 横切关注点(cross-cutting concern)。这些横切关注点从概念上来说是与业务逻辑相分离的,但是又会嵌入到各个业务逻辑当中,把横切关注点与业务逻辑分离开正是面向切面的编程(AOP)所要解决的问题。
spring aop 能帮助我们模块化横切关注点,就是把日志,事务等内容抽取出来单独模块化,这些关注点影响了程序的很多地方。比如系统安全的开发,事务等等,就是横切关注点:
上面的图片很形象,每个业务(Service)都可以看做一个业务功能模块,每个横切关注点功能(安全、事务等等)整体抽象出来也可以各自看作一个模块,这些横切关注点模块可以为其它业务模块提供服务。
在使用面向切面的编程时,我们可以在一个地方定义通用功能(例如打日志),但是可以通过声明的方式定义这种功能要以何种方式在何处使用,而无需修改原有的业务逻辑类。横切关注点可以被模块化为特殊类,这些类被称为切面(Aspect),这样做有两个好处,首先,每个关注点都集中定义在一个地方,而不是各个业务模块各自开发各自的,然后就是业务代码中会更加简洁,它只声明使用的代码,关注点的定义代码被转移到了切面中。
了解了面向切面的编程,我们来看一下AOP中的一些术语。和大多数技术类似,AOP已经形成了自己的术语,描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)等等,如图展示了它们之间的关联:
虽然这些术语已经是业内行话,但是其实描述的并不直观。不过为了学习AOP,必须接受这些新概念。
通知(advice),每个切面都有各自要完成的工作(记录日志、事务等),切面的工作被称为通知。通知定义了切面是什么以及什么时候使用,同时,通知还解决了何时执行这个工作的问题。它是应该应用在某个方法被调用前还是调用后,还是前后都调用,还是只在方法抛出异常时调用等等。Spring切面有五种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知环绕了目标方法,在目标方法调用之前和调用之后都调用通知
连接点(join point),每个业务方法都有可能产生打日志或者事务等行为,这些行为产生的时机被称为连接点,连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时,或者抛出异常时,甚至是修改一个字段的时候,切面代码可以利用这些点插入到应用的正常流程中,并添加新的行为。
切点(pointcut),切点有助于缩小切面所通知的连接点的范围。如果说通知定义了切面要完成什么工作和何时执行的话,那么切点就定义了在什么地方执行。切点的定义会匹配通知(advice)所要织入的一个或多个连接点,通常用明确的类和方法名称(或是用正则表达式定义所匹配的类或方法名称)来指定这些切点。有些AOP框架允许我们创建动态切点,可以根据运行时的决策(比如方法的参数值)来决定是否调用通知。
切面(Aspect),切面是通知(advice)和切点(pointcut)的结合,通知和切点共同定义了切面的全部内容(它是什么,在何时和何处完成功能)。
引入(Introduction),引入允许我们向现有的类添加新的方法和属性。例如,用户信息可能会经常更新,我们可以创建一个通知类,记录最后一次对用户的修改时间和修改内容,这个功能需要一个方法就可以实现此要求:setLastChange(context) ,同时还需要一个实例变量来保存数据。这样的话,这个方法和实例变量就可以被引入到现有的类中,从而可以在无需修改现有类的情况下,让它们具有新的行为和状态。
织入(Weaving),织入是把切面应用到目标对象,并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器,AspectJ的织入编译器就是以这种方式织入切面的;
类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载期(ClassLoader),它可以在目标类被引入应用之前增强改目标类的字节码。AspectJ 5 的加载时织入(load-time weaving, LTW)就支持以这种方式织入切面;
运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态的创建一个代理对象。Spring AOP就是以这种方式织入切面的。
上面是一些基本的AOP术语,总结一下,通知包含了需要用于多个应用对象的横切行为,连接点是程序执行过程中能够应用通知的所有点,切点定义了通知被应用的具体位置(在哪些连接点),关键的概念是切点定义了哪些连接点会得到通知。下面来看一下Spring是如何实现AOP这些概念的。
AOP有很多框架,但是不是所有的都是相同的,它们在连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知(调用通知方法),还有些只支持与方法相关的调用和连接点,它们织入切面的方式和时机也有所不同,但是无论如何,创建切点来定义切面所织入的连接点是AOP框架的基本功能。虽然这篇文章讨论的是Spring AOP,但是Spring和AspectJ项目之间有大量的协作,而且Spring 对 AOP 的支持在很多方面借鉴了AspectJ项目。具体来说,Spring提供了四种类型的AOP支持:
- 基于代理的经典Spring AOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用于Spring各个版本)
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此Spring对AOP的支持局限于方法拦截。“经典”通常意味着不错的东西,但是经典Spring AOP编程模型并不怎么样。虽然它曾经不错,但是现在Spring提供了更简洁和干净的面向切面编程方式。引入了简单的声明式AOP和基于注解的AOP之后,Spring经典AOP就显得很笨重复杂了,所以下面不再讨论Spring经典AOP。
借助Spring 的aop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件时所有调用的方法。遗憾的是,这种技术需要xml方式的配置,不过这的确是声明式的将对象转换为切面的简便方式。
Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于注解的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。这种AOP风格的好处在于能够不使用xml来完成功能。
如果你的AOP需求超过了简单的方法调用(如构造器或者属性拦截等),那么可以考虑使用AspectJ来实现切面。在这种情况下,上面所说的第四种方式(注入式AspectJ切面)能够帮助你将值注入到AspectJ驱动的切面中。
上面是四种Spring AOP的实现,下面来了解一下Spring AOP框架的一些关键知识:
- Spring 通知是Java编写的
- Spring 在运行时通知对象
- Spring只支持方法级别的连接点
来看上面第一个关键知识,Spring所创建的通知都是用标准的Java类编写的。这样的话,我们就可以使用与普通Java开发一样的开发环境来开发切面。而且,定义通知所应用的切点通常会使用注解或在Spring配置文件里采用xml配置来编写,这两种语法对于Java程序员都没有问题。AspectJ与之相反,虽然AspectJ现在支持基于注解的切面,但是AspectJ最初是以Java语言扩展的方式编写的,这种方式优缺点都有。通过特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集,但是我们需要额外学习新的工具和方法。
来看上面第二个关键知识,通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如图所示:
直到应用需要被代理的bean时,Spring才创建代理对象,如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
来看上面第三个关键知识,Spring只支持方法级别的连接点。正如前面所探讨过的,通过使用各种AOP方案,可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其它的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点,Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改,而且它不支持构造器连接点,我们就无法在bean创建时应用通知。不过方法拦截已经可以满足绝大部分需求了,如果实在需要方法拦截之外的连接点拦截功能,我们可以利用AspectJ来补充Spring AOP的功能。
2. 通过切点来选择连接点
正如前面提到的,切点用于准确定位应该在什么地方应用切面的通知(缩小连接点范围),通知和切点是切面的最基本元素。因此学习如何编写切点非常重要。说白了切点就是具体定义切面应用在哪些类的哪些方法上。
在Spring AOP中,需要使用AspectJ的切点表达式语言来定义切点。下面来快速介绍一下如何编写AspectJ的切点,关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。Spring AOP是基于代理的(只到方法级别),而某些切点表达式是与基于代理的AOP无关的,下面是Spring AOP所支持的AspectJ切点指示器:
在Spring中尝试使用AspectJ其它指示器时,将会抛出IlleaglArgument-Exception异常。上面的指示器中,只有execution是实际执行匹配的,而其它指示器都是用来限制匹配的,这说明execution指示器是我们在编写切点定义时,最主要使用的指示器。在使用它的基础上,再使用其它的来限制所匹配的切点。
下面来举个实际切点的例子,我们来实际编写一个业务方法接口:
上面的方法不用管什么意义,可以理解为一个类似增删改查的业务方法,如果我们想在这个方法执行时,触发通知,那么切点表达式可以像下面这样写(表达式具体在哪里写先不用管):
上面使用了execution()指示器,后面的括号里是对具体哪些方法的选择,通过方法返回类型,方法类的路径,方法的名字,以及参数列表等内容定义了非常具体的范围内的方法,其中*表示不关心方法返回类型,可以是任意的类型, concert.Performance.perform 制定了全限定类名和方法名,对于方法的参数列表,我们使用两个点(..)表名切点忽略参数类型和个数,要选择方法内任意的perform方法,无论参数是什么。
再来看一个within指示器的例子,它用来限制连接点匹配指定的类型,假设我们需要配置的切点除了指定范围内的perform方法,还必须在concert包下,此情况下可以使用within()指示器来限制匹配,如图:
注意 && 逻辑符号与 and 一样,表示必须满足两个条件,类似的可以使用 || 符号来表示或(or)的意思,使用!来表示非(not)。
上面举了两个指示器的用法例子,除了上面列出的指示器外,Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的 id 来标识bean。bean()使用bean的id或者名字作为参数来限制切点只匹配特定的bean。例如:
上面表示我们匹配的perform方法的 bean的 id 必须是 woodstock。这种限定id在某些场景下很有意义,除此之外,还可以换个逻辑符号,使用 ! 来排处某个bean 的id:
这样触发通知时,就会排出范围内,id为woodstock的bean。这里其它指示器不再举例。
3. 使用注解创建切面
上面的内容在理论上介绍了Spring的AOP和切点定义,下面来看几个实际的例子,首先看通过注解的方式来创建一个切面。首先定义一个普通的业务类:
然后使用配置类的方式配置bean:
下面使用注解定义一个切面类:
该类使用@Aspect注解进行标注,该注解表明Audience是一个切面类,并且该类中的方法都使用注解来定义切面的具体行为。类中共有四个方法,定义了业务方法前后可能会执行的操作,这里定义的都是一些简单的打印。这里专门用一个方法 performance 来定义了具体的切点方法,使用的是 @Pointcut注解,而注解中使用了我们上面提过的execution指示器指明方法的范围,正是我们上面的业务类中的方法。下面的三个方法在注解参数中引用了上面的performance()方法,表示引用上面的位置,三个注解 @Before ,@AfterReturning,@AfterThrowing 分别表示在方法执行前,返回后,和抛出异常后执行下面的打印内容。
AspectJ提供了如下几个常用注解来定义通知行为,如图:
本示例代码中,使用到了其中的三个。这些注解后面的括号中,都调用了performance方法的切点表达式,其实每个注解在参数中都可以写各自不同的切点表达式,但是如果表达式一样,那么没必要写多次,使用@Pointcut注解单独定义会是一个比较好的方案,注解下面方法的内容最好是空的。
上面的切面类定义好以后,还是需要把这个类配置成一个bean:
开发切面类的工作虽然完成了,但是目前为止它仍然只是被识别为一个普通的bean,虽然类上面有@AspectJ注解,也不会主动被视为切面类,因为本例中使用的是Java配置类的形式,可以在配置类上面使用@EnableAspectJAutoProxy注解,这样可以启用自动代理功能,这样就可以自动解析切面类的注解:
这样开发和配置工作就做好了,下面看一下测试代码:
在测试代码中,我们执行目标业务方法,来看一下运行结果:
这里在方法的前后分别执行了两个切面中的方法,达到了我们预期的效果,因为运行正常,没有抛出异常,所以@AfterThrowing注解的方法不会被执行。
再来看一个环绕通知的例子,这个切面方法顾名思义就是在方法执行前和执行后都定义执行一些代码。但是和前面的前置通知等稍微有些不同,来看方法:
这个通知功能最强大,可以完全代替前面的几个通知,注意方法需要接收一个参数 ProceedingJoinPoint ,这个参数对象是必须的,它可以将业务方法向下执行下去。 point.proceed() 代码表示正常执行业务方法。而上下两行打印表示在方法执行前后分别做的事情,catch中也可以加入异常后执行的方法。
注意这里如果不调用point.proceed()方法,那么执行就会阻塞在方法调用上,还要注意,你还可以多次调用这个业务方法,这样可以实现一些操作重试的逻辑。来看现在测试代码的执行结果:
上面的代码中,我们的切面方法都是非常简单没有参数的,只有环绕通知里面,使用了ProceedingJoinPoint 作为参数,而其它方法没有任何参数,这是因为我们的业务方法 hello() 中也没有任何参数。下面来写一个有参数的业务方法:
然后来定义这个业务方法的切点表达式:
其中int表示接收int类型的参数,args中表示指定参数为num,注意performanceArgs()方法中也要带着指定的参数。下面来看一个切面通知方法:
我们在这个切面方法的注解和参数中直接写入参数,就可以结合参数编写要操作的内容。来看一下这个业务方法的运行结果(传入参数为99):
4. 在xml中声明切面
下面来看看基于XML的配置实现AOP,在Spring AOP的命名空间中,提供了多个元素用来声明切面,如图:
其中,<aop:aspectj-autoproxy /> 能够自动代理AspectJ注解的通知类,其它元素能够让我们直接在Spring配置中声明切面,而不需要使用注解。
由于是基于xml的实现,因为我们的业务类和切面类中不需要写任何注解,写普通的代码即可:
接下来首先创建xml配置文件,在里面配置两个基本的bean:
上面都是一些正常的类和bean的配置,下面来加入关键的aop配置,第一个是加入自动代理AspectJ注解的元素,第二是配置所有通知:
其中 ref="audience" 引用的就是上面bean的id。下面是前置通知和后置通知。指定了通知表达式和方法。我们在Java配置类中可以把通知表达式提取出来单独定义,这里也可以:
这样aop的配置就完成了,下面写一个测试类并运行查看结果:
按照上面例子的步骤,继续看环绕通知,首先定义一个通知方法:
这个环绕通知的方法和前面的没什么区别,不做介绍。配置环绕通知的标签元素如下:
从规则上来说,和前面的两个通知是一样的,下面来看一下运行效果:
继续来看给通知加入参数,首先来定义一个带参数的业务类:
再来看切面类的通知方法:
最后看一下配置:
这里切点表达式的书写与Java配置类中规则基本一致,不再解释,下面运行测试代码查看结果:
注意在xml中要使用and 不能使用 &&,否则会被解析成实体的开始。
5. 总结
AOP是面向对象编程的强大补充,Spring AOP框架帮我们完成了很多琐碎的功能。Spring框架的基础知识(IOC+AOP)学完了。
代码地址:https://gitee.com/blueses/spring-framework-demo
21-22:spring aop