什么是面向切面编程
切面能够帮助我们模块化横切关注点,横切关注点可以被描述为影响应用多处的功能,下图直观的呈现了横切关注点的概念。
每个模块的核心功能都是为特定业务领域提供服务,但是如果要做到通用功能的话,最常见的面向对象技术十集成或委托,但是如果在整个应用程序中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对象进行复杂的调用。
切面提供了另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做的好处就是每个关注点都集中于一个地方,而不是分散到多处代码中,其次服务模块更加简洁,只需要关注核心代码就行了。
定义AOP术语
- 通知(Advice):通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。因此可以说,切面的工作被称为通知。Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能。
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出。
- 返回通知(After-returning):在目标方法成功执行之后调用通知。
- 异常通知(After-throwing):在目标方阿飞抛出异常后调用通知。
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
- 连接点(Join point):我们的应用可能有数以千计的时机应用通知,这些时机被称为连接点,连接点是在应用程序中能够插入切面的一个点。
- 切点(Pointcut):如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”,切点的定义会匹配通知所要织入的一个或多个连接点。
- 切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容:它是什么,在何时和何处完成其功能。
- 引入(Introduction):引入允许我们向现有的类添加新方法或属性。
- 织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
- 编译器:切面在目标类编译时被织入,AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。
- 运行期:切面在应用运行的某个时刻被织入。Spring AOP就是以这种方式织入切面的。
通过切点来选择连接点
切点用于准确定位应该在什么地方应用切面的通知。通知和切点时切面的最基本元素。在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。下面是Spring AOP所支持的AspectJ切点指示器:
在Spring中尝试使用AspectJ其它指示器时,将会抛出IllegalArgumentException异常。
编写切点
为阐述Spring中的切面,我们需要有个主题来定义切面的切点,为此我们定义一个Performance接口:
1 | package concert; |
Performance可以代表任何类型的现场表演,假设我们想编写Performance的perform()方法触发的通知,下面展示了一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用:
我们使用execution()指示器选择Preformance的perform()方法。方法表达式以“*”号开始,表明我们不关心方法返回值的类型。然后我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。假设我们需要配置的切点仅匹配concert包,可以使用within()指示器来限制匹配。使用“&&”来表示与的关系,“||”和“!”也同理:
在切点中选择bean
Spring引入一个新的bean()指示器,允许我们在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean:
在这里我们希望在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock。
在此场景下,切面的通知会被编织到所有ID不为woodstock的bean中。
使用注解创建切面
我们已经定义了Performance接口,他们是切面中切点的目标对象,现在使用AspectJ注解来定义切面。
定义切面
从演出的角度来看,观众是非常重要的,因此,将观众定义为一个切面,并将其应用到演出上:
1 | package concert; |
Audience类使用@Aspect注解标注,该注解表明Audience不仅仅是一个POJO,还是一个切面。
Audience四个方法定义了一个观众在观看演出时可能会做的事情:手机调至静音、就坐、喝彩鼓掌以及退款。可以看到这些方法都使用通知注解来表明它们应该在什么时候调用,注解都有如下:
你可能注意到所有的这些注解都给定了一个切点表达式作为它的值,同时这四个方法的切点表达式都是相同的。所以我们可以改进一下,如果我们只定义这个切点一次,然后每次需要的时候引用它,那么这会是一个很好的方案。@Poincut注解能够在一个@AspectJ切面内定义可重用的切点:
1 | package concert; |
需要注意的是,除了注解和没有操作的performance()方法,Audience类依然是一个独立的POJO,只不过它通过注解表明会作为切面使用,像其他类一样,它可以装配为Spring中的bean:
1 |
|
但是到此为止的话,@Audience只会是Spring容器中的一个bean,并不会被视为切面,需要将这些注解启用配置到配置文件中:
1 | package concert; |
当然使用XML文件配置也是完全可以的:
1 | / 在XML中,通过Spring的aop命名空间启用AspectJ自动代理 |
创建环绕通知
环绕通知是最为强大的通知类型,为了介绍它,我们重写Audiences切面:
1 | // 使用环绕通知重新实现Audience切面 |
可以看到,这个通知所达到的效果与之前的前置通知和后置通知是一样的,到那时现在它们处于同一个方法中。关于这个新的通知方法,你首先注意到的可能是它接受ProceedingJoinPoint作为参数,这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。需要注意的是,别忘记调用proceed()方法,如果不调用这个方法,那么你的通知实际上会阻塞对被通知方法的调用。
处理通知中的参数
之前的例子中都是没有参数的,但是如果切面所通知的方法有参数怎么办呢?有这样一个例子,play()方法会玄幻播放所有磁道并调用playTreck()方法,假设你想记录每个磁道被播放的次数,我们创建TrackCounter类:
1 | // 使用参数化的通知来记录磁道播放的次数 |
与之前不同的是,切点表达式中的args(trackNumber)限定符,它表明传递给playTrack方法的int类型参数也会传递到通知中去,参数的名称trackNumber也与切点方法签名中的参数相匹配。
现在我们可以在Spring配置中将BlankDisc和TrackCounter定义为bean,并启用AspectJ自动代理:
1 | / 配置TrackCount记录每个磁道播放的次数 |
通过注解引入新功能
静态语言的Java并不能像动态语言那样有开放类的理念,但是我们依旧可以使用切面为Spring bean添加新方法。在Spring中,切面只是实现了它们所包装bean相同接口的代理,如果除了实现这些接口,代理也能暴露新接口的话,会怎样呢?
我们想办法使用AOP为示例中的所有Performance实现引入下面的Encoreable接口:
1 | package concert; |
为此我们需要创建一个新的切面:
1 | package concert; |
可以看到EncoreableIntroducer是一个切面,但是他与我们之前的切面有所不同,使用了@DeclareParents注解,该注解由三部分组成:
- value:该属性制定了哪种类型的bean要引入该接口,在本例中即Performance类型。标记后面的加号表示是Performance的所有子类型,而不是Performance本身。
- defaultImpl:该属性指定了为引入功能提供实现的类。
- @DeclarationParents:该注解所标注的静态属性指明了要引入了接口。
和其他切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean:
1 | <bean class = "concert.EncoreableIntroducer" /> |
在XML中声明切面
除了使用注解的方式,还可以考虑使用XML文件配置的方式。在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面,如下表:
在使用XML文件配置方式之前,先把之前在Audience类中的注解全部移除掉。
声明前置和后置通知
接着我们在XML文件中配置将没有注解的Audience类转换为切面:
1 | <aop:config> |
声明环绕通知
将之前的watchPerformance()方法及其类中的注解都移除:
1 | package concert; |
然后在XML文件中配置:
1 | <aop:config> |
为通知传递参数
将TrackCounter类中的注解都移除掉,然后在XML文件中配置如下:
1 | <aop:config> |
通过切面引入新的功能
依旧将原Encoreable的注解移除掉,然后配置为:
1 | <aop:aspect> |
我们还可以使用delegate-ref属性来标识:
1 | <aop:aspect> |
delegate-ref属性引入了一个Spring的bean作为引入的委托:
1 | <bean id="defaultEncoreable" class="com.springinaction.perf.DefaultEncoreable" /> |