1.什么是面向切面编程

  在软件开发中,散布于应用中多处的功能被称为横切关注点,这些横切关注点从概念上是与应用的业务逻辑相分离的,但往往分直接嵌入到应用的业务逻辑之中,把这些横切关注点与业务逻辑相分离正式面向切面(AOP)要解决的问题。DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响的对象之间的解耦。

  例如,安全是一个横切关注点,应用中许多方法都会涉及到安全规则,如果要重用通用功能的话,常见的面向对象技术是继承或委托,但是继承往往会导致一个脆弱的对象体系,而使用委托可能需要对委托对象进行复杂的调用。切面提供了一种替代方案,在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处,首先每个关注点都集中于一个地方,而不是分散到多处代码中,其次,应用只包含它们的业务逻辑代码,安全、事务管理等被转移到切面中了。

  定义AOP术语

  AOP常用的术语有通知(advice)、切点(pointcut)和连接点(joinpoint)。切面的工作被称为通知,通知描述了切面要完成的工作和何时执行这个工作,Spring切面可以应用5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

  应用通知的时机被称为连接点,连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。

  切点定义了“何处”应用通知,切点的定义会匹配通知要织入的一个点或多个连接点,我们通常使用明确的类和方法名称或利用正则表达式定义所匹配的类和方法名称指定这些切点,有些AOP框架允许创建动态的切点,可以根据运行时的决策决定是否应用通知。

  切面是通知和切点的结合,通知和切点共同定义了切面的全部内容,它是什么,在何时和何处完成其功能。

  引入(Introduction)允许我们向现有的类添加新方法或属性,我们可以创建一个通知类,该类通过一个实例变量和Setter方法记录了对象最后一次修改时的状态。然后这个新方法和实例变量就可以被引入到现有的类中,从而在无需修改现有的类的情况下,使它们具有新的行为和状态。

  织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期有多个点可以进行织入:编译期、类加载期、运行期,Spring AOP是在应用运行时织入的。

  Spring对AOP的支持

  Spring对AOP的支持在很多方面借鉴了AspectJ项目,Spring提供了4种类型的AOP支持:

  • 基于代理的经典Spring AOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面

  经典的Spring AOP模型显得笨重和复杂,不过多讨论;纯POJO借助Spring的aop命名空间可以转化为切面,但是需要XML配置;注解驱动的好处在于可以不适用XML配置;第4种类型的AOP可以将值注入到AspectJ驱动的切面。

  Spring通知是Java编写的,切点的定义使用注解或XML。(AspectJ最初是使用特有的AOP语言(现在支持基于注解的切面),这意味着需要额外学习新的工具和语法,而Spring AOP无需新的语法)。

  Spring在运行时通知对象

  Spring只支持方法级别的连接点

2.通过切点选择连接点

  Spring AOP要使用AspectJ的切点表达式语言定义切点,因为Spring是基于代理的,所以它仅支持AspectJ切点指示器的一个子集,其中只有execution是实际执行匹配的,其它的指示器都是限制匹配的。

execution(** concert.Performance.perform(..))

//* = 返回任意类型
//concert.Performace = 全限定类名
//perform = 方法名
//(..) = 使用任意参数
execution(** concert.Performance.perform(..) and within(concert.*))

//使用within()限制匹配,仅匹配concert包
execution(** concert.Performance.perform(..) and bean('woodstock'))

//限定bean的ID

3.使用注解创建切面

  定义切面

  Spring使用@AspectJ注解标注该类不仅仅是一个POJO,还是一个切面。类中的方法都使用注解定义切面的具体行为。

@Aspect
public class Audience {
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("Silencing cell phones");
}
@Before("execution(** concert.Performance.perform(..))")
public void takeSeats(){
System.out.println("Taking Seats");
}
@AfterReturning("execution(** concert.Performance.perform(..))")
public void applause(){
System.out.println("Applause");
}
@AfterThrowing("execution(** concert.Performance.perform(..))")
public void demandRefund(){
System.out.println("Demand Refund");
}
}

  这四个方法的切点表达式都是相同的,我们可以使用@Pointcut注解在切面内定义可重用的切点。

@Aspect
public class Audience {
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance(){
}
@Before("performance()")
public void silenceCellPhones(){
System.out.println("Silencing cell phones");
}
@Before("performance()")
public void takeSeats(){
System.out.println("Taking Seats");
}
@AfterReturning("performance()")
public void applause(){
System.out.println("Applause");
}
@AfterThrowing("performance()")
public void demandRefund(){
System.out.println("Demand Refund");
}
}

  如果只将Audience类装配为Spring中的bean,即便使用了@AspectJ注解,它也不会被视为切面。这些注解不会解析,也不会创建将其转换为切面的代理。需要启用自动代理功能。

  使用JavaConfig启动自动代理功能

