面向切面的Spring
散布于应用多处的功能被称为横切关注点,这些横切关注点从概念上讲是与应用的业务逻辑相分离的,把这些横切关注点与业务逻辑相分离正式面向切面编程所要解决的问题
切面实现了横切关注点的模块化(横切关注点可以被简单描述为影响应用多处的功能),在重用通用功能上,切面提供了取代继承和委托的另一种可选方案。
AOP术语
通知 : 切面的工作被称为通知
- 前置通知(Before): 在目标方法被调用之前调用通知功能
- 后置通知(After): 在目标方法完成之后调用通知,不会关心方法的输出是什么
- 返回通知(After-returning) : 在目标方法成功执行之后调用通知
- 异常通知(After-throwing) :在目标方法抛出异常之后调用
- 环绕通知(Around) : 通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、修改字段时等等,切面代码利用这些点插入到应用的正常流程之中。
切点(Poincut)
如果说通知定义了切面”什么”和”何时”的话,那么切点就定义了”何处”。切点的定义会匹配通知所要织入的一个或多个连接点
切面(Aspect)
切面时=是通知和切点的结合。通知和切点共同定义了切面的全部内容,他是什么,在何时和何处完成其功能。
引入(Introduction)
引入允许我们向现有的类添加新方法和属性
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程
Spring对AOP的支持
Spring提供了四种类型的AOP支持
- 基于代理的经典SpringAOP
- 纯POJO切面
- @AspectJ 注解驱动的切面
- 注入式AspectJ切面
前三种都是SPringAOP实现的变体,SpringAOP构建在动态代理的基础之上,因此Spring对AOP的支持局限于方法拦截
借助Spring的aop命名空间,可将纯POJO转换为切面,但需要xml配置,而注解驱动的AOP借鉴了AspectJ的切面能够不用xml来完成功能
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中,直到应用需要被代理的bean时,Spring才会创建代理对象
Spring只支持方法级别的连接点
通过切点来选择连接点
Spring 所支持AspectJ切点指示器
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this | 限定连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象应用的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用SpringAOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标示bean
编写切点
package concert;
public interface Performance {
public void perform();
}
编写Performance的perform方法执行时触发通知的调用
execution(* concert.Performance.perform(...))
execution 表示在方法执行时触发
* 表示返回任意类型
后面是方法所属的类和方法名
可以用&&连接芝士切 类似还有|| 和 !
在xml中用and or not代替
execution(* concert.Performance.perform(...)) && within(concert.*)
Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标示bean,使用beanID 或bean名称作为参数来限制切点只匹配特定的bean
execution(* concert.Performance.perform(...)) and bean('woodstock')
execution(* concert.Performance.perform(...)) and !bean('woodstock')
使用注解创建切面
注解 | 通知 |
---|---|
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
package concert;
@Aspect
public class Audience{
@Before("execution(* concert.Performance.perform(...))")
public void silenceCellPhones(){
System.out.println("Silence cell phones");
}
@Before("execution(* concert.Performance.perform(...))")
public void takeSeats(){
System.out.println("Taking Seats");
}
@AfterReturning("execution(* concert.Performance.perform(...))")
public void applause(){
System.out.println("Clap,Clap");
}
@AfterThrowing("execution(* concert.Performance.perform(...))")
public void silenceCellPhones(){
System.out.println("Demanding a refund");
}
}
以上有重复的execution 切点
可以用@Pointcut注解在切面内定义一个可重用切点
package concert;
@Aspect
public class Audience{
@Pointcut("execution(* concert.Performance.perform(...))")
public void performance(){}
@Before("performance()")
public void silenceCellPhones(){
System.out.println("Silence cell phones");
}
@Before("perfromance()")
public void takeSeats(){
System.out.println("Taking Seats");
}
@AfterReturning("performance")
public void applause(){
System.out.println("Clap,Clap");
}
@AfterThrowing("performance")
public void silenceCellPhones(){
System.out.println("Demanding a refund");
}
}
Audience可以被装配为Spring中的bean
@Configuration
@EnableAspectJAutoProxy //启用AspectJ自动代理
@ComponentScan
public class ConcertConfig{
@Bean
public Audience audience(){
return new Audience();
}
}
或者在xml中
<beans ...>
<aop:aspectj-autoproxy />
<bean class="concert.Audience" />
</beans>
创建环绕通知
package concert;
@Aspect
public class Audience{
@Pointcut("execution(* concert.Performance.perform(...))")
public void performance(){}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp){
try{
System.out.println("Silencing");
System.out.println("Taking Seats");
jp.proceed();
System.out.println("CLAP,CLAP");
}
catch(Throwable e){
System.out.println("Demanding a refound");
}
}
}
ProceedingJoinPoint 作为参数,这个是必须要有的,因为要在通知中通过它来调用被通知的方法,
当要将控制权交给被通知方法时,需要调用ProceedingJoinPoint 的 proceed() 方法
处理通知中的参数
如 CD磁道的播放次数与播放本身是不同的关注点,因此要用切面来记录播放次数而不是在播放方法中记录
package soundsystem;
public interface CompactDisc{
void play();
void playTrack(int number);
}
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public void setTitle(String title) {
this.title = title;
}
public void setArtist(String artist) {
this.artist = artist;
}
public void setTracks(List<String> tracks) {
this.tracks = tracks;
}
public void playTrack(int number){
System.out.println(tracks.get(i));
}
@Override
public void play() {
// TODO Auto-generated method stub
for(int i = 0;i < tracks.size();i++){
playTrack(i);
}
}
}
package soundsystem
@Aspect
public class TrackCounter{
private Map<Integer,Integer> trackCounts = new HashMap<Integer,Integer>();
@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);
}
public int getPlayCount(int trackNumber){
return trackCounts.containsKey(trackNuber) ? trackCounts.get(trackNumber) : 0;
}
}
args(trackNumber) 表明传递给playTrack()方法的int类型参数也会传递到通知中去,参数的名称trackNumber也与切点方法签名中的参数想匹配
这个参数会传递到通知方法中,这个通知方法是通过@Before注解和命名切点trackPlayed(trackNumber)定义的。
切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移
package soundsystem;
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig{
@Bean
public CompactDisc sgtPeppers(){
BlankDisc cd = new BlankDisc();
cd.setTitle("...");
cd.setArtist("...");
List<String> tracks = new ArrayList<String>();
tracks.add(...);
tracks.add(...);
tracks.add(...);
cd.setTracks(tracks);
return cd;
}
@Bean
public TrackCounter trackCounter(){
return new TrackCounter();
}
}
通过注解引入新功能
利用被称为引入的AOP概念,切面可以为SPring bean添加新方法
在Spring中,切面只是实现了他们所包装bean相同接口的代理,如果除了实现这些接口,代理也能暴露新接口 切面所通知的bean看起来就像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓
当引入接口的方法被调用时,代理会把此调用委托给实现了新街口的某个其他对象,实际上,,一个bean的实现被拆分到多个类中
package concert;
public interface Encoreable{
void performance();
}
@Aspect
public class EncoreableIntroducer{
@DeclareParents(value="concert.Performance+",defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
通过@DeclareParents注解讲Encoreable接口引入到Performance bean中
@DeclareParents注解由三部分组成
value属性指定了那种类型的bean要引入该接口。本例中是所有实现Performance的类型(加号表示是Performance的所有子类型,而不是Performance本身)
defaultImpl属性指定了为引入功能提供实现的类。本例指定DefaultEncoreable提供实现
@DeclareParents注解所标注的静态属性指明了要引入了接口,这里所引入的是Encoreable接口。
要将EncoreableIntroducer声明为一个bean
在xml中声明切面
AOP配置元素 | 用途 |
---|---|
< aop:advisor> | 定义AOP通知器 |
< aop:after> | 定义AOP后置通知(不管被通知方法是否执行成功) |
< aop:after-returning> | 定义AOP返回通知 |
< aop:after-throwing> | 定义AOP异常通知 |
< aop:around> | 定义AOP环绕通知 |
< aop:aspect> | 定义一个切面 |
< aop:aspectj-autoproxy> | 启用@AspectJ注解驱动的切面 |
< aop:before> | 定义一个AOP前置通知 |
< aop:config> | 顶层的AOP配置元素,大多数< aop: *>元素必须包含在< aop:config>元素内 |
< aop:declare-parents> | 以透明的方式为被通知对象引入额外的接口 |
< aop:pointcut> | 定义一个切点 |
<aop:config>
<aop:aspect ref="audience">//引入audiencebean
<aop:before pointcut="execution(...)"
method="silenceCellPhones" />
<aop:before pointcut="execution(...)"
method="takeSeats" />
...
</aop:aspect>
</aop:comfig>
消除重复的pointcut
<aop:config>
<aop:aspect ref="audience">//引入audiencebean
<aop:pointcut id="performance" execution="execution(...)" />
<aop:before pointcut="performance"
method="silenceCellPhones" />
<aop:before pointcut="performance"
method="takeSeats" />
...
</aop:aspect>
</aop:comfig>
声明环绕通知
<aop:config>
<aop:aspect ref="audience">//引入audiencebean
<aop:pointcut id="performance" execution="execution(...)" />
<aop:around pointcut-ref="performance"
method = "watchPerformance" />
...
</aop:aspect>
</aop:comfig>
为通知传递参数
<aop:config>
<aop:aspect ref="audience">//引入audiencebean
<aop:pointcut id="trackCounter" execution="execution(* soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)" />
<aop:before pointcut="trackCounter"
method="countTrack" />
...
</aop:aspect>
</aop:comfig>
引入新功能
<aop:aspect>
<aop:declare-parents
type-matching="concert.Performance+"
implement-interface="concert.Encoreable"
delegate-ref="encoreableDelegate" />
</aop:aspect>