【Spring学习笔记】9:使用Spring AOP的面向切面

这两天读了《Spring实战》第四章,总结一下。

其中章节4.3.4的”通过注解引入新功能”,以及章节4.5的”注入AspectJ切面”这本书上讲的不是很清楚,而且也有些复杂,也并不常用,有机会单独拿出来学习一下。

面向切面编程

在程序中,如日志、安全、缓存、事务管理等不是业务本身该做的,但是总是和很多业务逻辑一起出现,这些事件对业务逻辑来说是被动的。在业务程序中被动地做这些事的位置称为横切关注点,AOP所做的事情就是将这些横切关注点和业务逻辑分离。

重用这些通用功能的常见做法是继承委托,AOP也是提供了另外的思路,通过声明的方式定义通用功能在业务逻辑的什么位置使用,当横切关注点被模块化成特殊的类时,该类称为切面

想象一下空间中的多个点可以用曲面来拟合,多个横切关注点可以被模块化成切面。切面也就是整合这些通用功能的服务模块。

这里写图片描述

AOP术语

这些东西太抽象了,大致理解一下,知道怎么用就好了。

通知(Advice)

切面的工作被称为通知,通知定义了切面要完成的具体事情和何时使用。

Spring的切面有5种类型的通知:

  • 前置通知(Before):在目标方法被调用前调用
  • 后置通知(After):在目标方法完成之后调用,不关心方法输出了什么
  • 返回通知(After-returning):在目标方法成功执行之后调用
  • 异常通知(After-throwing):在目标方法抛出异常之后调用
  • 环绕通知(Around):包裹被通知的方法,在调用的前后做事情
连接点(Join point)

连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、修改一个字段时等等。切面中的通知代码通过这些点插入到业务的流程中,从而添加新的行为。

切点(Poincut)

一个切面不需要通知应用的所有连接点,切点的定义会匹配通知所要织入的一个或多个连接点,有助于缩小切面所通知连接点的范围。

切面(Aspect)

切面是通知和切点的结合——它是什么,在何时和何处完成其功能。

引入(Introduction)

引入允许向现有的类添加新的方法或属性,可以在无需修改现有类的情况下让它们具有新的行为和状态。

织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。织入可以在编译期、类加载期、运行期。目前讨论的都是运行期的织入,Spring AOP会为目标对象动态地创建一个代理对象

Spring对AOP的支持

Spring提供了4种类型的AOP支持:

  • 基于代理的Spring AOP(太老了不看)
  • 纯POJO切面
  • @Aspect注解驱动的切面
  • 注入式AspectJ切面
Spring中

Spring的@Aspect注解借鉴了AspectJ,但本质上仍是基于代理的AOP。总之在Spring本身的AOP中使用注解或XML来实现AOP。

Spring在运行期间把切面织入到Spring管理的bean中,代理类封装了目标类,并拦截被通知方法的调用,在切面逻辑的包围下,在适当的时机把调用转发给真正的目标bean。

因为Spring基于动态代理,所以只支持方法级别的连接点,但已经可以满足绝大多数功能,需要更强大的功能时可以选择使用AspectJ。

AspectJ中

在AspectJ中,可以通过注解或特有的扩展语言获得更强大的细粒度的控制,以及更丰富的AOP工具集。

通过切点选择连接点

Spring借助AspectJ的切点表达式语言定义Spring切面,但它只支持AspectJ切点指示器的一部分功能。可用的有:

AspectJ指示器描述
arg()限制连接点匹配参数为指定类型的执行方法
@arg()限制连接点匹配参数由指定注解标注的执行方法
execution()用于匹配是连接点的执行方法
this()限制连接点匹配AOP代理的bean引用为指定类型的类
target()限制连接点匹配目标对象为指定类型的类
@target()限制连接点匹配特定的执行对象,这些对象对应的类要有指定类型的注解
within()限制连接点匹配指定的类型
@within()限制连接点匹配指定注解标注的类型(当使用Spring AOP时,方法定义在由Spring注解所标注的类里)
@annotation()限制匹配带有指定注解的连接点

