到目前为止,我们的切面都很简单,没有任何参数。唯一的例外是我们为环绕通知所编写的 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 )) ;
}
}
到目前为止,在我们所使用的切面中,所包装的都是被通知对象的已有方法。但是,方法包装仅仅是切面所能实现的功能之一。让我们看一下如何通过编写切面,为被通知的对象引入全新的功能。