Spring面向切面中处理通知中的参数
到目前为止,我们的切面都很简单,没有任何参数。唯一的例外是我们为环绕通知所编写的 watchPerformance() 示例方法中使用了 ProceedingJoinPoint 作为参数。除了环绕通知,我们编写的其他通知不需要关注传递给被通知方法的任意参数。这很正常,因为我们所通知的 perform() 方法本身没有任何参数。
但是,如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗?
为了阐述这个问题,让我们重新看一下 BlankDisc 样例。play() 方法会循环所有磁道上的歌曲。如果我需要单独播放某个磁道上的歌曲呢?为此,我们需要修改一下 CompactDisc 接口,添加一个 playTrack() 方法,我们可以通过 playTrack() 方法直接播放某一个磁道中的歌曲。
/**
* 一个CD唱片接口
*/
public interface CompactDisc {
/**
* 播放
*/
void play();
/**
* 播放指定音轨
* @param trackNum 音轨
*/
void playTrack(int trackNum);
}
在 BlankDisc 中重写 playTrack() 方法
import java.util.List;
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks; // 磁道,CD 都会包含十多个磁道,每个磁道上包含一首歌
public BlankDisc(String title, String artist, List<String> tracks) {
this.title = title;
this.artist = artist;
this.tracks = tracks;
}
@Override
public void playTrack(int trackNum) {
System.out.println("Playing " + title + "【"+tracks.get(trackNum-1)+"】 by " + artist);
}
@Override
public void play() {
System.out.println("Playing " + title + " by " + artist);
for (String track : tracks) {
System.out.println("-Track: " + track);
}
}
}
假设你现在想记录每个磁道被播放的次数。一种方法就是修改 playTrack() 方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,因此不应该属于 playTrack() 方法。看起来,这应该是切面要完成的任务。
为了记录每个磁道所播放的次数,我们创建了 TrackCounter 类,它是通知 playTrack() 方法的一个切面。TrackCounter 类使用参数化的通知来记录磁道播放的次数:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import java.util.HashMap;
import java.util.Map;
@Aspect
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<>();
@Pointcut("execution(* com.syuez.CompactDisc.playTrack(int)) && args(trackNumber)") // 通知 playTrack() 方法
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(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
像之前所创建的切面一样,这个切面使用 @Pointcut 注解定义命名的切点,并使用 @Before 将一个方法声明为前置通知。但是,这里的不同点在于切点还声明了要提供给通知方法的参数。图 4.6 将切点表达式进行了分解,以展现参数是在什么地方指定的。

在图 4.6 中需要关注的是切点表达式中的 args(trackNumber) 限定符。它表明传递给 playTrack() 方法的 int 类型参数也会传递到通知中去。参数的名称 trackNumber 也与切点方法签名中的参数相匹配。
这个参数会传递到通知方法中,这个通知方法是通过 @Before 注解和命名切点 trackPlayed(trackNumber) 定义的。切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。
现在,我们可以在 Spring 配置中将 BlankDisc 和 TrackCounter 定义为 bean,并启用 AspectJ 自动代理:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.ImportResource;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration
@EnableAspectJAutoProxy
@ImportResource({"classpath*:blank-disc.xml"})
public class TrackCounterConfig {
@Bean
public TrackCounter trackCounter() {
return new TrackCounter();
}
}
最后,为了证明它能正常工作,你可以编写如下的简单测试。它会播放几个磁道并通过 TrackCounter 断言播放的数量。
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TrackCounterConfig.class)
public class TrackCounterTest {
@Autowired
private CompactDisc cd;
@Autowired
private TrackCounter counter;
@Test
public void testTrackCounter() {
cd.playTrack(1);
cd.playTrack(2);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(5);
cd.playTrack(5);
assertEquals(1, counter.getPlayCount(1));
assertEquals(1, counter.getPlayCount(2));
assertEquals(4, counter.getPlayCount(3));
assertEquals(0, counter.getPlayCount(4));
assertEquals(2, counter.getPlayCount(5));
}
}
到目前为止,在我们所使用的切面中,所包装的都是被通知对象的已有方法。但是,方法包装仅仅是切面所能实现的功能之一。之后将学习如何通过编写切面,为被通知的对象引入全新的功能。