分布于应用中多处的功能称为横切关注点,通过这些横切关注点在概念上是与应用的业务逻辑相分离的,但其代码往往直接嵌入在应用的业务逻辑之中。将这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的。
什么是面向切面编程
面向切面编程中,通过声明的方式定义通用功能(安全、事务等)以何种方式在何处应用,而无需修改受影响的类(CourseService、StudentService等)。
AOP术语
通知(Advice):何种功能、何时
切面的工作被称为通知,同时通知还要解决何时执行这个工作的问题。Spring切面可以应用5种类型的通知:
- Before:在方法被调用之前调用通知;
- After:在方法调用之后调用通知;
- After-returning:在方法成功执行后;
- After-throwing:在方法抛出异常后;
- Around:在方法调用之前和之后都会调用通知;
连接点(Joinpoint):能够应用通知的点
连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程中。
切点(Pointcut):何处,应用通知的连接点的集合
切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称来指定这些切点,或是利用正则表达式定义匹配来指定这些切点。
切面(Aspect)
切面是通知和切点的结合,即何时在何处完成何种功能。
引入(Introduction)
引入允许我们向现有的类添加新方法或属性,从而可以在无需修改现有类的情况下,让它们具有新的行为和状态。
织入(Weaving)
将切面应用到目标对象来创建新的***对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入:
- 编译期:需要特殊的编译器,AspectJ的织入编译器就是这种方式;
- 类加载期:在目标类加载到JVM时被织入,需要特殊的类加载器。
- 运行期:在应用运行的某个时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个***对象。Spring AOP就是这种方式。
Spring对AOP的支持
- 基于***的经典AOP;
- @AspectJ注解驱动的切面;
- 纯POJO切面;
- 注入式AspectJ切面(适合Spring个版本);
Spring是在运行期将切面织入到所管理的Bean中的,如图所示,***类封装了目标类,当拦截到方法调用时,在调用目标Bean的方法之前,***会执行切面逻辑。真正应用需要被***的Bean时,Spring才会创建***对象。Spring的切面由包裹了目标对象的***类实现,***类处理方法的调用,执行额外的切面逻辑,并调用目标方法。
Spring只支持方法连接点,缺少对字段连接点的支持,例如拦截对象字段的修改。也不支持构造器连接点,也就无法在Bean创建时应用通知。
使用切点选择连接点
Spring AOP中,需要使用AspectJ的切点表达式来定义切点。
AspectJ指示器 | 描述 |
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP***的Bean引用为指定类型的类 |
target() | 限制连接点匹配目标对象为执行类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型 |
@annotation() | 限制匹配带有指定注解连接点 |
编写切点
这里使用了execution()指示器来选择Instrument的play()方法。表达式以*
开头表示不关心返回值的类型,然后指定了全限定类名和方法名,使用..
作为方法的参数列表,表示可以是任意的入参。
使用&&
将execution()和within()进行连接,那么也就可以使用||
(或)和!
(非)。
使用Spring的bean()指示器
bean()使用Bean id来作为参数,从而限制切点只匹配特定的Bean,如:
execution(* com.springinaction.springidol.Instrument.play()) and bean(eddie)
这里,表示在执行Instrument的play()方法时应用通知,但限定Bean的id为eddie。
在XML中声明切面
AOP配置元素 | 描述 |
<aop:advisor> | 定义AOP通知器 |
<aop:after> | 定义AOP后置通知(不管该方法是否执行成功) |
<aop:after-returning> | 在方法成功执行后调用通知 |
<aop:after-throwing> | 在方法抛出异常后调用通知 |
<aop:around> | 定义AOP环绕通知 |
<aop:aspect> | 定义切面 |
<aop:aspect-autoproxy> | 定义@AspectJ 注解驱动的切面 |
<aop:before> | 定义AOP前置通知 |
<aop:config> | 顶层的AOP配置元素,大多数的<aop:*> 包含在<aop:config> 元素内 |
<aop:declare-parent> | 为被通知的对象引入额外的接口,并透明的实现 |
<aop:pointcut> | 定义切点 |
下面定义一个观众类:
|
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="eddie" class="com.springinaction.springidol.Instrumentalist">
<property name="instrument">
<bean class="com.springinaction.springidol.Guitar" />
</property>
<property name="song" value="my love" />
</bean>
<bean id="audience" class="com.springinaction.springidol.Audience" />
<aop:config>
<aop:aspect ref="audience"><!-- 引用audience Bean -->
<!-- 声明切入点 -->
<aop:pointcut id="performance"
expression="execution(* com.springinaction.springidol.Performer.perform(..))" />
<!-- 表演之前 -->
<aop:before pointcut-ref="performance" method="takeSeats" />
<aop:before pointcut-ref="performance" method="turnOffCellPhones" />
<!-- 表演之后 -->
<aop:after-returning pointcut-ref="performance"
method="applaud" />
<!-- 表演失败之后 -->
<aop:after-throwing pointcut-ref="performance"
method="demandRefund" />
</aop:aspect>
</aop:config>
</beans>
|
在<aop:config>
中,可以声明一个或多个通知器、切面或者切点。pointcut
属性定义了通知所引用的切点。最终的通知逻辑如何织入到业务逻辑中:
测试代码:
|
@Test
public void testBeforeAndAfter() throws PerformanceException{
ApplicationContext context = new ClassPathXmlApplicationContext("spring-idol.xml");
Performer performer = (Performer) context.getBean("eddie");
performer.perform();
}
|
测试结果:
The audience is taking their seats.
The audience is turning off their cellphones
Playing my love : Guitar Guitar Guitar
CLAP CLAP CLAP CLAP CLAP
声明环绕通知
前置通知和后置通知之间共享消息需要使用成员变量,而Audience是单例,使用成员变量有可能存在线程安全问题。使用环绕通知可以完成之前前置和后置所实现的相同功能,而且只需一个方法。
|
package com.springinaction.springidol;
import org.aspectj.lang.ProceedingJoinPoint;
public class AroundAudience {
public void watchPerformance(ProceedingJoinPoint joinpoint) {
try {
// 表演之前
System.out.println("The audience is taking their seats.");
System.out.println("The audience is turning off their cellphones");
long start = System.currentTimeMillis();
// 执行被通知的方法
joinpoint.proceed();
// 表演之后
long end = System.currentTimeMillis();
System.out.println("CLAP CLAP CLAP CLAP CLAP");
System.out.println("The performance took " + (end - start) + " milliseconds.");
} catch (Throwable t) {
// 表演失败之后
System.out.println("Boo! We want our money back!");
}
}
}
|
ProceedingJoinPoint
作为入参,从而可以在通知里调用被通知的方法。
XML配置:
|
<bean id="audience" class="com.springinaction.springidol.AroundAudience" />
<aop:config>
<aop:aspect ref="audience"><!-- 引用audience Bean -->
<!-- 声明切入点 -->
<aop:pointcut id="performance"
expression="execution(* com.springinaction.springidol.Performer.perform(..))" />
<aop:around method="watchPerformance" pointcut-ref="performance" />
</aop:aspect>
</aop:config>
|
为通知传递参数
读心者:
|
package com.springinaction.springidol;
public interface MindReader {
void interceptThoughts(String thoughts);
String getThoughts();
}
|
Magician是MindReader 接口的一个简单实现:
|
package com.springinaction.springidol;
public class Magician implements MindReader {
private String thoughts;
@Override
public void interceptThoughts(String thoughts) {
System.out.println("Intercepting volunteer's thoughts");
this.thoughts = thoughts;
}
@Override
public String getThoughts() {
return thoughts;
}
}
|
下面是一个志愿者,供读心者去截取他的内心感应:
|
package com.springinaction.springidol;
public interface Thinker {
void thinkOfSomething(String thoughts);
}
|
|
package com.springinaction.springidol;
public class Volunteer implements Thinker {
private String thoughts;
@Override
public void thinkOfSomething(String thoughts) {
System.out.println("Thinker: " + thoughts);
this.thoughts = thoughts;
}
public String getThoughts() {
return thoughts;
}
}
|
通过配置实现将被通知方法的参数传递给通知:
|
<bean id="volunteer" class="com.springinaction.springidol.Volunteer" />
<bean id="magician" class="com.springinaction.springidol.Magician" />
<aop:config>
<aop:aspect ref="magician"><!-- 引用magician Bean -->
<!-- 声明切入点 -->
<aop:pointcut id="thinking"
expression="execution(* com.springinaction.springidol.Thinker.thinkOfSomething(String)) and args(thoughts) " />
<aop:before method="interceptThoughts" pointcut-ref="thinking"
arg-names="thoughts" />
</aop:aspect>
</aop:config>
|
切入点指定了Thinker的thinkOfSomething()方法,指定了String参数,然后在args参数中标识了将thoughts作为参数。
同时,引用了thoughts参数,标识该参数必须传递给magician的interceptThoughts()方法。
注意:
引用的thoughts
参数和pointcut标识的thoughts
参数,二者名称必须一致!
测试:
|
@Test
public void testBeforeArgs() throws PerformanceException {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-magician.xml");
Thinker thinker = (Thinker) context.getBean("volunteer");
MindReader mindReader = (MindReader) context.getBean("magician");
thinker.thinkOfSomething("晚上吃啥呢?");
System.out.println("MindReader: " + mindReader.getThoughts());
}
|
测试结果:
Intercepting volunteer’s thoughts
Thinker: 晚上吃啥呢?
MindReader: 晚上吃啥呢?
通过切面引入新功能
切面可以为SpringBean添加新方法:
|
<aop:aspect>
<aop:declare-parents types-matching="com.springinaction.springidol.Performer+"
implement-interface="com.springinaction.springidol.Contestant"
default-impl="com.springinaction.springidol.GraciousContestant" />
</aop:aspect>
|
声明了此切面所通知的Bean在它的对象层次结构中拥有新的父类,即类型匹配Performer接口(由types-matching指定)的Bean会实现Contestant接口(由implement-interface指定),同时可以指定Contestant的实现(default-impl,也可以用delegate-ref指定一个Spring Bean来实现)。
注解切面:@Aspect、@Pointcut、
采用注解的方式将之前的Audience标注为一个切面:
|
package com.springinaction.springidol;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class AspectJAudience {
// 定义切点
@Pointcut("execution(* com.springinaction.springidol.Performer.perform(..))")
public void performance() {
}
// 表演之前
@Before("performance()")
public void takeSeats() {
System.out.println("The audience is taking their seats.");
}
// 表演之前
@Before("performance()")
public void turnOffCellPhones() {
System.out.println("The audience is turning off their cellphones");
}
// 表演之后
@AfterReturning("performance()")
public void applaud() {
System.out.println("CLAP CLAP CLAP CLAP CLAP");
}
// 表演失败之后
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("Boo! We want our money back!");
}
}
|
@Pointcut
注解用于定义一个在@AspectJ
切面内可重用的切点,其值是一个AspectJ切点表达式,这里标识该切点必须匹配Performer接口的perform()方法。performance()切点的名称作为参数赋值给了所有的通知注解,从而可以标识每一个通知方法应该应用在哪里。
**AfterReturning 和After 的区别:
- AfterReturning 增强处理处理只有在目标方法成功完成后才会被织入。
- After 增强处理不管目标方法如何结束(保存成功完成和遇到异常中止两种情况),它都会被织入。
使用配置注解,首先我们要将切面在spring上下文中声明成自动***bean,即
<aop:aspectj-autoproxy />
。
测试代码:
|
@Test
public void testAspectJBeforeAndAfter() throws PerformanceException {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-idol.xml");
Performer performer = (Performer) context.getBean("eddie");
performer.perform();
}
|
测试结果:
The audience is taking their seats.
The audience is turning off their cellphones
Playing my love : Guitar Guitar Guitar
CLAP CLAP CLAP CLAP CLAP
运行测试程序时可能会出错,形如:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘eddie’ defined in class path resource [spring-idol.xml]: Cannot create inner bean ‘com.springinaction.springidol.Guitar#365d15c6’ of type [com.springinaction.springidol.Guitar] while setting bean property ‘instrument’; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘com.springinaction.springidol.Guitar#365d15c6’ defined in class path resource [spring-idol.xml]: Initialization of bean failed; nested exception is java.lang.IllegalArgumentException: error at ::0 can’t find referenced pointcut performance
上网搜了一下,发现是JDK不匹配。
我原来用的JDK1.7匹配的是aspectjrt.1.6.2和aspectjweaver.1.6.2,因此会报错。
如果要使用AspectJ完成注解切面需要注意下面的JDK与AspectJ的匹配:
JDK1.6 —— aspectJ1.6
JDK1.7 —— aspectJ1.7.3+
注解环绕通知:@Around
|
package com.springinaction.springidol;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class AspectJAroundAudience {
// 定义切点
@Pointcut("execution(* com.springinaction.springidol.Performer.perform(..))")
public void performance() {
}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinpoint) {
try {
// 表演之前
System.out.println("The audience is taking their seats.");
System.out.println("The audience is turning off their cellphones");
long start = System.currentTimeMillis();
// 执行被通知的方法
joinpoint.proceed();
// 表演之后
long end = System.currentTimeMillis();
System.out.println("CLAP CLAP CLAP CLAP CLAP");
System.out.println("The performance took " + (end - start) + " milliseconds.");
} catch (Throwable t) {
// 表演失败之后
System.out.println("Boo! We want our money back!");
}
}
}
|
不要忘了配置:@Aspect和
测试代码:
|
@Test
public void testAspectJAround() throws PerformanceException {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-idol-around.xml");
Performer performer = (Performer) context.getBean("eddie");
performer.perform();
}
|
传递参数给所标注的通知
|
package com.springinaction.springidol;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class AspectJMagician implements MindReader {
private String thoughts;
@Pointcut("execution(* com.springinaction.springidol.Thinker.thinkOfSomething(String)) && args(thoughts))")
public void thinking(String thoughts){
}
@Override
@Before("thinking(thoughts)")
public void interceptThoughts(String thoughts) {
System.out.println("Intercepting volunteer's thoughts");
this.thoughts = thoughts;
}
@Override
public String getThoughts() {
return thoughts;
}
}
|
变为@Pointcut,变为@Before,注解里不需要arg-names属性所对应的注解。
引文地址:《Spring实战》学习笔记-第四章:面向切面的Spring