Spring学习(四)--面向切面的Spring

一.Spring--面向切面
  在软件开发中,散布于应用中多处的功能被称为横切关注点(cross- cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业 务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把 这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
--什么是面向切面编程
  切面能帮助我们模块化横切关注点。简而言之,横切关注 点可以被描述为影响应用多处的功能。例如,安全就是一个横切关注 点,应用中的许多方法都会涉及到安全规则:

--如上图展现的模块划分的典型应用.每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如:安全及事务.如果要重用通用功能的话,那么最常见的面向对象的技术就是继承和委托,但是如果在整个应用中都是用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托则需要对委托对象进行复杂的调用.而使用切面编程的技术,则可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类.横切关注点就可以被模块化为特殊的类,这些类被称为切面(sapect).这样做有两个好处:首先,现在每个 关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模 块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次 要关注点的代码被转移到切面中了。
--定义AOP术语
  与大多数技术一样,AOP已经形成了自己的术语。描述切面的常用术 语有通知(advice)、切点(pointcut)和连接点(join point):

--通知(Advice):正如之前所提到的Knight的示例,骑士在接收了任务之后,不需要去强迫一个吟游诗人来完成对其功绩的歌颂,他只需要最好自己分内的事(拯救公主或者杀死恶龙),无需去关心吟游诗人是在什么时候.怎么样称赞他的.因此切面也有目标——它必须要完成的工作。在AOP术语中,切 面的工作被称为通知。通知定义了切面是什么以及何时使用。除了描述切面要完成的工作, 通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调 用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?在Spring之中,提供了5种通知的类型:
  1.前置通知(Before):在目标方法被调用之前调用通知功能;
  2.后置通知(After):在目标方法完成之后调用通知,此时不会关 心方法的输出是什么;
  3.返回通知(After-returning):在目标方法成功执行之后调用通 知;
  4.异常通知(After-throwing):在目标方法抛出异常后调用通知;
  5.环绕通知(Around):通知包裹了被通知的方法,在被通知的方 法调用之前和调用之后执行自定义的行为。
--连接点(Join Point):对于骑士来说,完成了他的任务之后,必然会在悬赏公告牌中告知大家这个骑士完成了怎样的壮举,而对于吟游诗人来说这个发布骑士完成任务的点就是他所需要进行赞颂准备的点.对于切面来说,这就是拯救点.事实上,在切面编程之中,这个连接点可以是调用方法时(骑士正在执行任务),抛出异常时(骑士执行任务遇到困难时),程序修改一个字段时(骑士在中途进行修整时),切面代码 可以利用这些点插入到应用的正常流程之中,并添加新的行为。
--切点(Pointcut):对于吟游诗人来说,他不需要去关心这个骑士中途多有详细任务报告的公布(各个连接点),对于他来说,他十分清楚自己的职责(专门负责歌颂骑士的凯旋归来又或者是专门歌颂骑士在中途遇到困难时的坚韧不拔),而这就是所谓的切点.类似地,一个切面并不需要通知应用的所有连接点。切点有助于 缩小切面所通知的连接点的范围。如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义 了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。
--切面(Aspect):当一个吟游诗人准备歌颂一个骑士的时候,他需要明确的整理自己收集到的这个骑士他所需要称赞的点(执行过程还是凯旋归来的结局)的所有细节.切面就是通知和切点的结合通知和切点共同定义了切面的全部内容 ——它是什么,在何时和何处完成其功能。
--引入(Introduction):引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一 个Auditable通知类,该类记录了对象最后一次修改时的状态。这 很简单,只需一个方法,setLastModified(Date),和一个实例 变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到 现有的类中,从而可以在无需修改这些现有的类的情况下,让它们具 有新的行为和状态。
--织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指 定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点 可以进行织入:
  1.编译期:切面在目标类编译时被织入。这种方式需要特殊的编译 器。AspectJ的织入编译器就是以这种方式织入切面的。
  2.类加载期:切面在目标类加载到JVM时被织入。这种方式需要特 殊的类加载器(ClassLoader),它可以在目标类被引入应用 之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
  3.运行期:切面在应用运行的某个时刻被织入。一般情况下,在织 入切面时,AOP容器会为目标对象动态地创建一个代理对象。 Spring AOP就是以这种方式织入切面的。
--总结:通知包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的 具体位置(在哪些连接点)。其中关键的概念是切点定义了哪些连接 点会得到通知。

二.Spring对AOP的支持
   Spring提供了四种类型的AOP支持:
    1.基于代理的经典Spring AOP;
    2.纯POJO切面;
    3.@AspectJ注解驱动的切面;
    4.注入式AspectJ切面(适用于Spring各版本)。
--前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础 之上,因此,Spring对AOP的支持局限于方法拦截。借助Spring的aop命名空间,我们可以将纯POJO转换为切面。实际 上,这些POJO只是提供了满足切点条件时所要调用的方法。这种技术需要XML配置.在深入探讨Spring的AOP技术之前,我们需要对Spring AOP框架的一些关键性技术进行了解:
  1.Spring通知是Java编写的:Spring所创建的通知都是用标准的Java类编写的。这样的话,我们就 可以使用与普通Java开发一样的集成开发环境(IDE)来开发切面。 而且,定义通知所应用的切点通常会使用注解或在Spring配置文件里 采用XML来编写,这两种语法对于Java开发者来说都是相当熟悉的。
  2.Spring在运行时通知对象:通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理 的bean中,代理类封装了目标类,并拦截被通知方法的 调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时, 在调用目标bean方法之前,会执行切面逻辑:

  直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的 是ApplicationContext的话,在ApplicationContext从 BeanFactory中加载所有bean的时候,Spring才会创建被代理的对 象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译 器来织入Spring AOP的切面。
  3.Spring只支持方法级别的连接点:因为Spring基于动态代理,所以Spring只支持方法连接点。Spring缺少对字段连接点的 支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而 且它不支持构造器连接点,我们就无法在bean创建时应用通知。

三,通过切点来选择连接点
  在Spring AOP中,使用AspectJ的切点表达式语言来定义切点;Spring是基于代理的,而某些切点表达式是于基于代理的AOP无关的,给出SpringAOP支持的AspectJ切点指示器:

--当我们查看如上所展示的这些Spring支持的指示器时,注意只 有execution指示器是实际执行匹配的,而其他的指示器都是用来 限制匹配的。这说明execution指示器是我们在编写切点定义时最 主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。
--编写切点:定义Performance接口作为我们的自定义切面的切点:

 1 package 面向切面编程;
 2 
 3 /**
 4  * @author : S K Y
 5  * @version :0.0.1
 6  */
 7 public interface Performance {
 8     /**
 9      * 触发方法
10      */
11     void perform();
12 }

--观察切点表达式的构成:

--使用execution()指示器选择Performance的perform()方 法。方法表达式以“*”号开始,表明了我们不关心方法返回值的类 型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我 们使用两个点号(..)表明切点要选择任意的perform()方法,无 论该方法的入参是什么,假设现在所需要的切点仅匹配concert包:

--使用"&&"操作符讲execution()和within()指示器连接在一起(切点必须匹配所有的指示器),类 似地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操 作符来标识非(not)操作(因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切 点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来 代替“||”和“!”).
--在切点中选择bean,Spring还引入了一个新的bean指示,他允许我们在切点表达式中使用bean的ID作为标识来限定指示器:execution(* concert.Performance.perform()) and bean('student');这个表达式表名了我们希望执行performance的perform()方法时应用通知,但是限定了bean的ID为student,当然也可以限定在bean的ID不是'student'的情况下应用通知:execution(* concert.Performance.perform()) and !bean('student');
--使用注解创建切面

 1 package 面向切面编程.concert;
 2 
 3 import org.aspectj.lang.annotation.AfterReturning;
 4 import org.aspectj.lang.annotation.AfterThrowing;
 5 import org.aspectj.lang.annotation.Aspect;
 6 import org.aspectj.lang.annotation.Before;
 7 
 8 /**
 9  * @author : S K Y
10  * @version :0.0.1
11  */
12 @Aspect     //使用@Aspect注解将该类设置为特殊的切面类
13 public class Audience {
14     private String name;
15 
16     @Before("execution(* *.concert.Performance.perform(..))")
17     public void silenceCellPhone() {     //在表演开始之前,观众需要将他的手机静音
18         System.out.println("表演即将开始,观众" + name + "将他的手机设置为了静音状态;");
19     }
20 
21     @Before("execution(* *.concert.Performance.perform(..))")
22     public void takeSeats() {
23         System.out.println("表演还有5分钟就要开始了,观众" + name + "入座等待;");
24     }
25 
26     @AfterReturning("execution(* *.concert.Performance.perform(..))")
27     public void applause() {
28         System.out.println("表演结束,观众" + name + "为演员们热烈的鼓掌;");
29     }
30 
31     @AfterThrowing("execution(* *.concert.Performance.perform(..))")
32     public void demandRefund() {
33         System.out.println("由于线程设备故障,观众" + name + "要求退款....");
34     }
35 }

--在AspectJ中提供了五个注解来定义通知:

--在配置文件中,如我我们想要开启切面支持,那么需要让Spring配置文件知道我们切面类的存在,在JavaConfig中我们可以使用@EnableAspectJAutoProxy注解来开启AspectJ的自动代理,同时还需要开启@ComponentScan注解来实现我们的包扫描,从而Spring才能识别我们@AspectJ注解声明的类:

 1 package 面向切面编程.concert;
 2 
 3 /**
 4  * @author : S K Y
 5  * @version :0.0.1
 6  */
 7 public class DancingPerformance implements Performance {
 8     @Override
 9     public void perform() {
10         System.out.println("演员们表演精彩的舞蹈....");
11     }
12 }
 1 package 面向切面编程.concert;
 2 
 3 import org.springframework.context.annotation.Bean;
 4 import org.springframework.context.annotation.ComponentScan;
 5 import org.springframework.context.annotation.Configuration;
 6 import org.springframework.context.annotation.EnableAspectJAutoProxy;
 7 
 8 /**
 9  * @author : S K Y
10  * @version :0.0.1
11  */
12 @Configuration
13 @EnableAspectJAutoProxy     //启用AspectJ的自动代理
14 @ComponentScan(basePackages = {"面向切面编程.concert"})
15 public class AudienceConfig {
16     @Bean
17     public Audience audienceA() {
18         return new Audience("观众A");
19     }
20 
21     @Bean
22     public Performance performance() {
23         return new DancingPerformance();
24     }
25 }
 1 package 面向切面编程.concert;
 2 
 3 import org.junit.Test;
 4 import org.junit.runner.RunWith;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.test.context.ContextConfiguration;
 7 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 8 
 9 /**
10  * @author : S K Y
11  * @version :0.0.1
12  */
13 @RunWith(SpringJUnit4ClassRunner.class)
14 @ContextConfiguration(classes = AudienceConfig.class)
15 public class AudienceTest {
16 
17     @Autowired
18     private Performance performance;
19 
20     @Test
21     public void testAudienceA() {
22         performance.perform();
23     }
24 }

--运行结果

表演即将开始,观众观众A将他的手机设置为了静音状态;
表演还有5分钟就要开始了,观众观众A入座等待;
演员们表演精彩的舞蹈....
表演结束,观众观众A为演员们热烈的鼓掌;

Process finished with exit code 0

--可以看到使用注解轻松的实现了我们的AOP编程,但是,此时仍然存在问题,在Audience中我们多次书写了相同的切面表达式,事实上,我们可以使用@Pointcut注解来声明我们的切点表达式

 1 package 面向切面编程.concert;
 2 
 3 import org.aspectj.lang.annotation.*;
 4 
 5 /**
 6  * @author : S K Y
 7  * @version :0.0.1
 8  */
 9 @Aspect     //使用@Aspect注解将该类设置为特殊的切面类
10 public class Audience {
11     private String name;
12 
13     public Audience(String name) {
14         this.name = name;
15     }
16 
17     @Pointcut("execution(* *.concert.Performance.perform(..))")
18     public void performance() {
19     }
20 
21     @Before("performance()")
22     public void silenceCellPhone() {     //在表演开始之前,观众需要将他的手机静音
23         System.out.println("表演即将开始,观众" + name + "将他的手机设置为了静音状态;");
24     }
25 
26     @Before("performance()")
27     public void takeSeats() {
28         System.out.println("表演还有5分钟就要开始了,观众" + name + "入座等待;");
29     }
30 
31     @AfterReturning("performance()")
32     public void applause() {
33         System.out.println("表演结束,观众" + name + "为演员们热烈的鼓掌;");
34     }
35 
36     @AfterThrowing("performance()")
37     public void demandRefund() {
38         System.out.println("由于线程设备故障,观众" + name + "要求退款....");
39     }
40 
41 }

--在Audience中,performance()方法使用了@Pointcut注解。 为@Pointcut注解设置的值是一个切点表达式,就像之前在通知注 解上所设置的那样。通过在performance()方法上添 加@Pointcut注解,我们实际上扩展了切点表达式语言,这样就可 以在任何的切点表达式中使用performance()了,如果不这样做的 话,你需要在这些地方使用那个更长的切点表达式。performance()方法的实际内容并不重要,在这里它实际上应该是 空的。其实该方法本身只是一个标识,供@Pointcut注解依附。
--使用XML完成配置:如果想要在Spring的XML配置文件中来装配这个@AspectJ声明的bean的话,那么需要使用Spring aop命名空间中的<aop:aspectj-autoproxy/>来完成:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
 4        xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context"
 5        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 6     <!--开启AspectJ动态代理-->
 7     <aop:aspectj-autoproxy/>
 8     <bean class="面向切面编程.concert.Audience" c:name="小明"/>
 9     <bean class="面向切面编程.concert.DancingPerformance" id="performance"/>
10 </beans>
 1 package 面向切面编程.concert;
 2 
 3 import org.junit.Test;
 4 import org.junit.runner.RunWith;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.test.context.ContextConfiguration;
 7 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 8 
 9 /**
10  * @author : S K Y
11  * @version :0.0.1
12  */
13 @RunWith(SpringJUnit4ClassRunner.class)
14 @ContextConfiguration(locations = {"classpath:面向切面编程/concert/audienct-config.xml"})
15 public class XMLConfigTest {
16     @Autowired
17     private Performance performance;
18 
19     @Test
20     public void testAudience() {
21         performance.perform();
22     }
23 }

--创建环绕通知
  环绕通知是最为强大的通知类型,可以在一个通知方法中同时完成前置通知,后置通知以及异常通知,重写我们的Audience类:

 1 package 面向切面编程.concert;
 2 
 3 import org.aspectj.lang.ProceedingJoinPoint;
 4 import org.aspectj.lang.annotation.*;
 5 
 6 /**
 7  * @author : S K Y
 8  * @version :0.0.1
 9  */
10 @Aspect     //使用@Aspect注解将该类设置为特殊的切面类
11 public class Audience {
12     private String name;
13 
14     public Audience(String name) {
15         this.name = name;
16     }
17 
18     @Pointcut("execution(* *.concert.Performance.perform(..))")
19     public void performance() {
20     }
21 
22     @Around("performance()")
23     public void watchPerform(ProceedingJoinPoint point) {     //观众观看演出
24         try {
25             System.out.println("表演即将开始,观众" + name + "将他的手机设置为了静音状态;");
26             System.out.println("表演还有5分钟就要开始了,观众" + name + "入座等待;");
27             point.proceed();            //执行方法
28             System.out.println("表演结束,观众" + name + "为演员们热烈的鼓掌;");
29         } catch (Throwable throwable) {
30             System.out.println("由于线程设备故障,观众" + name + "要求退款....");
31         }
32     }
33 }

--在这个新的通知方法中,接受一个ProceedingJoinPoint作为参数,并且这个对象是必须要有的,因为我们需要在通知中通过他来调用被通知的方法,通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,那么会调用ProceedingJoinPoint的proceed()方法.如果我们不去调用这个方法,我们的通知会阻塞我们被通知方法的调用.有意思的是,你可以不调用proceed()方法,从而阻塞对被通知方 法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样 做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行 重复尝试。
--处理通知中的参数
  到目前为止,我们的切面都很简单,没有任何参数。唯一的例外是我 们为环绕通知所编写的watchPerformance()示例方法中使用了 ProceedingJoinPoint作为参数。除了环绕通知,我们编写的其 他通知不需要关注传递给被通知方法的任意参数。这很正常,因为我 们所通知的perform()方法本身没有任何参数。但是如果我们进行切面通知的方法确实有参数存在,切面如何访问和使用传递给被通知的方法的参数呢?例如:对于一个晚会表演来说,主持人也是必不可少的,对于Performance来说,可能存在多个需要表演的节目,但是对于当前的表演来说,需要通知观众进行表演的不应该是演员和表演本身(即不应该由Performance类来完成对于晚会的播报环节):

 1 package 面向切面编程.concert;
 2 
 3 /**
 4  * @author : S K Y
 5  * @version :0.0.1
 6  */
 7 public interface Performance {
 8     /**
 9      * 进行表演
10      */
11     void perform();
12 
13     /**
14      * 在拥有多个表演的节目的时候,表演当前指定的节目
15      *
16      * @param index 当前指定的节目的索引
17      * @param name  当前表演的节目的名称
18      */
19     void perform(int index, String name);
20
 1 package 面向切面编程.concert;
 2 
 3 import org.aspectj.lang.annotation.Aspect;
 4 import org.aspectj.lang.annotation.Before;
 5 import org.aspectj.lang.annotation.Pointcut;
 6 
 7 import java.util.List;
 8 
 9 /**
10  * @author : S K Y
11  * @version :0.0.1
12  */
13 @Aspect
14 public class Host {
15     private List<String> dancingList;
16 
17     @Pointcut("execution(* *.concert.Performance.perform(.. )) && args(index,name)")
18     private void performance(int index, String name) {
19     }
20 
21     @Before("performance(index,name)")
22     public void host(int index, String name) {
23         System.out.println("[主持人]:当前为大家带来的是第" + (index + 1) + "个表演" + name +
24                 ".请大家欣赏;");
25     }
26 
27     public List<String> getDancingList() {
28         return dancingList;
29     }
30 
31     public void setDancingList(List<String> dancingList) {
32         this.dancingList = dancingList;
33     }
34 }

--只有主持人知道当前所需要进行的表演的序号和名称,而所有表演的演员只需要按照主持人所给出的清单进行表演:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
 4        xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context"
 5        xmlns:util="http://www.springframework.org/schema/util"
 6        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
 7     <!--开启AspectJ动态代理-->
 8     <aop:aspectj-autoproxy/>
 9     <bean class="面向切面编程.concert.Audience" c:name="小明"/>
10     <util:list id="dancingList">
11         <value>芭蕾舞表演</value>
12         <value>孔雀舞表演</value>
13         <value>名族舞表演</value>
14         <value>花式敲代码表演</value>
15     </util:list>
16     <bean class="面向切面编程.concert.DancingPerformance" id="performance"/>
17     <bean class="面向切面编程.concert.Host">
18         <property name="dancingList" ref="dancingList"/>
19     </bean>
20 
21 </beans>
 1 package 面向切面编程.concert;
 2 
 3 import org.junit.Test;
 4 import org.junit.runner.RunWith;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.test.context.ContextConfiguration;
 7 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 8 
 9 import java.util.List;
10 
11 /**
12  * @author : S K Y
13  * @version :0.0.1
14  */
15 @RunWith(SpringJUnit4ClassRunner.class)
16 @ContextConfiguration(locations = {"classpath:面向切面编程/concert/audienct-config.xml"})
17 public class XMLConfigTest {
18     @Autowired
19     private Performance performance;
20     @Autowired
21     private Host host;
22 
23     @Test
24     public void testAudienceWithInt() {
25         List<String> dancingList = host.getDancingList();
26         for (int i = 0; i < dancingList.size(); i++) {
27             performance.perform(i, dancingList.get(i));
28         }
29     }
30 }

--运行结果

表演即将开始,观众小明将他的手机设置为了静音状态;
表演还有5分钟就要开始了,观众小明入座等待;
[主持人]:当前为大家带来的是第1个表演芭蕾舞表演.请大家欣赏;
演员们表演精彩的芭蕾舞表演...
表演结束,观众小明为演员们热烈的鼓掌;
表演即将开始,观众小明将他的手机设置为了静音状态;
表演还有5分钟就要开始了,观众小明入座等待;
[主持人]:当前为大家带来的是第2个表演孔雀舞表演.请大家欣赏;
演员们表演精彩的孔雀舞表演...
表演结束,观众小明为演员们热烈的鼓掌;
表演即将开始,观众小明将他的手机设置为了静音状态;
表演还有5分钟就要开始了,观众小明入座等待;
[主持人]:当前为大家带来的是第3个表演名族舞表演.请大家欣赏;
演员们表演精彩的名族舞表演...
表演结束,观众小明为演员们热烈的鼓掌;
表演即将开始,观众小明将他的手机设置为了静音状态;
表演还有5分钟就要开始了,观众小明入座等待;
[主持人]:当前为大家带来的是第4个表演花式敲代码表演.请大家欣赏;
演员们表演精彩的花式敲代码表演...
表演结束,观众小明为演员们热烈的鼓掌;
九月 08, 2019 6:24:05 下午 org.springframework.context.support.GenericApplicationContext doClose
信息: Closing org.springframework.context.support.GenericApplicationContext@67b6d4ae: startup date [Sun Sep 08 18:24:05 CST 2019]; root of context hierarchy

Process finished with exit code 0

--通过注解引入新功能
  一些编程语言,例如Ruby和Groovy,有开放类的理念。它们可以不用 直接修改对象或类的定义就能够为对象或类增加新的方法。不过, Java并不是动态语言。一旦类编译完成了,我们就很难再为该类添加 新的功能了。但是事实上,我们一直在使用AOP功能为已经编写完的方法来增加额外的功能,此外,其实我们也可以为一个对象新增方法,利用被称为引入的AOP概念,切面可以为Spring bean添加新方法.回顾一下,在Spring中,切面只是实现了它们所包装bean相同接口的 代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎么样 呢?那样的话,切面所通知的bean看起来像是实现了新的接口,即便 底层实现类并没有实现这些接口也无所谓。

--为Performance实现引入MoreForPerformance接口

 1 package 面向切面编程.concert;
 2 
 3 /**
 4  * @author : S K Y
 5  * @version :0.0.1
 6  */
 7 public interface MoreForPerformance {
 8     /**
 9      * 在表演之中做更多的事
10      */
11     void moreForPerformance();
12 }

--我们需要一种方式将这个接口应用到Performance实现中.如果当前Performance的实现数量并不多,找到所有的实现,为他们都实现MoreForPerformance接口是没有什么问题的,但是如果当前具有上百个Performance的实现,这样做就显得特别的麻烦和不可取了,而且我们需要知道的是并不是所有的Performance的实现都需要MoreForPerformance支持的.同时如果有部分实现是由第三方jar来完成的,那么我们也无法为所有的实现都添加MoreForPerformance接口.因此我们可以借助AOP的引入功能,我们可以不必在设计上妥协或者侵入性的改变现有的实现.为此,我们创建一个新的切面:

 1 package 面向切面编程.concert;
 2 
 3 /**
 4  * @author : S K Y
 5  * @version :0.0.1
 6  */
 7 public class MoreForPerformanceImpl implements MoreForPerformance {
 8     @Override
 9     public void moreForPerformance() {
10         System.out.println("[附加]舞蹈表演特别的棒,每一个舞者脸上都流露着灿烂的笑容...");
11     }
12 }
 1 package 面向切面编程.concert;
 2 
 3 import org.aspectj.lang.annotation.Aspect;
 4 import org.aspectj.lang.annotation.DeclareParents;
 5 
 6 /**
 7  * @author : S K Y
 8  * @version :0.0.1
 9  */
10 @Aspect
11 public class PointcutForMore {
12     @DeclareParents(value = "面向切面编程.concert.Performance+", defaultImpl = MoreForPerformanceImpl.class)
13     public static MoreForPerformance more;
14 }

--PointcutForMore是一个切面,但是其实现与其他所创建的切面都不同,他没有提供前置,后置,环绕等通知,而是通过@DeclareParents注解,将MoreForPerformance接口引入到了Performance bean中.
--@DeclareParents注解由三部分组成:
  1.value属性指定了哪种类型的bean要引入该接口。在本例中,也 就是所有实现Performance的类型。(标记符后面的加号表示 是Performance的所有子类型,而不是Performance本 身。)
  2.defaultImpl属性指定了为引入功能提供实现的类。在这里, 我们指定的是MoreForPerformanceImpl提供实现。
  3.@DeclareParents注解所标注的静态属性指明了要引入了接 口。在这里,我们所引入的是MoreForPerformance 接口。
--在Spring中,注解和自动代理提供了一种很便利的方式来创建切面。 它非常简单,并且只涉及到最少的Spring配置。但是,面向注解的切 面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到 这一点,必须要有源码。 如果你没有源码的话,或者不想将AspectJ注解放到你的代码之中, Spring为切面提供了另外一种可选方案:在Spring XML配置文件中声明切面.
--在XML中声明切面
  在Spring的aop命名空间之中,提供了多个元素用来在XML中声明切面:

 

 1 package 面向切面编程.concert;
 2 
 3 import org.aspectj.lang.ProceedingJoinPoint;
 4 import org.aspectj.lang.annotation.*;
 5 
 6 /**
 7  * @author : S K Y
 8  * @version :0.0.1
 9  */
10 @Aspect     //使用@Aspect注解将该类设置为特殊的切面类
11 public class Audience {
12     private String name;
13 
14     public Audience(String name) {
15         this.name = name;
16     }
17 
18     @Pointcut("execution(* *.concert.Performance.perform(..))")
19     public void performance() {
20     }
21 
22     public void silenceCellPhone() {     //在表演开始之前,观众需要将他的手机静音
23         System.out.println("表演即将开始,观众" + name + "将他的手机设置为了静音状态;");
24     }
25 
26     public void takeSeats() {
27         System.out.println("表演还有5分钟就要开始了,观众" + name + "入座等待;");
28     }
29 
30     public void applause() {
31         System.out.println("表演结束,观众" + name + "为演员们热烈的鼓掌;");
32     }
33 
34     public void demandRefund() {
35         System.out.println("由于线程设备故障,观众" + name + "要求退款....");
36     }
37 
38     @Around("performance()")
39     public void watchPerform(ProceedingJoinPoint point) {     //观众观看演出
40         try {
41             System.out.println("表演即将开始,观众" + name + "将他的手机设置为了静音状态;");
42             System.out.println("表演还有5分钟就要开始了,观众" + name + "入座等待;");
43             point.proceed();
44             System.out.println("表演结束,观众" + name + "为演员们热烈的鼓掌;");
45         } catch (Throwable throwable) {
46             System.out.println("由于线程设备故障,观众" + name + "要求退款....");
47         }
48     }
49 }

--现在在Audience中,除了watchPerform为环绕通知外,其他的方法都不是通知,为了避免测试结果混乱,我们可以先将@Around注解注释掉,定义我们的XML文件:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
 4        xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context"
 5        xmlns:util="http://www.springframework.org/schema/util"
 6        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
 7     <!--开启AspectJ动态代理-->
 8     <aop:aspectj-autoproxy/>
 9     <bean class="面向切面编程.concert.Audience" c:name="小明" id="audience"/>
10     <util:list id="dancingList">
11         <value>芭蕾舞表演</value>
12         <value>孔雀舞表演</value>
13         <value>名族舞表演</value>
14         <value>花式敲代码表演</value>
15     </util:list>
16     <bean class="面向切面编程.concert.DancingPerformance" id="performance"/>
17     <bean class="面向切面编程.concert.Host">
18         <property name="dancingList" ref="dancingList"/>
19     </bean>
20     <bean class="面向切面编程.concert.PointcutForMore"/>
21 
22     <aop:config>
23         <aop:aspect ref="audience">
24             <aop:pointcut id="pointcut" expression="execution(* *.concert.Performance.perform(..))"/>
25             <aop:before method="silenceCellPhone" pointcut-ref="pointcut"/>
26             <aop:before method="takeSeats" pointcut-ref="pointcut"/>
27             <aop:after-returning method="applause" pointcut-ref="pointcut"/>
28             <aop:after-throwing method="demandRefund" pointcut-ref="pointcut"/>
29         </aop:aspect>
30     </aop:config>
31 
32 </beans>

--在<aop:config>元素内,我们可以声明一个或多个通知器、切面 或者切点,给出AOP的整体业务逻辑:

--但是目前在前置通知和后置通知中有一些限制.如果不使用成员变量存储信息的话,在前置通知和后置通知之间共享信息特别的麻烦.假设除了进场关闭手机和表演结束后鼓掌,我们还希望观众确 保一直关注演出,并报告每个参赛者表演了多长时间。使用前置通知 和后置通知实现该功能的唯一方式是在前置通知中记录开始时间并在 某个后置通知中报告表演耗费的时间。但这样的话我们必须在一个成 员变量中保存开始时间。因为Audience是单例的,如果像这样保存 状态的话,将会存在线程安全问题。相对于前置通知和后置通知,环绕通知在这点上有明显的优势。使用 环绕通知,我们可以完成前置通知和后置通知所实现的相同功能,而 且只需要在一个方法中 实现。因为整个通知逻辑是在一个方法内实 现的,所以不需要使用成员变量保存.
--在XML中实现环绕通知的方式和其他通知是一样的.同样,对于具有参数传递的通知,我们也可以使用XML配置文件的形式来实现:

 1     <aop:config>
 2         <aop:pointcut id="pointcut" expression="execution(* *.concert.Performance.perform(..))"/>
 3         <aop:pointcut id="pointcutWithParam"
 4                       expression="execution(* *.concret.Performance.perform(..)) and args(index,name)"/>
 5         <aop:aspect ref="audience">
 6             <!--  <aop:before method="silenceCellPhone" pointcut-ref="pointcut"/>
 7              <aop:before method="takeSeats" pointcut-ref="pointcut"/>
 8              <aop:after-returning method="applause" pointcut-ref="pointcut"/>
 9              <aop:after-throwing method="demandRefund" pointcut-ref="pointcut"/>-->
10             <aop:around method="watchPerform" pointcut-ref="pointcut"/>
11         </aop:aspect>
12         <aop:aspect ref="host">
13             <!--注意在XML中不应该使用&& 而应该使用关键字and-->
14             <aop:before method="host" pointcut-ref="pointcutWithParam" arg-names="index,name"/>
15 
16         </aop:aspect>
17     </aop:config>

--同样我们也可以在XML中使用Spring AOP命名空间中的<aop:declare-parents>标签完成引入功能:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
 4        xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context"
 5        xmlns:util="http://www.springframework.org/schema/util"
 6        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
 7     <!--开启AspectJ动态代理-->
 8     <aop:aspectj-autoproxy/>
 9     <bean class="面向切面编程.concert.Audience" c:name="小明" id="audience"/>
10     <util:list id="dancingList">
11         <value>芭蕾舞表演</value>
12         <value>孔雀舞表演</value>
13         <value>名族舞表演</value>
14         <value>花式敲代码表演</value>
15     </util:list>
16     <bean class="面向切面编程.concert.DancingPerformance" id="performance"/>
17     <bean class="面向切面编程.concert.Host" id="host">
18         <property name="dancingList" ref="dancingList"/>
19     </bean>
20     <bean class="面向切面编程.concert.PointcutForMore" id="forMore"/>
21 
22     <aop:config>
23         <aop:pointcut id="pointcut" expression="execution(* *.concert.Performance.perform(..))"/>
24         <aop:pointcut id="pointcutWithParam"
25                       expression="execution(* *.concret.Performance.perform(..)) and args(index,name)"/>
26         <aop:aspect ref="audience">
27             <!--  <aop:before method="silenceCellPhone" pointcut-ref="pointcut"/>
28              <aop:before method="takeSeats" pointcut-ref="pointcut"/>
29              <aop:after-returning method="applause" pointcut-ref="pointcut"/>
30              <aop:after-throwing method="demandRefund" pointcut-ref="pointcut"/>-->
31             <aop:around method="watchPerform" pointcut-ref="pointcut"/>
32         </aop:aspect>
33         <aop:aspect ref="host">
34             <!--注意在XML中不应该使用&& 而应该使用关键字and-->
35             <aop:before method="host" pointcut-ref="pointcutWithParam" arg-names="index,name"/>
36 
37         </aop:aspect>
38         <aop:aspect ref="forMore">
39             <aop:declare-parents types-matching="面向切面编程.concert.Performance+"
40                                  implement-interface="面向切面编程.concert.MoreForPerformance"
41                                  default-impl="面向切面编程.concert.MoreForPerformanceImpl"/>
42         </aop:aspect>
43     </aop:config>
44 
45 </beans>

--顾名思义,<aop:declare-parents>声明了此切面所通知的bean 要在它的对象层次结构中拥有新的父类型。具体到本例中,类型匹 配Performance接口(由types-matching属性指定)的那些bean 在父类结构中会增加MoreForPerformance接口(由implement- interface属性指定)。最后要解决的问题是MoreForPerformance接口中 的方法实现要来自于何处。
--我们可以进行测试,查看代理是否成功

 1 package 面向切面编程.concert;
 2 
 3 import org.junit.Test;
 4 import org.junit.runner.RunWith;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.context.ApplicationContext;
 7 import org.springframework.test.context.ContextConfiguration;
 8 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 9 
10 import java.util.List;
11 
12 /**
13  * @author : S K Y
14  * @version :0.0.1
15  */
16 @RunWith(SpringJUnit4ClassRunner.class)
17 @ContextConfiguration(locations = {"classpath:面向切面编程/concert/audienct-config.xml"})
18 public class XMLConfigTest {
19     @Autowired
20     private Performance performance;
21     @Autowired
22     private Host host;
23     @Autowired
24     private PointcutForMore more;
25 
26     @Autowired
27     private ApplicationContext context;
28     @Autowired
29     private MoreForPerformance moreForPerformance;
30 
31     @Test
32     public void testMoreForPerformance() {
33         MoreForPerformance performanceMore = context.getBean("performance", MoreForPerformance.class);
34         performanceMore.moreForPerformance();
35         Performance performance = context.getBean("performance", Performance.class);
36         performance.perform();
37     }
38 
39 }

--运行结果

[附加]舞蹈表演特别的棒,每一个舞者脸上都流露着灿烂的笑容...
表演即将开始,观众小明将他的手机设置为了静音状态;
表演还有5分钟就要开始了,观众小明入座等待;
演员们表演精彩的舞蹈...
表演结束,观众小明为演员们热烈的鼓掌;

Process finished with exit code 0

--我们可以发现,成功的为performance bean添加了新的方法完成了我们的实现
--虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比, Spring AOP 是一个功能比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。当我们需要在创建对象时应用通知,构造器切点就非常方便。 不像某些其他面向对象语言中的构造器,Java构造器不同于其他的正 常方法。这使得Spring基于代理的AOP无法把通知应用于对象的创建 过程。
附:AspectJ语法:

execution()表达内部写方法名,方法名加全类名,内部参数也需要表名全类名(两个方法用execution() or execution()连接):
  public boolean addStudent(com.sky.model.Student):所有返回类型为Boolean并且返回参数为此的方法
  public boolean com.sky.model.StudentService.addStudent(com.sky.model.Student): 只有该类下的addStudent方法
  public * addStudent(com.sky.model.Student):返回值任意
  public void * (com.sky.model.Student):方法名任意
  public void addStudent(..) :任意参数列表
  *com.sky.service.*.*(..):com.sky.service.StudentServic中的所有方法,不包含子包中的
  *com.sky.service..*.*(..):com.sky.service.StudentServic中的所有方法,包含子包中的

posted @ 2019-09-08 21:14  灰色天空_graySky  阅读(355)  评论(0编辑  收藏  举报