通知
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面包含5类通知:
连接点
我们的应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时,抛出异常时,甚至修改一个字段的时候。
切点
一个切面并不需要通知应用的所有连接点。切点有助于缩小切面所通知的连接点的范围。切点有助于缩小所通知的连接点的范围。
切面
切面是通知和切点的结合。如果说通知定义了切面的“什么”和“何时”的话,切点定义了“何处”。切面就是是什么,何时何地完成其功能。
引入
引入允许我们向现有的类添加新方法或属性。
织入
织入就是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
因为学习Spring,所以我们关注SpringAOP。话虽如此,Spring和AspectJ项目之间有大量的合作,Spring对AOP的支持在很多方面借鉴了AspectJ项目。
Spring提供了4种类型的AOP支持:
前三种都是Spring AOP实现的变体,它构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
经典的Spring AOP过于复杂和笨重,会让人感到厌烦。
借助Spring的aop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件所调用的方法。遗憾的是,这种技术需要XML配置,但这的确是声明式地将对象转换为切面的简便方式。
Spring通知是Java编写的
Spring在运行时通知对象
通过在代理类的包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如图。
代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。
Spring只支持方法级别的连接点
方法拦截可以满足绝大部分的需求,如果需要方法拦截之外的连接点拦截功能,那么我们可以利用Aspect来补充Spring AOP的功能。
在Spring AOP中,要使用AsprctJ的切点表达式语言来定义切点。关于AspectJ切点,Spring仅支持AspectJ切点指示器的一个子集。
Spring在尝试使用AspectJ的其他指示器的时候,会抛出异常。
编写切点
为了阐述Spring中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个Performance接口:
package com.lee.concert;
public interface Performance {
public void perform();
}
假设我们想编写Performance的perform()发方法触发的通知。下图展示了一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用。
我们使用execution()指示器选择Perfomance的perform()方法。
方法表达式以*
开始,表明了我们不关心方法返回值的类型。
我们制定了全限定类名和方法名。对于方法参数列表,我们使用两个点号..
表明切点要选择任意的perform()方法,无论方法的入参是什么。
假设我们需要设置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限制匹配:
execution(* concert.Performance.perform(..) && within(concert.*))
在切点中选择bean
除了以上的指示器外,还有一个新的bean()指示器,bean()使用bean ID来限制切点只匹配特定的bean。
execution(* concert.Performance.perform(..) && !bean('workstock'))
使用注解来创建切面是AspectJ 5所引入的关键特性。
注:spring+mvn搭建方法:
https://blog.csdn.net/jasmine_lh/article/details/69159357
定义切面
package com.lee.concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class Audience {
@Before("execution(* com.lee.concert.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("请电话静音");
}
@Before("execution(* com.lee.concert.Performance.perform(..))")
public void takeSeats(){
System.out.println("请坐");
}
@AfterReturning("execution(* com.lee.concert.Performance.perform(..))")
public void applause(){
System.out.println("热烈鼓掌");
}
@AfterThrowing("execution(* com.lee.concert.Performance.perform(..))")
public void demandRefund(){
System.out.println("演砸了");
}
}
Audience类使用了@AspectJ
注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。
你可能注意到了,相同的切点表达式execution...
我们重复了四遍,这可能有点问题。所以我们使用@Pontcut
注解定义一个可重用的切点:
package com.lee.concert;
import org.aspectj.lang.annotation.*;
@Aspect
public class Audience {
@Pointcut("execution(* com.lee.concert.Performance.perform(..))")
public void performance(){}
@Before("performance()")
public void silenceCellPhones(){
System.out.println("请电话静音");
}
@Before("performance()")
public void takeSeats(){
System.out.println("请坐");
}
@AfterReturning("performance()")
public void applause(){
System.out.println("热烈鼓掌");
}
@AfterThrowing("performance()")
public void demandRefund(){
System.out.println("演砸了");
}
}
performance()的实际内容并不重要,所以这里是空的。其实该方法本身只是一个标识而已。
当然,除了注解和没有实际操作的performance()方法。Audience类本身依然是一个POJO。我们能够像使用其他java类一样调用它的方法,它的方法也能够独立的进行单元测试。
@Bean
public Audicence audience(){
return new Audience();
}
而如果你使用了AspectJ注解,它仍然不被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。如果你使用JavaConfig的话,可以使用@EnableAspectJAutoProxy
注解启动自动代理功能:
package com.lee.concert;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class ConcertConfig {
@Bean
public Audience audience(){
return new Audience();
}
}
而如果用XML的话,使用
即可。
创建环绕通知
package com.lee.concert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
public class Audience {
@Pointcut("execution(* com.lee.concert.Performance.perform(..))")
public void performance(){}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp){
try {
System.out.println("请电话静音");
System.out.println("请坐");
jp.proceed();
System.out.println("热烈鼓掌");
} catch (Throwable throwable) {
System.out.println("演砸了");
}
}
}
@Around
注解表明watchPerformance()方法会作为performance()切点的环绕通知,这个通知的关键在于ProceedingJoinPoint作为参数的。它的proceed()方法不能忘记调用,如果不调用这个方法,你会阻塞被通知方的调用。当然你也可以多次调用已达到通知失败后的重复尝试。
处理通知中的参数
如果切面所通知的方法有参数,即我们例子中的perform()方法有参数。切面能访问和使用传递给被通知方法的参数么?
为了阐述这个问题,我们回到BlankDisc样例,假如我们有一个统计磁道并调用playTrack()方法。我们想记录每个磁道被播放的次数,一种方法是直接修改playTrack()方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,所以不应该包含在playTrack()方法内。
为了记录每个磁道播放的次数,我们创建了TrackCounter类:
package com.lee.soundsystem.properties;
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.lee.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);
}
public int getPlayCount(int trackNumber){
return trackCounts.containsKey(trackNumber)?trackCounts.get(trackNumber):0;
}
}
可以看出args(trackNumber)就是获取参数的关键,他的类型是int的。参数的名称和方法签名中的参数匹配。现在,我们可以将BlankDisc和TrackCounter定义为bean。
package com.lee.soundsystem.properties;
import com.lee.soundsystem.CompactDisc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {
@Bean
public CompactDisc sgtPeppers(){
BlankDisc cd=new BlankDisc();
cd.setTitle("this is title");
cd.setArtist("this is artist");
List<String> tracks=new ArrayList<>();
tracks.add("1 d");
tracks.add("2 d");
tracks.add("3 d");
tracks.add("4 d");
//..更多磁道
cd.setTracks(tracks);
return cd;
}
@Bean
public TrackCounter trackCounter(){
return new TrackCounter();
}
}
最后我们编写如下的测试,验证它能工作:
package com.lee.soundsystem.properties;
import com.lee.soundsystem.CompactDisc;
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;
import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TrackCounterConfig.class)
public class TrackCounterConfigTest {
@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(2);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(3);
assertEquals(1,counter.getPlayCount(1));
assertEquals(2,counter.getPlayCount(2));
assertEquals(3,counter.getPlayCount(3));
}
}
很明显,获取了playTrack的arg。
通过注解引入新功能
回顾一下,在Spring中,切面只是实现了它们所包装bean相同接口的代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢?
我们需要注意的是,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上一个bean的实现被拆分到了多个类中。
为了证明该注意能够行得通,我们为Performance实现引入了Encoreable接口:
package com.lee.concert;
public interface Encoreable {
void performEncore();
}
现在假设能够访问Performance的所有实现,并对其进行修改,让它们都实现Encoreable接口。但是从设计角度来看,这并不是很好的做法,并不是所有Performance都是具有Encoreable特性的。另一方面,有时候可能无法修改所有的Performance实现,当使用第三方实现并没有源码的时候更是如此。
值得庆幸的是,借助AOP的引入功能,我们可以不必在设计上妥协或者侵入性的改变现有实现:
package com.lee.concert;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value = "com.lee.concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
可以看到EncoreableIntroducer是一个切面,但是它与我们之前所创建的切面不同,它并没有提供前置、后置或者环绕通知,而是通过@DeclareParents
注解将Encoreable接口引入到Performance bean中。
@DeclareParents
由三部分组成:
+
表示Performance的所有子类型,而不是Performance本身)了解。暂略。P125