面向切面编程
本节课示例代码:https://github.com/jingjiecb/section3
Spring框架最重要的内容之一,面向切面编程
软件编程方法的发展:
- 面向过程编程 POP
- 面向对象编程 OOP
- 面向切面编程 AOP
- 函数式编程 FP
- 反应式编程 Rx
面向切面编程:对于已经写好的业务代码,如果想添加新的功能比如日志或者认证,直接通过集成或者委托会对代码构成侵入。面向切面编程可以不修改任何原本的业务代码,将新功能加入。
例如:对于音乐会对象,要求不修改任何业务代码,能够增加新的行为(添加新的方法)。
概念
横切关注点:需要切入到业务逻辑中的(通用)功能和模块,例如日志、安全、事务、缓存。
织入:将横切关注点的功能加入原有的目标对象的过程。
通知Advice:包括什么时候切入(切入时机)以及做什么(新功能)
切点Poincut:在何处切入(切入时机)。通过切点表达式指名。
切面Aspect:一个写着各种切点和通知的类,可包含多个通知。用@Aspect指明。
引入:引入新的行为和状态
Tips: 将切面应用到原有的逻辑中,只需要在JavaConfig配置类中配置切面类的Bean即可。
需要在配置类上面通过注解@EnableAspectJAutoProxy启动代理,才能切入。开启代理后,从Spring容器中拿到的是代理对象,代理对象可以根据切面,在调用实际对象的方法前后进行插入新行为。Spring实际上是在运行期进行织入。
定义切入
通知类型:
- @Before 在被切入的方法执行之前执行。有多个Before切入时,按照写的顺序进行切入。
- @AfterReturning 方法正常返回后调用
- @AfterThrowing 方法抛出异常后调用
- @After 上面两种情况导致方法结束时调用
- @Around 可以自由指定被切入方法的执行时机。参数中传入一个ProceedingJoinPoint对象,调用它的proceed()方法就可以起到调用原来被切入方法的作用。
Around的例子如:
@Aspect
public class Audience2 {
@Pointcut("execution(* concert.Performance.perform( .. )) ")
public void performance() {
}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinPoint) {
try {
System.out.println(".Silencing cell phones");
System.out.println(".Taking seats");
joinPoint.proceed();
System.out.println(".CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println(".Demanding a refund");
}
}
}
另外可以通过@PointCut注解复用切点,例如:
@Aspect
public class Audience1 {
// 声明一个切点
@Pointcut("execution(* concert.Performance.perform( .. ))")
public void performance() {
}
@Before("performance()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
@Before("performance()")
public void takeSeats() {
System.out.println("Taking seats");
}
@AfterReturning("performance()")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("Demand a refund");
}
}
切面表达式
被切入方法可能有传入的参数。在切面表达式中通过args关键字可以获取到方法调用传入的参数。例如:
@Pointcut(
"execution(* soundsystem.CompactDisc.playTrack( int )) " +
"&& args(trackNumber)")
public void trackPlayed(int trackNumber) { // 参数名和自己头上的注解保持一致
}
@Before("trackPlayed(trackNumber)")
public void countTrack(int trackNumber) { // 参数名和自己头上的注解保持一致
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount + 1);
}
可以通过within关键字指定切入类的范围,例如 within(soundSystem.*) 表示对soundSystem包下的指定类进行切入,而不管其他包。
也可以用过bean关键字指定特定名字的Bean进行织入。例如bean(sgtPeppers)表示只对名字为sgtPeppers的Bean进行织入。
引入
为一个类增加新的方法,而不改变原来的代码文件。
首先需要为新行为定义一个接口,并写出具体实现该接口的类,指明具体的业务逻辑。然后通过一个切面类,中间用注解指明切点和具体实现。例如:
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value = "concert.Performance+",//后面的+表示应用到所有实现了该接口的Bean
defaultImpl = DefaultEncoreable.class) //表示具体实现类
public static Encoreable encoreable; // 以成员变量的方式加入即可
}
注意:织入的包含新行为的对象是单例的。
XML配置织入
<aop:aspectj-autoproxy/>
<bean id="audience" class="concert2.Audience"/>
<bean id="concert" class="concert.Concert"/>
<aop:config>
<aop:aspect ref="audience">
<aop:before method="silenceCellPhones"
pointcut="execution(* concert.Performance.perform(..))"/>
<aop:before method="takeSeats"
pointcut="execution(* concert.Performance.perform(..))"/>
<aop:after method="applause"
pointcut="execution(* concert.Performance.perform(..))"/>
<aop:after-throwing method="demandRefund"
pointcut="execution(* concert.Performance.perform(..))"/>
</aop:aspect>
</aop:config>
<aop:aspectj-autoproxy/>表示启动代理。后面通过aop:config标签进行配置,指定切面、切点、切入的方法等信息。
当然也可以进行切点复用:
<aop:aspectj-autoproxy/>
<bean id="audience" class="concert2.Audience"/>
<bean id="concert" class="concert.Concert"/>
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance"
expression="execution(* concert.Performance.perform(..))"/>
<aop:before method="silenceCellPhones"
pointcut-ref="performance"/>
<aop:before method="takeSeats"
pointcut-ref="performance"/>
<aop:after method="applause"
pointcut-ref="performance"/>
<aop:after-throwing method="demandRefund"
pointcut-ref="performance"/>
</aop:aspect>
</aop:config>
先定义一个aop:pointcut即可。
也可以像下面一样声明around:
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance"
expression="execution(* concert.Performance.perform(..))"/>
<aop:around method="watchPerformance"
pointcut-ref="performance"/>
</aop:aspect>
</aop:config>
引入:
<!-- <bean id="encoreableDelegate" class="concert.DefaultEncoreable"/>-->
<aop:config>
<aop:aspect>
<aop:declare-parents types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"/>
<!-- delegate-ref="encoreableDelegate"/>-->
</aop:aspect>
</aop:config>
如果要使用单例,则可以通过先配置bean,然后在aop:declare-parents中写明 delegate-ref 指向单例bean即可。