@Configuration
@EnableAspectJAutoProxy //启用AspectJ自动代理
public class ConcertConfig {
@Bean
public Audience audience(){ //声明 Audience Bean
return new Audience();
}
}

  使用XML启动自动代理功能

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <aop:aspectj-autoproxy /> <!-- 启用Aspectj自动代理 -->
<bean class="concert.Audience"/> <!-- 声明Audience bean -->
</beans>

  AspectJ自动代理会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。这样的话会为Concert bean创建一个代理,Audience类中的通知方法将会在perform方法调用前后执行。

  创建环绕通知

  环绕通知是更为强大的通知类型,它可以让切面的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。

@Aspect
public class Audience {
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance(){
}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp){
try {
System.out.println("Silencing cell phones");
System.out.println("Taking Seats");
jp.proceed();
System.out.println("Applause");
} catch (Throwable e) {
System.out.println("Demand Refund");
}
}
}

  通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,需要调用ProceedingJoinPoint的proceed()方法。有时你可以不调用proceed()方法,从而阻塞对被通知方法的访问。类似的,也可以在通知中对其多次调用,实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

  处理通知中的参数

  如果被通知的方法中有参数,切面能够访问和使用传递给被通知方法的参数。

@Aspect
public class TrackCounter {
private HashMap<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;
}
}

  args(trackNumber)限定符表明传递给playTrack()方法的int类型参数也会传递到通知中去,切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。

  通过注解引入新功能

  如果需要为已知的API添加新功能,由于是已知的API,我们不能修改其类,只能通过外部包装,最简单的办法就是实现某个我们自定义的接口,这个接口包含了想要添加的方法。但是Java不是一门动态的语言,无法在编译后动态添加新的功能,这时候可以用aop的declare-parents来做。我们为所有的Performance实现引入Endoreable接口。

@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable; //value属性指定了哪种类型的bean要引入该接口,在本例中也就是所有实现Performance的类型,加号表示是其子类型
//defaultImpl属性指定了为引入功能提供实现的类
//@DeclareParents注解所标注的静态属性指明了要引入的接口
}

  和其它的切面一样,我们需要在Spring应用中将EncoreableIntorducer声明为一个bean。在使用时,直接通过getBean获得bean再转换成相应的接口就可以使用了。

@Test
public void intro(){
ApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class);
Encoreable concert = (Encoreable) context.getBean("concert");
concert.performEncore();
}

4.在XML中声明切面

  面向注解的切面声明必须能够为通知类添加注解,所以必须要有源码。如果没有源码或者不想将AspectJ注解放到代码中,Spring提供了在XML配置文件中声明切面。

  声明前置和后置通知

<aop:config>
<aop:aspect ref="audience">
<aop:before
pointcut="execution(** concert.Performance.perform(..))" method="silence"/>
<aop:before
pointcut="execution(** concert.Performance.perform(..))" method="takeSeats"/>
<aop:after-returning
pointcut="execution(** concert.Performance.perform(..))" method="applause"/>
<aop:after-throwing
pointcut="execution(** concert.Performance.perform(..))" method="refund"/>
</aop:aspect>
</aop:config>

  当切点重复时,需要使用<aop:pointcut>元素。

<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance" expression="execution(** concert.Performance.perform(..))"/>
<aop:before
pointcut-ref="performance" method="silence"/>
<aop:before
pointcut-ref="performance" method="takeSeats"/>
<aop:after-returning
pointcut-ref="performance" method="applause"/>
<aop:after-throwing
pointcut-ref="performance" method="refund"/>
</aop:aspect>
</aop:config>

  声明环绕通知

  环绕通知可以在一个方法中实现前置通知和后置通知,在前置通知和后置通知之间只需要局部变量共享信息而不需使用成员变量,避免了线程安全问题。我们首先重写Audience类。

@Aspect
public class Audience {
public void watchPerformance(ProceedingJoinPoint jp){
try {
System.out.println("Silence");
System.out.println("Seats");
jp.proceed();
System.out.println("Applause");
} catch (Throwable e) {
System.out.println("Refund");
}
}
}

   声明环绕通知使用<aop:around>元素。

<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance" expression="execution(** concert.Performance.perform(..))"/>
<aop:around
pointcut-ref="performance" method="watchPerformance"/>
</aop:aspect>
</aop:config>

  为通知传递参数

<aop:config>
<aop:aspect ref="trackCounter">
<aop:pointcut
id="trackPlayed" expression="execution(* soundsystem.CompactDsic.playTrack(int)) and args(trackNumber)"/>
<aop:before
pointcut-ref="trackPlayed" method="countTrack"/>
</aop:aspect>
</aop:config>

  通过切面引入新的功能

