上文我们一起学习了Spring IoC控制反转,下面我们继续学习一下AoP(Aspect Orient Programming), 面向切面编程。AoP同样是一种设计思想,AoP要达到的效果是,在不修改源代码的前提下,为系统中的业务组件添加某种功能增强。
AoP实现分为:静态AoP和动态AoP。
- 静态AoP在编译阶段对程序源代码进行修改, 生成了静态的静态AoP代理类, 来实现增强功能的织入;
- 动态AOP在运行阶段对动态生成代理对象(使用JDK动态代理或CGlib动态地生成AoP代理类), 来实现增强功能的织入;
Spring AoP使用的是动态代理,AoP框架并非Spring独有,市场上AoP框架有很多种。因为Spring AoP使用了aspectJ AoP框架的注解和表达式,而Spring-context包含了Spring-aop,所以我们只需要在pom.xml中额外引入aspectJ的依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
Eclipse项目对应的spring容器的配置文件也需要修改, 开启AspectJAutoProxy:
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop = "http://www.springframework.org/schema/aop" <!-- 引入aop的xmml名称空间-->
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean name="bankingServiceImpl1" class="org.littlestar.learning.package6.BankingServiceImpl1"></bean>
<bean name="bankingServiceImpl2" class="org.littlestar.learning.package6.BankingServiceImpl2"></bean>
<bean name="onlineBank" class="org.littlestar.learning.package6.OnlineStore">
<property name="bankingService" ref="bankingServiceImpl2"></property>
</bean>
<!--
<context:annotation-config />
<context:component-scan base-package="org.littlestar.learning.package6" />
-->
<bean name="withdrawAspect" class="org.littlestar.learning.package6.WithdrawAspect"></bean>
<!--打开aspectj-autoproxy, 也可以通过@EnableAspectJAutoProxy注解来开启 -->
<aop:aspectj-autoproxy />
<!--aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="false" /-->
</beans>
还是网上商城的案例,假设由于政策或者法规的变化,需要在取款服务(withdraw)之前做一些检查和控制,如反洗钱/反诈骗,我们如何在不修改代码的情况下,实现这个需求呢?哪天boss又提出需求, 想看看withdraw方法的执行耗时,你该如何以最小的代价,快速响应频繁的需求变化? 使用Spring AoP是一个完美答案。
我们需要BankingService.withdraw()方前添加检查控制功能,那么withdraw在AoP中就称为Pointcut(切入点)表示需要切入的位置。我们需要编写一个新的类WithdrawAspect,在该类中实现取款服务功能增强(前置检查控制功能),WithdrawAspect在Aop中称为Aspect(切面)表示一个横切进业务(withdraw())的一个对象。
有了切面(标注的类-WithdrawAspect), 还要在切面类内编写(相应功能增强)方法, 并在AoP框架中定义这些方法执行的时机,如在BankingService.withdraw()方法之前执行,这些有特殊配置的切面方法称为通知(Advice), 在Spring中有5种Advice注解: @Before, @After, @AfterReturning, @AfterThrowing和@Around 。
除了使用Advice注解也是可以通过XML配置文件配置,但没有使用注解直观简单。
WithdrawAspect.java
package org.littlestar.learning.package6;
import java.util.Date;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect // 注解为一个切面(Aspect)
//@Component // 必须是一个Bean
public class WithdrawAspect {
// @Pointcut: 切入点声明, 即切入到哪个(些)目标方法;
// 这样Advice注解(@After/@Before)只需要关联此切入点声明即可,无需再重复写切入点表达式;
// 我们可以表达式来指定要切入那些方法, 表达式可以使用通配符, 非常灵活;
@Pointcut("execution(public boolean org.littlestar.learning.package6.BankingService.withdraw(..))")
//@Pointcut("execution(* org.littlestar.learning.package6.BankingService.withdraw(..))") //与上等价
public void withdrawPointcut() {
// @Pointcut注解的方法体要留空;
// 函数名withdrawPointcut()是切入点的名称;
}
// 通知(Advice): 执行其注解方法在切入点执行的时机或者说位置, Spring AoP有5种Advice:
// @Before: Advice(通知)的一种, 切入点的方法体执行之前执行;
// @After : Advice(通知)的一种, 切入点的方法体正常运行结束后执行;
// @AfterReturning: Advice(通知)的一种, 在切入点正常运行结束后执行, 异常则不执行;
// @AfterThrowing: Advice(通知)的一种, 在切入点运行异常时执行;
// @Around: Advice(通知)的一种, 切入点的方法在@Around标注的方法内部执行;
@Before(value = "withdrawPointcut()")
//@Before("execution(public boolean org.littlestar.learning.package6.BankingService.withdraw(..))") //与上等价
public void withdrawBeforeAdvice(JoinPoint joinPoint) { //
Object[] args = joinPoint.getArgs();
long id = Long.parseLong(args[0].toString());
double amount = Double.parseDouble(args[1].toString());
//System.out.println("Before BankingService.withdraw: checking );
System.out.println("@Before withdrawPointcut: account-id = " + id +", amount = " + amount);
}
@Around(value = "withdrawPointcut()")
public Object withdrawAroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long beforeTimestamp = new Date().getTime();
System.out.println("@Around withdrawPointcut -> Before procced" );
// if(...) return false;
Object result = joinPoint.proceed(joinPoint.getArgs());
System.out.println("@Around withdrawPointcut -> After procced " );
long afterTimestamp = new Date().getTime();
System.out.println("@Around withdrawPointcut: return: " + result +"; elapse: " + (afterTimestamp-beforeTimestamp)+" ms");
return result;
}
}
启动方法:
package org.littlestar.learning.package6;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App6 {
public static void main(String[] args) {
try (ConfigurableApplicationContext context = new ClassPathXmlApplicationContext(
"org/littlestar/learning/package6/package6-bean-config.xml")) {
OnlineStore onlineBank = context.getBean(OnlineStore.class);
//System.out.println(onlineBank.getProviderName());
onlineBank.withdraw(10, 1000.00);
}
}
}
执行结果:
@Around withdrawPointcut -> Before procced
@Before withdrawPointcut: account-id = 10, amount = 1000.0
Calling withdraw(10, 1000.0)
@Around withdrawPointcut -> After procced
@Around withdrawPointcut: return: true; elapse: 3 ms
我们要做的是将WithdrawAspect测试打包,发布到生产环境,修改Spring容器配置文件即可实现上述两个需求,已有的代码不需要修改和重新编译。