03.AOP
面向切面
假设这样的情况, 你有三个类, 需要这三个类都有 "日志" 的功能. 如果用一般面向对象的思维来做的话 , 也许需要这三个类都实现一个 "日志" 的接口, 或者继承一个父类, 这三个类中的方法的时候调用父类或接口中的日志方法.
这样听起来就很麻烦, 更好的方法是, 利用代理, 让一个代理类继承目标类的接口(为了让这个代理类拥有目标类的基本功能), 然后在调用方法的时候同时调用日志方法.
代理设计模式是Spring AOP 的核心. 如果使用了AOP 代理Bean, 当调用一个 Bean 的方法时, Spring 会拦截这个调用, 给这个方法的某一步(执行前, 返回前, 执行后, 抛出错误后, 或者全部)加上需要的功能. 再进行调用
AOP 的一些名词
了解了面向切面, 就应该了解一下有关的术语.
- 通知: 通知就是被代理的方法在执行的时候添加的额外的内容
- 切点: 切点定义了通知在什么地方起作用
- 切面: 通知和切点结合就是切面
Spring对AOP的支持
- 基于代理的经典Spring AOP
- 纯 POJO 切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用于Spring各个版本)
第一种太经典了, 书里没讲
纯 POJO 切面用 XML 配置
@AspectJ注解挺好, 可以用注解完成, 不用XML
前三种都是基于代理的 AOP, 功能上仅限于方法拦截, 如果对 AOP 的需求有构造器或属性拦截. 或者要代理的目标类没有接口. 那么需要考虑 AspectJ
通过切点来选择连接点
Spring 借助 AspectJ 切点表达式来定义 Spring 切面.
比如说, 要给一个目标类的方法加上一个通知, 在 AOP 中不需要对目标类进行修改. 要编写一个类, 在这个类中定义切点, 再写一个方法作为通知, Spring 会把这个通知应用到切点所定位到的目标类方法中去
切点怎么写
假如有一个目标类, 他的接口是这样的
public interface Perfoormance{
public void perform();
}
需要给 perform() 方法加上通知. AspectJ 表达式如下
execution(* concert.Performance.perform(..))
第一个 * 表示 返回类型随意, concert.Performance.perform 表示方法名, 两个英文句号表示参数随意
编写切面
@Aspect
public class Audience{
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("Silencing cell phones");
}
@After("execution(** concert.Performance.perform(..))")
public void after(){
System.out.println("after");
}
@AfterReturning("execution(** concert.Performance.perform(..))")
public void afterReturning(){
System.out.println("after returning");
}
@AfterThrowing("execution(** concert.Performance.perform(..))")
public void afterThrowing(){
System.out.println("after Throwing");
}
}
从这里看出来, 通知类型有before, after, afterreturning afterthrowing
如果要重复使用切点可以这样:
@Aspect
public class Audience{
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance();
@Before("performance")
public void before(){
System.out.println("before");
}
}
完成以后, 还需要再配置文件中开启 AspectJ 自动代理
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class Configuration(){}
用 XML 开启 AspectJ 自动代理
<aop:aspectj-autoproxy />
创建环绕通知
前面提到的几种通知类型, 都只是再方法的某一个位置进行通知. 而环绕通知是可以对方法全方位多角度地代理
@Aspect
public class Audience{
// 这里声明一个 performance 的切点
@Around("performance()")
public void around(ProceedingJoinPoing jp){
try{
System.out.println("before");
jp.proceed();
}catch(Throwable e){
// ..
System.out.println("afterThrowing")
}finally{
System.out.println("After")
}
}
}
jp.proceed() 就是在运行原始方法, 在这行代码前面的部分相当于 before 通知, catch 中相当于 afterThrowing(); finally 中的相当于 after, 这里无法表现出 afterreturing, 如果如果报错, afterreturing是不会运行的
有参数的情况
如果目标类要被代理的方法有参数, 应该这么写
表达式
execution(* soundsystem.CompactDisc.playTract(int)) && args(trackNumber)
通知
@Before("pointcut(trackNumber)")
public void countTrack(int trackNumber){
//...
}
首先, 表达式要匹配到这个有参数的方法, 然后, 方法的 @Before 标签要有一个参数, 这个参数是参数名, 方法的参数也需要接收一个参数名相同的参数.