面向切面的Spring
一. 基础概念
定义:日志、安全和事务管理等的确都很重要,但它们不应该作为应用对象主动参与的行为,这些模块作为独立运行的存在。简单来说就是业务代码只需要去关注业务本身就可以了,日志记录、安全判断和事务管理这些就交给Spring的面向切面功能去完成。
切面的常用术语:通知、切点、连接点
Spring对AOP的支持:
- 基于代理的经典Spring AOP的支持
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面
Spring支持的Aspect指示器:
Aspect指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用语匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象应用的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型 |
@annotation() | 限定匹配带有指定注解的连接点 |
二. 通知
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring所创建的通知都是用标准的Java类编写的。这样的话我们就可以使用与普通Java开发一样的集成开发环境(IDE)来开发切面。
Spring切面可以应用五种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
三. 连接点
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
Spring只支持方法级别的连接点
四. 切点
切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
编写切点示例:
接口Performance定义
package concert;
public interface Performance{
public void perform();
}
execution(* concert.Performance.perform(..)) and within(concert.*) //切点仅匹配concert包,当执行Performance的perform()方法时
execution(* concert.Performance.perform(..)) and bean('woodstock') //当执行Performance的perform()方法应用通知,但限定bean的ID为woodstock
五. 使用注解创建切面
AspectJ提供了五个注解来定义通知:
- @After:通知方法会在目标方法返回或抛出异常后调用
- @Before:通知方法会在目标方法调用之前执行
- @Around:通知方法会将目标方法封装起来
- @AfterReturning:通知方法会在目标方法返回后调用
- @AfterThrowing:通知方法会在目标方法抛出异常后调用
切面代码示例:
package cn.wukun;
import org.aspectj.lang.annotation.*;
@Aspect
public class Audience {
@Pointcut("execution(* cn.wukun.Performance.perform(..))")
public void performance(){}
@Before("performance()")
public void silenceCellPhones(){
System.out.println("执行前1号");
}
@Before("performance()")
public void takeSeats(){
System.out.println("执行前2号");
}
@After("performance()")
public void applause(){
System.out.println("执行完了就执行老子");
}
@AfterReturning("performance()")
public void demandRefund(){
System.out.println("正常执行的");
}
@AfterThrowing("performance()")
public void errorRefund(){
System.out.println("程序执行时出现了错误");
}
}
使用@AspectJ注解进行了标注,该注解表明Audience不仅仅是一个POJO,还是一个切面
@Pointcut注解:能够在一个@AspectJ切面内定义可重用的切点。
类即使用了@AspectJ注解,也不会被视为切面,如果没有对应配置的话。JavaConfig配置方法,可以在配置类的级别上通过使用@EnableAspectJAutoProxy注解启用自动代理功能。
JavaConfig配置代码示例:
package cn.wukun;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @EnableAspectJAutoProxy注解:启用自动代理功能
*/
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AudienceConfig {
@Bean
public Audience audience(){
return new Audience();
}
@Bean
public TrackCounter trackCounter(){
return new TrackCounter();
}
}
环绕通知使用示例:
package cn.wukun;
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 AroundAudience {
@Pointcut("execution(* cn.wukun.Performance.perform(..))")
public void performance(){}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinPoint){
try{
System.out.println("执行前1号");
System.out.println("执行前2号");
//将控制权限交给被通知的方法
joinPoint.proceed();
System.out.println("正常执行完成");
}catch (Throwable e){
System.out.println("出错了");
}
}
}
ProceedingJoinPoint参数:这个对应是必需要有的在使用环绕通知的时候,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,需要调用ProceedingJoinPoint的proceed()方法。
处理通知中的参数示例:
package cn.wukun;
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;
@Aspect
public class TrackCounter {
//需要注意的是args(i)限定符,它表明传递给track()方法的int类型参数也会传递到通知中去。
@Pointcut(value = "execution(* cn.wukun.Performance.track(int)) && args(i)")
public void declareJoinPointExpression(int i) {}
@Before(value="declareJoinPointExpression(i)",argNames = "i")
public void beforeMethod(int i) {
System.out.println("---------begins with " + i);
}
@Around(value = "declareJoinPointExpression(i)", argNames = "point, i")
public Object Method(ProceedingJoinPoint point, int i) {
Object result = null;
try {
result = point.proceed();
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("---------Around with " + i +"-"+result);
return 33;
}
}
六. 注意
Spring IN ACTION书中还有“注入Aspect切面”和“通过注解引入新功能”这两个知识点,如有需要自行了解。
详细代码参考地址:https://github.com/wukunpdd/SpringInActionCode/tree/master/aspect