只有execution()是执行匹配的,其它的指示器都是用来限制匹配的。

编写切点表达式

业务逻辑的抽象接口,就按照书上的例子,提供一个可以做”表演”的接口:

//表演接口
public interface Performance {
    //表演方法
    void perform() throws Exception;
}



要让这个接口的实现类所实现的perform()方法成为切点,要使用切点表达式语言:

"execution(* org.aop.Performance.perform(..))"

其中execution()表示在方法执行时触发,里面紧跟的一个*表示返回任意类型,后面是方法所属的类(这里是接口),然后是方法的名称,方法中..表示选择任意重载的该方法,而不关心其参数表。



该接口可能有多个实现类,如果要加个条件,限制只选择某个指定包下的所有该接口的实现类,则要这样写:

"execution(* org.aop.Performance.perform(..)) && within(org.aop.imp.*)"

使用&&让它和within()指示器之间形成合取关系,当在XML中使用时,因为&符有特殊含义,可以使用相应的英文来代替:

关系符号在XML中使用英文代替
&&and
||or
!not



bean()指示器则可以明确指明该接口的实现bean的id:

"execution(* org.aop.Performance.perform(..)) && bean(pfmcimp)"

这个切点表达式在XML中则要写成(书上加单引号是错的)

"execution(* org.aop.Performance.perform(..)) and bean(pfmcimp)"



使用非操作排除指定id的bean,为其他实现bean提供应用通知:

"execution(* org.aop.Performance.perform(..)) && !bean(pfmcimp)"

使用注解配置切面

四种简单的通知

@Before@AfterReturning@AfterThrowing@After是四种最常用的最简单的通知。

切面类
package org.aop;

import org.aspectj.lang.annotation.*;

@Aspect//标注该类为切面类
public class Audience {

    //定义一个可重用的切点表达式,可供各个通知方法引用
    @Pointcut("execution(* org.aop.Performance.perform(..)) && bean(pfmcimp)")
    public void perform() {
        //该方法不需要内容,只是作为一个标识,供@Pointcut注解依附
    }

    //在目标方法执行前
    @Before("perform()")//这里不再直接写切点表达式,而是引用切点方法
    public void silenceCellPhones() {
        System.out.println("[Before]表演前,把手机静音");
    }

    //在目标方法正常返回后
    @AfterReturning("perform()")
    public void applause() {
        System.out.println("[AfterReturning]表演正常结束(return返回)后,鼓掌");
    }

    //在目标方法抛出异常后
    @AfterThrowing("perform()")
    public void demandRefund() {
        System.out.println("[AfterThrowing]表演没有顺利完成(抛出异常),要求退款");
    }

    //在目标方法执行后(不论是正常返回还是抛出异常)
    @After("perform()")
    public void evaluate() {
        System.out.println("[After]表演结束了,观众在心里对表演进行评估");
    }

}
配置类
package org.sb;

import org.aop.Audience;
import org.aop.Performance;
import org.aop.imp.PerformanceImp;
import org.springframework.context.annotation.*;

@Configuration//标注该类为JavaConfig配置类
@EnableAspectJAutoProxy//启动自动代理功能
@ImportResource(value = {"classpath:ApplicationContext.xml"})//引入其它配置类或配置文件
@ComponentScan(basePackageClasses = {Audience.class})//自动扫描指定类或接口所在的基本包
public class SbConfig {

    @Bean
    public Audience audience() {
        //观众的行为类(切面配置类)
        return new Audience();
    }

    @Bean
    public Performance pfmcimp() {
        //Performance(表演接口)的一个实现类对象,将被代理
        return new PerformanceImp();
    }

}

特别注意在JavaConfig中使用@EnableAspectJAutoProxy注解开启自动代理功能,如果是在XML中,应使用:

