到目前为止,我们的切面都很简单,没有任何参数。唯一的例外是我们为环绕通知所编写的 watchPerformance() 示例方法中使用了 ProceedingJoinPoint 作为参数。除了环绕通知,我们编写的其他通知不需要关注传递给被通知方法的任意参数。这很正常,因为我们所通知的 perform() 方法本身没有任何参数。
但是,如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗?
为了阐述这个问题,让我们重新看一下 2.4.4 小节中的 BlankDisc 样例。play() 方法会循环所有的磁道并调用 playTrack() 方法。但是,我们也可以通过 playTrack() 方法直接播放某一个磁道中的歌曲。
假设你想记录每个磁道被播放的次数。一种方法就是修改 playTrack() 方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,因此不应该属于 playTrack() 方法。看起来,这应该是切面要完成的任务。
为了记录每个磁道所播放的次数,我们创建了 TrackCounter 类,它是通知 playTrack() 方法的一个切面。下面的程序清单展示了这个切面,使用参数化的通知来记录磁道播放的次数:
程序清单 4.6 使用参数化的通知来记录磁道播放的次数
Copy package soundsystem;
import java.util.HashMap;
import java.util.Map;
import org.aspect.lang.annotation.Aspect;
import org.aspect.lang.annotation.Before;
import org.aspect.lang.annotation.Pointcut;
@Aspect
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<>();
@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(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
像之前所创建的切面一样,这个切面使用 @Pointcut 注解定义命名的切点,并使用 @Before 将一个方法声明为前置通知。但是,这里的不同点在于切点还声明了要提供给通知方法的参数。图 4.6 将切点表达式进行了分解,以展现参数是在什么地方指定的。
在图 4.6 中需要关注的是切点表达式中的 args(trackNumber) 限定符。它表明传递给 playTrack() 方法的 int 类型参数也会传递到通知中去。参数的名称 trackNumber 也与切点方法签名中的参数相匹配。
这个参数会传递到通知方法中,这个通知方法是通过 @Before 注解和命名切点 trackPlayed(trackNumber) 定义的。切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。
现在,我们可以在 Spring 配置中将 BlankDisc 和 TrackCounter 定义为 bean,并启用 AspectJ 自动代理,如程序清单 4.7 所示。
程序清单 4.7 配置 TrackCount 记录每个磁道播放的次数
Copy package soundsystem;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {
@Bean
public CompactDisc sgtPeppers() {
BlankDisc cd = new BlankDisc();
cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band");
cd.setArtist("The Beatles");
List<String> tracks = new ArrayList<>();
tracks.add("Sgt. Pepper's Lonely Hearts Club Band");
tracks.add("With a Little Help from My Friends");
tracks.add("Lucy in the Sky with Diamonds");
tracks.add("Getting Better");
tracks.add("Fixing a Hole");
// ...other tracks omitted for brevity...
cd.setTracks(tracks);
return cd
}
@Bean
public TrackCounter trackCounter() {
return new TrackCounter();
}
}
最后,为了证明它能正常工作,你可以编写如下的简单测试。它会播放几个磁道并通过 TrackCounter 断言播放的数量。
程序清单 4.8 测试 TrackCounter 切面
Copy package soundsystem;
import static org.junit.Assert.*;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=TrackCounterConfig.class)
public class TrackCounterTest {
@Rule
public final StandardOutputStreamLog log = new StandardOutputStreamLog();
@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(7);
cd.playTrack(7);
assertEquals(1, counter.getPlayCount(1));
assertEquals(1, counter.getPlayCount(2));
assertEquals(4, counter.getPlayCount(3));
assertEquals(0, counter.getPlayCount(4));
assertEquals(0, counter.getPlayCount(5));
assertEquals(0, counter.getPlayCount(6));
assertEquals(2, counter.getPlayCount(7));
}
}
到目前为止,在我们所使用的切面中,所包装的都是被通知对象的已有方法。但是,方法包装仅仅是切面所能实现的功能之一。让我们看一下如何通过编写切面,为被通知的对象引入全新的功能。