<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"
/>
</aop:aspect>

SpringInAction读书笔记--第4章面向切面的更多相关文章

  1. SpringInAction读书笔记--第1章Spring之旅

    1.简化Java开发 Spring是一个开源框架,它的根本使命在于简化java开发.为了降低java开发的复杂性,Spring采取了以下4种关键策略: 基于POJO的轻量级和最小侵入性编程      ...

  2. SpringInAction读书笔记--第2章装配Bean

    实现一个业务需要多个组件相互协作,创建组件之间关联关系的传统方法通常会导致结构复杂的代码,这些代码很难被复用和单元测试.在Spring中,对象不需要自己寻找或创建与其所关联的其它对象,Spring容器 ...

  3. 《Linux内核设计与实现》第八周读书笔记——第四章 进程调度

    <Linux内核设计与实现>第八周读书笔记——第四章 进程调度 第4章 进程调度35 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配 ...

  4. 《Linux内核设计与分析》第六周读书笔记——第三章

    <Linux内核设计与实现>第六周读书笔记——第三章 20135301张忻估算学习时间:共2.5小时读书:2.0代码:0作业:0博客:0.5实际学习时间:共3.0小时读书:2.0代码:0作 ...

  5. 《Linux内核设计与实现》 第八周读书笔记 第四章 进程调度

    20135307 张嘉琪 第八周读书笔记 第四章 进程调度 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统.只有 ...

  6. 《Linux内核分析》读书笔记(四章)

    <Linux内核分析>读书笔记(四章) 标签(空格分隔): 20135328陈都 第四章 进程调度 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行 ...

  7. 《LINUX内核设计与实现》第三周读书笔记——第一二章

    <Linux内核设计与实现>读书笔记--第一二章 20135301张忻 估算学习时间:共2小时 读书:1.5 代码:0 作业:0 博客:0.5 实际学习时间:共2.5小时 读书:2.0 代 ...

  8. 《Linux内核设计与实现》第四周读书笔记——第五章

    <Linux内核设计与实现>第四周读书笔记--第五章 20135301张忻 估算学习时间:共1.5小时 读书:1.0 代码:0 作业:0 博客:0.5 实际学习时间:共2.0小时 读书:1 ...

  9. 《Linux内核设计与实现》第五周读书笔记——第十一章

    <Linux内核设计与实现>第五周读书笔记——第十一章 20135301张忻 估算学习时间:共2.5小时 读书:2.0 代码:0 作业:0 博客:0.5 实际学习时间:共3.0小时 读书: ...

随机推荐

  1. C#基础系列(一)

    1.vs中F5(调试)和Ctrl + F5(直接运行不调试)的区别: Ctrl+F5是直接运行生成的程序,不进行重新编绎,所以运行起来比较快F5是重新编绎后再运行,这样可以在程序代码中设置断点跟踪来调 ...

  2. Oracle- 日期加减

    加法 select sysdate,add_months(sysdate,12) from dual;        --加1年 select sysdate,add_months(sysdate,1 ...

  3. ASP.Net 使用SqlBulkCopy批量插入

    批量插入,以前我的做法是生成一堆insert into的sql语句,然后用程序一次值行,来实现. 今天看到了ASP.Net里可以使用DataTable,先将数据写入到DataTable中,然后使用Sq ...

  4. 实现Linux下的ls -l命令

    基本实现了Linux下的ls -l命令,对于不同的文件显示不同的颜色和显示符号链接暂时没有实现: /************************************************** ...

  5. 教你50招提升ASP.NET性能(三):使用Microsoft的PDBs调试和分析外部的程序集和库

    (3)Use Microsoft’s PDBs to debug or profile external assemblies or libraries 招数3: 使用Microsoft的PDBs调试 ...

  6. 金蝶K3 破解版

  7. [React Fundamentals] Component Lifecycle - Mounting Basics

    React components have a lifecycle, and you are able to access specific phases of that lifecycle. Thi ...

  8. 学习笔记之DB2 9 Fundamentals 730

    Sequence中cache的影响,每新建一个连接,next value值增加increment * cache.如果加上order,则会按顺序生成值. increment cache ; Conne ...

  9. Java自学成长路线(转载)

    JAVA自学之路 一:学会选择  决心做软件的,大多数人选的是java,或是.net,也有一些选择了手机.嵌入式.游戏.3G.测试等.  JAVA是一种平台,也是一种程序设计语言,如何学好程序设计不仅 ...

  10. #284 div.2 C.Crazy Town

    C. Crazy Town   Crazy Town is a plane on which there are n infinite line roads. Each road is defined ...