<aop:aspectj-autoproxy/>

不论哪种配置方式,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。

实现bean

在实现bean所实现的目标方法中故意抛出一个异常:

package org.aop.imp;

import org.aop.Performance;
import org.springframework.stereotype.Component;

@Component//标注该类是组件类,扫描后将成bean交给Spring容器管理
public class PerformanceImp implements Performance {
    @Override
    public void perform() throws Exception {
        System.out.println("LZH表演了自己是怎么吃饭的");
        throw new Exception("666");//故意抛出一个异常看一下
    }
}
场景类

使用者只需调用目标方法,而无需关心切面上的通知:

package org.aop;

import org.sb.SbConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        //从JavaConfig配置类中加载Spring的ApplicationContext上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(SbConfig.class);
        //获得目标方法所在接口的一个实现bean
        Performance performance = (Performance) context.getBean("pfmcimp");
        try {
            //调用其中的实现方法,该方法已经被定义成了[切点],并被加上了[切面]
            //代理类封装了目标bean,拦截方法调用,执行切面逻辑
            performance.perform();
        } catch (Exception e) {
            //执行过程中可能抛出的异常
            System.err.println("处理目标方法中抛出的异常");//System.err输出不带缓存,有时会跑到System.out的前面
        }
    }
}
输出

这里写图片描述
如果不用System.err而还是用System.out的话,输出顺序就能保证了。

环绕通知

环绕通知是更强大的一种通知类型,顾名思义,就是将目标方法包裹起来调用。环绕通知中ProceedingJoinPoint类型的参数用来执行目标方法,环绕通知必须有返回值,返回值即为目标方法的返回值

切面类

修改一下切面类,只需要一个环绕通知就可以完成前面四种通知的功能。

package org.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect//标注该类为切面类
public class Audience {

    //定义一个可重用的切点表达式,可供各个通知方法引用
    @Pointcut("execution(* org.aop.Performance.perform(..)) && bean(pfmcimp)")
    public void perform() {
        //该方法不需要内容,只是作为一个标识,供@Pointcut注解依附
    }

    //环绕在目标方法上
    @Around("perform()")
    public Object watchPerformance(ProceedingJoinPoint joinPoint) {
        Object result = null;
        try {
            System.out.println("[Before]表演前,把手机静音");//@Before
            result = joinPoint.proceed();//调用目标方法
            System.out.println("[AfterReturning]表演正常结束(return返回)后,鼓掌");//@AfterReturning
        } catch (Throwable throwable) {
            System.out.println("[AfterThrowing]表演没有顺利完成(抛出异常),要求退款");//@AfterThrowing
        }
        System.out.println("[After]表演结束了,观众在心里对表演进行评估");//@After
        return result;//返回目标方法的执行结果
    }

}
输出

这里写图片描述
注意和直接使用四种通知时的区别,After通知的位置不太一样。另外这种方式在内部处理了异常,在异常处理内执行操作,而AfterThrowing实际是在抛出异常时执行的操作,所以前面那种是能在场景类中捕获异常的,这种方式则因为处理过了不能再捕获。

在通知中访问目标方法的参数

如果目标方法带有参数,如在前面例子的接口和实现类方法中加入一个String类型的name参数用来指明是谁在表演,在场景类调用时传入”刘知昊”。

切面类
package org.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect//标注该类为切面类
public class Audience {

    //指明要String类型的参数,在这里给参数一个别名performer
    @Pointcut("execution(* org.aop.Performance.perform(String)) && bean(pfmcimp) && args(performer)")
    //方法中传入String类型的这个参数
    public void perform(String performer) {
    }

