11、面向切面的Spring(1)(spring笔记)

在生活中,监控用电量是一个很重要的功能,但并不是大多数家庭重点关注的问题。软件系统的一些功能就像家里的电表一样,这些功能需要用到应用程序的多个地方,但是我们又不想在每个点都明确调用它们。日志、安全和事务管理的确都很重要,但它们是否为应用对象主动参与的行为呢?在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的。

一、什么是面向切面编程

图中展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,如安全和事务管理。


1
  • 如果要重用通用的功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。

  • 切面提供了取代继承和委托的另一种可选方案。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无须修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。

1.1 定义AOP术语

描述切面术语有通知(advice)、切点(pointcut)和连接点(join point),如图所示:

2

1.1.1 通知(advice)

电表抄表员需要统计每家每户的用电量,而记录用电量是其主要的工作。类似的,切面也有目标——它必须要完成的工作。在AOP术语中,切面的工作被称为通知。其实就是整个程序执行过程中要插入的具体的功能,如日志等。
Spring切面中,可以应用五种类型的通知:

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

1.1.2 连接点(Join point)

通知可能需要在程序的很多时机应用。这些时机被称为连接点。连接点是应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

1.1.3 切点(Pointcut)

如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。一般使用明确的类和方法名称或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

1.1.4 切面(Aspect)

切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

1.1.5 引入(Introduction)

引入允许我们向现有的类添加新方法或属性。如我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简单,只需一个方法setLastModified(Data)和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类中,从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。

1.1.6 织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的声明周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入引用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

总结:如日志记录、安全性检查等这些问题,我们可以称作横切性关注点,而将这个横切性的问题模块化为一个类,这个类就叫做切面(Aspect);切面中有很多内容,其中有一个内容就是实际处理如日志记录的方法,这个方法就叫做通知(Advice);而这个方法要应用在那些地方,这需要规定一个范围,这个范围就是切点(Pointcut);而这个范围中有很多连接点(Join Point),即应用在哪些方法或类上(spring只支持方法);而通知(或切面)应用的过程就叫织入(Weave)。

1.2 Spring对AOP的支持

Spring中提供了四种类型的AOP支持(其中,前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础上,因此,SpringAOP的支持局限于方法拦截):

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

由于Spring经典的AOP不怎么样,这里就不再细说。借助Springaop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件时所要调用的方法。遗憾的是,这种技术需要XML配置,但是这的确是声明式地将对象转换为切面的简便方式。

Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。这种方式不需要使用XML配置。

1.2.1 Spring 通知是 Java 编写的

Spring所创建的通知都是标准的Java类编写的。而AspectJ最初是以Java语言扩展的方式实现的,这种方式有优点也有缺点。通过特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集。

1.2.2 Spring在运行时通知对象

3

通过在代理类中包裹切面,Spring在运行期把切面织入到Springbean中。如图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。直到应用需要被代理的bean时,才会创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContextBeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。

1.2.3 Spring只支持方法级别的连接点

因为Spring基于动态代理,所以只支持方法连接点。而像AspectJ,除了方法切点,还提供了字段和构造器连接点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它还不支持构造器连接点,我们就无法在bean创建时应用通知。

但是方法拦截可以满足绝大部分需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用AspectJ来补充Spring AOP功能。

二、通过切点来选择连接点

Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。关于Spring AOPAspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器的一个子集。下表是Spring AOP所支持的AspectJ切点指示器。

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

Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgumentException异常。其中只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的,这表示execution是我们在编写切点定义时最主要使用的指示器,在此基础上,我们使用其他指示器显示所匹配的切点。

2.1 编写切点

为了阐述Spring中的切面,举例说明。

package concert;
public interface Performance{
    public void perform();
}

说明:这个接口可以表示任何类型的现场表演,如果我们想编写Performanceperform()方法出发的通知,如图所示:

4

这里使用execution()指示器选择Performanceperform()方法。方法表达式以"*"号开始,表明了我们不关心方法返回值的类型。然后指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)标明切点要选择任意的perform()方法,无论该方法的入参是什么。

现在假设需要配置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限定匹配,如图所示。

5

还可以使用"||"操作符表示或(or)关系,而使用"!"操作符来标识非(not)操作。因为"&"XML中有特殊的含义,所以在XML中配置时需要使用and来替代"&&",同样,ornot可以分别用来替代"||""!"

2.2 在切点中选择bean

可以使用Springbean()指示器,它允许我们在切点表达式中使用beanID来标识bean。如下:

execution( * concert.Performance.perform(..)) and bean('woodstock')
execution( * concert.Performance.perform(..)) and !bean('woodstock')

说明:第一种表示在应用通知时,限定beanIDwoodstock。第二种表示可以限定为除了特定ID以外的其他bean引用通知。

三、使用注解创建切面

3.1 定义切面

使用注解来创建切面是AspectJ 5所引入的关键特性。

6

说明:从上图中可以清晰看到如何使用注解来声明通知方法(注意:图中execution右边应该是一个星号,书中可能有误)。Spring使用AspectJ注解来声明通知方法有几种方式:

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

可能会注意到,所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。其实它们可以设置成不同的切点表达式,但是此处却是一样的。相同的切点表达式重复多次可不是很好,这里我们可以使用@Pointcut注解定义一个重用的切点。

7

说明:首先定义了一个切点,这样就可以在任何切点表达式中使用performance()方法了,此方法的实际内容并不重要,只是供@Pointcut注解依附。需要注意的是,除了注解和没有实际操作的performance()方法,Audience类依然是一个POJO,可以像使用其他的Java类那样调用它的方法,也能够独立的进行测试(注意:图中execution右边应该是一个星号,书中可能有误)。还可以装配为Spring中的bean

@Bean
public Audience audience(){
  return new Audience();
}

但是如果就此止步的话,Audience只会是Spring容器中的一个bean,即便使用了AspectJ注解,但并不会被视为切面。如果使用JavaConfig的话,需要使用@EnableAspectJAutoProxy注解启用自动代理功能。

package concert;
import org.springframework.context.annotation.*;

@Configuration
@EnableAspectJAutoProxy/*启用AspectJ自动代理*/
@ComponentScan
public class ConcertConfig {

    @Bean
    public Audience andience(){
        return new Audience();
    }
}

而如果是在XML中配置,则如下:

<context:component-scan base-package="concert" />
<aop:aspectj-autoproxy />
<bean class="concert.Audience" />

说明:SpringAspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。本质上,它依然是Spring基于代理的切面。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 本章内容: 面向切面编程的基本原理 通过POJO创建切面 使用@AspectJ注解 为AspectJ切面注入依赖 ...
    谢随安阅读 3,132评论 0 9
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 1.以《三言两拍》中“二拍”的第一篇为标尺,确定出自己的阅读层次,并据此制定一个阅读套餐。(文言几成+旧小说几成+...
    静待花开_7b5d阅读 230评论 1 1
  • 一个姑娘可能太爱她的男朋友了,就在他的颈上留下一枚深深的吻痕。谁成想太用劲,把静脉血管咬破了,造成脑供血不足而昏迷...
    lady愚阅读 300评论 0 2
  • 当看到街边的花店门口,摆着大束束的玫瑰花的时候,我知道情人节即将来临;大小商家的微信公众号上醒目的活动促销,告诉我...
    苏城明月阅读 518评论 0 0