1、在软件系统中,有很多功能是被动调用的(这些功能不是主要关注的),在很多地方都需要明确的调用。这种被分散到多处的功能称为“横切关注点”。将横切关注点和业务逻辑分析是AOP要解决的问题。AOP将横切关注点模块化为了一些特殊的类,这些类称为切面。(以声明的方法定义横切关注点要以何时何地调用)
2、AOP术语:
1:通知(advice)
切面必须要完成的工作称之为通知。
通知除了描述要完成的工作之外,还描述了何时执行这个工作,它应该应用在什么方法之前,什么方法之后(方法连接点)。
5种类型的通知:
1:前置通知:在目标方法调用之前调用通知功能
2:后置通知:在目标方法完成之后调用通知,此时不会关心方法的输出是什么
3:返回通知:在目录方法成功执行之后调用的通知
4:异常通知:在目标方法抛出异常时调用的通知
5:环绕通知:包含了被通知的方法,在被通知方法执行之前和执行之后执行的自定义行为。
2:连接点(Join point)
应用执行过程中能够插入切面的一个点,可以是一个方法调用时、抛出异常时、修改字段时(其实就是一个触发的时机,查电表的电表)。
3:切点(pointcut)
切点匹配一部分连接点。
4:切面(Aspect)
切面是切点和通知的结合(切面知道自己要对谁做什么事情),通知和切点共同定义了切面的全部内容(是什么,在何时何处完成其功能)
5:引入(Introduction):
引入允许我们向现有的类添加新方法和新属性。
6:织入(Weaving):
把切面应用到目标对象,并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期中的编译器、类加载期、运行期被织入:
3、Spring AOP 使用代理类将目标对象包裹。当调用者调用目标对象时,会被代理类拦截。
由于Spring AOP 是基于代理的,所以只支持方法连接点(不支持构造方法连接点)。
4、Spring AOP仅支持AspectJ切点指示器的一个子集。
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配 AOP 代理的 bean 引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里)
@annotation 限定匹配带有指定注解的连接点
5、编写切点
例如:
//定义一个表示表演的接口
package concert;
public interface performance{
public void perform();
}
//我们要编写一个perform()触发的通知。下面是一个切点表达式,这个切点表达式设置了perform调用触发通知的调用。
execution(* concert.Performance.perform(..))
说明:
execution:在方法执行时触发
*:不关心返回值
concert.Performance.perform:方法全路径
(..)任意参数
现在我们配置的切点只匹配concert包。可以使用within()指示器来限制匹配。
execution(* concert.Performance.perform(..)) && within(concert.*)
说明:
&& : 且(还有|| or !)
within(concert.*) : concert包下的任意方法被调用。
6、在切点中选择bean
bean() 是Spring新引入的指示器,可以在切点表达式中使用bean的ID来标识bean。
使用beanID或者名称作为参数来限制切点只匹配到特定的bean。
例如:
execution(* concert.Performance.perform()) and bean('BeanID')
执行concert.Performance.perform()时触发,但是bean的ID是'BeanID'
类似的:
execution(* concert.Performance.perform()) and !bean('BeanID')
7、使用注解创建切面:
AspectJ提供了5个注解来表明应该什么时候调用:
@After 通知方法会在目标方法返回或抛出异常后调用
@AfterReturning 通知方法会在目标方法返回后调用
@AfterThrowing 通知方法会在目标方法抛出异常后调用
@Around 通知方法会将目标方法封装起来
@Before 通知方法会在目标方法调用之前执行
下面使用@Aspect注解定义了一个“观众”切面(我们认为:观众是表演的横向关注点)
package concert;
@Aspect //定义了一个切面
public class Audience{
@Before("execution("** concert.Performance.perform(..)")") //这个通知方法会在目标方法调用之前调用
public void 手机静音(){//演出之前鼓掌}
@AfterReturning("execution("** concert.Performance.perform(..)")") //这个通知方法会在目标方法调用之后调用
public void 鼓掌(){//表演之后鼓掌}
@AfterThrowing("execution("** concert.Performance.perform(..)")") //这个通知方法会在目标方法抛出异常时调用
public void 退款(){//表演失败时要求退款}
}//上面这段代码重复的传入了3次切点表达式。
可以使用@Pointcut注解定义重用的节点。
package concert;
@Aspect //定义了一个切面
public class Audience{
@Pointcut("execution("** concert.Performance.perform(..)")") //定义命名的切点
public void performance(){} //这个方法的方法体不重要,这个方法只是@Pointcut的载体
@Before("performance()")
public void 手机静音(){}
@AfterReturning("performance()")
public void 鼓掌(){}
@AfterThrowing("performance()")
public void 退款(){}
}
8、启动AspectJ注解的自动代理
(7中配置了切面,只有启用了注解的自动代理才可以用)
1:在JavaConfig中配置:
@Configuration
@EnableAspectJAutoproxy //启动AspectJ自动代理
@ComponentScan
public class Config{
@Bean
public Audience audience(){return new Audience();} //声明Audience的Bean
}
2:使用xml配置:
<beans xmlns:xxx/yyy/aop> //引入Spring AOP命名空间
<context:component-scan base-package = "concert">
<aop:aspectJ-autoproxy /> //启用AspectJ自动代理
<bean class = "concert.Audience">
</beans>
注意:无论使用哪种方法,@Aspect注解的bean都会被创建一个代理,包裹切点匹配到的所有的bean周围。(形成了代理包裹目标bean的模型)
9、创建环绕通知
在一个通知方法中同时编写前置和后置通知。
例如:
@Aspect
public class Audience{
@Pointcut("execution(** concert.Performance.perform(..))") //定义命名的切点
public void performance(){}
@Around("performance()") //环绕通知方法(表示watchPerformance方法将作为performance切点的环绕通知)
public void watchPerformance(ProceedingJoinPoint jp){
try{
//调用前置通知
jp.proceed(); //掉用ProceedingJoinPoint的proceed()将控制权交给目标方法。(如果不写,将导致目标方法的调用阻塞。多次调用可以实现重试)
//调用后置通知
}catch(Throwable e){
//调用异常通知
}
}
}
10、处理通知中的参数:
之前说过光盘中的磁道。假设:playTrack()方法用于播放光盘的某一个磁道,如果想要记录磁道的播放次数。最简单的办法是在playTrack()中添加一个计数的标记。但是计数这个事情怎么看都应该又切面来完成。
使用切面记录磁道播放次数的例子:
@Aspect
public class TrackCount{
private Map<Integer , Integer> trackCounts = new HashMap<Integer , Integer>();
//playTrack(int):接收int类型的参数 args(trackNumber):指定参数。这个参数最终会被传递到通知中去。
@Pointcut("execution(* xxx.yyy.playTrack(int)) && args(trackNumber)")
public void trackPlayed(int trackNumber){}
@Before("trackPlayed(trackNumber)")
public void countTrack(int trackNumber){
int currentCount = trackCounts.containsKey(trackNumber) ? trackCount.get(trackNumber) : 0;
trackCount.put(trackNumber , currentCount + 1);
}
}
//接下来将光盘类和TrackCount类定义为Bean,并启用AspectJ自动代理:
@Configuration
@EnableAspectJAutoproxy //启动AspectJ自动代理
public class TrackCounterConfig{
@Bean
public 光盘类 方法名(){
光盘 盘 = new 光盘();
。。。
return 盘;
}
@Bean
public TrackCounter trackCounter(){
return new TrackCounter();
}
}
11、通过注解引入新功能
?????????????????????????????
12、在xml中声明切面
Spring的aop命名空间中,提供了多个元素在xml中声明切面:
<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 = "pojoBean"> //这个POJOBean中定义了切面的功能(就是前置通知方法,后置通知方法等)
<aop:before pointcut = "execution(** xxx.yyy.perform(..))" method = "method1" /> //method1.2.3都定义在了上面引入的pojoBean
<aop:after-returning "pointcut = "execution(** xxx.yyy.perform(..))" method = "method2" />
<aop:after-throwing "pointcut = "execution(** xxx.yyy.perform(..))" method = "method3" />
</aop:aspect>
</aop:config>
//上面的切点重复编写了。
抽取切点声明:
<aop:config>
<aop:aspect ref = "pojoBean"> //这个POJOBean中定义了切面的功能(就是前置通知方法,后置通知方法等)
<aop:pointcut id = "performance" expression = "execution(** xxx.yyy.perform(..))" />
<aop:before pointcut-ref = "performance" method = "method1" /> //method1.2.3都定义在了上面引入的pojoBean
<aop:after-returning pointcut-ref = "performance" method = "method2" />
<aop:after-throwing pointcut-ref = "performance" method = "method3" />
</aop:aspect>
</aop:config>
13、使用xml定义环绕通知
去掉注解的AOP环绕通知方法
public class Audience{
public void watchPerformance(ProceedingJoinPoint jp){
try{
前置通知();
jp.proceed();
后置通知();
}catch(Throwable e){
异常通知();
}
}
}
在xml中使用<aop:around>元素声明环绕通知
<aop:config>
<aop:aspect ref = "audience"> //链接环绕方法所在的Bean
<aop:pointcut id = "performance" expression = "execution(** xxx.yyy.perform(..))"> //定义切点
<aop:around pointcut-ref = "performance" method = "watchPerformance"> //给切点关联环绕通知方法。
</aop:config>
14、使用xml为通知传递参数:
上面的笔记中有提到。记录光盘的磁道赌取次数的案例,下面是使用xml配置的相同案例:
将切面注解去掉后的代码:
public class TrackCount{
private Map<Integer , Integer> trackCounts = new HashMap<Integer , Integer>();
public void countTrack(int trackNumber){
int currentCount = trackCounts.containsKey(trackNumber) ? trackCount.get(trackNumber) : 0;
trackCount.put(trackNumber , currentCount + 1);
}
}
xml配置文件;
<beans>
<bean id = "trackCount" class = "xxx.yyy.TrackCount">
<bean id = "cd" class = "xxx.yyy.光盘">
<property name = "" value = "">
<property name = "磁道">
<list>
<value>"..."</value>
</list>
</property>
</bean>
<aop:config>
<aop:aspect ref = "trackCount"> //将TrackCount声明为切面
<aop:pointcut id = "trackPlayed" expression = "execution(* xxx.yyy.playTrack(int)) and args(trackNumber)" />
<aop:before pointcut-ref = "trackPlayed" method = "countTrack" />
</aop:aspect>
</aop:config>
</beans>
15、通过xml引入新的功能
????????????
16、注入AspectJ切面
Spring的AOP解决方法相对于AspectJ非常粗糙的。可以使用Spring的依赖注入将AspectJ切面注入到Spring中使用。
???????????