    //环绕在目标方法上,写明参数
    @Around("perform(performer)")
    //目标方法上的参数如果需要通知使用,则从通知方法传入即可使用
    public Object watchPerformance(ProceedingJoinPoint joinPoint, String performer) {
        Object result = null;
        try {
            System.out.println("[Before]"+performer+"表演前,把手机静音");
            //在用此方法调用目标方法时,并不需要传入参数,参数仅仅是给通知用的
            result = joinPoint.proceed();
            System.out.println("[AfterReturning]表演正常结束(return返回)后,鼓掌");
        } catch (Throwable throwable) {
            System.out.println("[AfterThrowing]"+performer+"表演没有顺利完成(抛出异常),要求退款");
        }
        System.out.println("[After]表演结束了,观众在心里对表演进行评估");
        return result;
    }

}

特别注意,参数传进来仅仅考虑到通知可能会用到,目标方法本身是用不着重新传一遍的,这里千万不要搞混了。这体现在环绕通知里的joinPoint.proceed()不需要传这个参数。

输出

这里写图片描述

使用XML配置切面

为什么要使用XML配置

注解和自动代理提供了最简的配置AOP的方式,但它有个明显的劣势:必须有办法为通知类添加注解。也就是必须要有源码,如果不能获得源码的话就不能用这种方式了(因为没法去添加注解)。使用XML创建切面则避免了这个问题。

注意,使用XML配置时就相当于拿不到切面类的源码,切面类的所有与Spring AOP相关的注解全部去掉。

AOP配置元素

使用Spring的AOP配置元素能彻底以非侵入性的方式声明切面:

AOP配置元素功能
<aop:config>顶层的AOP配置元素,大多元素都需包含在这个元素内
<aop:advisor>定义AOP通知器
<aop:aspect>定义一个切面,通知需要定义在切面中
<aop:pointcut>定义一个切点,指明切点表达式
<aop:aspectj-autoproxy/>自动代理,启用@Aspect注解驱动的切面

在切面内:

切面内的元素功能
<aop:after>后置通知
<aop:after-returning>返回通知
<aop:after-throwing>异常通知
<aop:around>环绕通知
<aop:before>前置通知
<aop:declare-parents>以透明的方式为被通知的对象引入额外的接口
四种简单的通知
    <!--切面bean-->
    <bean class="org.aop.Audience" id="audience"/>

    <!--配置AOP-->
    <aop:config>
        <!--这个定义[切点]的元素完全可以放在切面外面,这样所有切面都能用它-->
        <aop:pointcut id="perform" expression="execution(* org.aop.Performance.perform(String)) and bean(pfmcimp) and args(performer)"/>
        <!--定义[切面],引用切面bean-->
        <aop:aspect ref="audience">
            <!--定义[通知],引用前面定义的[切点],通知参数用arg-names属性指定-->
            <aop:before method="silenceCellPhones" pointcut-ref="perform" arg-names="performer"/>
            <aop:after-returning method="applause" pointcut-ref="perform" arg-names="performer"/>
            <aop:after-throwing method="demandRefund" pointcut-ref="perform" arg-names="performer"/>
            <aop:after method="evaluate" pointcut-ref="perform" arg-names="performer"/>
        </aop:aspect>
    </aop:config>

输出:
这里写图片描述

环绕通知

只要把里面的前置、后置通知都换成环绕通知,别的不用改。

    <!--切面bean-->
    <bean class="org.aop.Audience" id="audience"/>

    <!--配置AOP-->
    <aop:config>
        <!--这个定义[切点]的元素完全可以放在切面外面,这样所有切面都能用它-->
        <aop:pointcut id="perform" expression="execution(* org.aop.Performance.perform(String)) and bean(pfmcimp) and args(performer)"/>
        <!--定义[切面],引用切面bean-->
        <aop:aspect ref="audience">
            <!--定义[通知],引用前面定义的[切点],通知参数用arg-names属性指定-->
            <aop:around method="watchPerformance" pointcut-ref="perform" arg-names="joinPoint,performer"/>
        </aop:aspect>
    </aop:config>

输出:
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值