Spring 面向切面编程详解

一、面向切面的基本原理

在软件开发中,散布于应用中多处的功能被称为横切关注点。通常来说,这些个横切关注点从概念上是与应用的业务逻辑相分离的,但往往会直接嵌入到应用的业务逻辑之中。因此,面向切面编程(AOP)核心在于把这些横切关注点与业务逻辑相分离。日志是应用切面的常见范例,但是切面所适用的场景很多,包括声明式事务、安全和缓存。

1. AOP术语

  1. 通知(Advice)

    在AOP中,切面的工作被称为通知。通知定义了切面是什么以及何时使用。Spring切面可以应用5种类型的通知:

    • 前置通知 Before —— 在方法被调用之前调用通知。
    • 后置通知 After —— 在方法完成之后调用通知,无论方法是否执行成功。
    • 返回通知 After-returning —— 在方法成功执行之后调用通知。
    • 异常通知 After-throwing —— 在方法抛出异常以后调用通知。
    • 环绕通知 Around —— 通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
  2. 连接点(Joinpoint)

    连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时,抛出异常时,甚至是修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

  3. 切点(Pointcut)

    切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称来指定这些切点,或是利用正则表达式定义匹配的类和方法名称模式来指定这些切点。

  4. 切面(Aspect)

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

  5. 引入(Introduction)

    引入允许我们向现有的类添加新的方法和属性。

  6. 织入(Weaving)

    织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

    • 编译器 —— 切面在目标类编译时被织入。
    • 类加载期 —— 切面在目标类加载到JVM时被织入。
    • 运行期 —— 切面在应用运行的某个时候被织入。Spring AOP就是以这种方式织入切面的。

二、在Spring XML中声明切面

1. AspectJ切点指示器

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

2. 步骤

1)基本数据
public interface Performance {
    public void perform();
}

@Component
public class Audience {
    // 表演之前
    public void takeSeats(){
        System.out.println("观众入座");
    }

    // 表演之前
    public void turnOffCellPhones(){
        System.out.println("关闭手机");
    }

    // 表演之后
    public void applaud(){
        System.out.println("鼓掌: 啪啪啪啪啪");
    }

    // 表演失败后
    public void failure(){
        System.out.println("坑爹,退钱!");
    }
}
2)编写切点
execution(* com.shiftycat.concert.Performance.perform(..))
// 或者使用bean,在执行Instrument的`play()`方法时应用通知,并且Bean的ID为eddie。
execution(* com.shiftycat.concert.Performance.perform()) and bean(rocketStar)
3)XML中配置AOP

在Spring XML中配置AOP使用元素,下表概述了AOP配置元素。

AOP配置元素描述
aop:advisor定义AOP通知器
aop:after定义AOP后置通知(不管被通知的方法是否执行成功)
aop:after-returning定义AOP after-returning通知
aop:after-throwing定义AOP after-throwing通知
aop:around定义AOP环绕通知
aop:aspect定义切面
aop:aspectj-autoproxy启用@AspectJ注解驱动切面
aop:before定义AOP前置通知
aop:config顶层的AOP配置元素
aop:declare-parents为被通知的对象引入额外的接口,并透明的实现
aop:pointcut定义切点

为了演示Spring AOP,现在定义一个观众类 Audience:

public class Audience {
    // 表演之前
    public void takeSeats(){
        System.out.println("观众入座");
    }

    // 表演之前
    public void turnOffCellPhones(){
        System.out.println("关闭手机");
    }

    // 表演之后
    public void applaud(){
        System.out.println("鼓掌: 啪啪啪啪啪");
    }

    // 表演失败后
    public void failure(){
        System.out.println("坑爹,退钱!");
    }
}

在Spring XML中配置该Bean:

<bean id="rocketStar" class="com.shiftycat.concert.RocketStar" />
<bean id="audience" class="com.shiftycat.concert.Audience" />
4)声明相关通知
<aop:config>
    <aop:aspect ref="audience">
        <aop:pointcut
                id="performance"
                expression="execution(* com.shiftycat.concert.Performance.perform(..)) and bean(rocketStar)"/>
        <aop:before
                pointcut-ref="performance"
                method="takeSeats"/>
        <aop:before
                pointcut-ref="performance"
                method="turnOffCellPhones"/>
        <aop:after-returning
                pointcut-ref="performance"
                method="applaud"/>
        <aop:after-throwing
                pointcut-ref="performance"
                method="failure"/>
    </aop:aspect>
</aop:config>
5)进行测试
@Test
public void performXMLTest() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");
    Performance performance = context.getBean(Performance.class);
    performance.perform();
}
6)测试结果
观众入座
关闭手机
梁博演唱《出现又离开》
鼓掌: 啪啪啪啪啪

3. 声明环绕通知

如果不使用成员变量,那么在前置通知和后置通知之间共享信息是非常麻烦的。可以使用环绕通知代替前置通知和后置通知,现在在Audience类里添加一个新的方法:

public void watchPerformance(ProceedingJoinPoint joinPoint) {
    try {
        System.out.println("观众入座了");
        System.out.println("关闭手机了");
        joinPoint.proceed();
        System.out.println("鼓掌了: 啪啪啪啪啪");
        joinPoint.proceed();
        System.out.println("鼓掌了: 啪啪啪啪啪");
    } catch (Throwable e) {
        System.out.println("坑爹,退钱!");
    }
}

对于新的方法,我们使用了ProceedingJoinPoint作为参数,这个对象可以在通知里调用被通知的方法!!我们要把控制转给被通知的方法时,必须调用ProceedingJoinPoint的proceed()方法。

修改<aop:config>元素:

<aop:config>
    <aop:aspect ref="audience">
        <aop:pointcut
                id="performance"
                expression="execution(* com.shiftycat.concert.Performance.perform(..)) and bean(rocketStar)"/>
        <aop:around pointcut-ref="performance"
                    method="watchPerformance"/>
    </aop:aspect>
</aop:config>
观众入座了
关闭手机了
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪

4. 为通知传递参数

1)基本数据

定义一个新的参赛者,他是一个读心者,由MindReader接口所定义:

public interface MindReader {
    void interceptThoughts(String thoughts);
    String getThoughts();
}

魔术师Magician实现该接口:

public class Magician implements MindReader{
    private String thoughts;
    public void interceptThoughts(String thoughts) {
        System.out.println("侦听志愿者的心声");
        this.thoughts=thoughts;
    }
    public String getThoughts() {
        return thoughts;
    }
}

再定义一个Magician所要侦听的志愿者,首先定义一个思考者接口:

public interface Thinker {
    void thinkOfSomething(String thoughts);
}

志愿者Volunteer实现该接口:

public class Volunteer implements Thinker{
    private String thoughts;
    public void thinkOfSomething(String thoughts) {
        this.thoughts=thoughts;
    }
    public String getThoughts(){
        return thoughts;
    }
}
2)XML文件配置AOP

接下来使用Spring AOP传递Volunteer的thoughts参数,以此实现Magician的侦听:

<bean id="magician" class="com.shiftycat.concert.Magician"/>
<bean id="volunteer" class="com.shiftycat.concert.Volunteer"/>

<aop:config proxy-target-class="true">
        <aop:aspect ref="magician">
                <aop:pointcut id="thinking" expression="
    execution(* com.shiftycat.concert.Volunteer.thinkOfSomething(String))
    and args(thoughts)"/>
                <!-- `arg-names`属性传递了参数给`interceptThoughts()`方法。 -->
                <aop:before pointcut-ref="thinking"
                            method="interceptThoughts"
                arg-names="thoughts"/>
        </aop:aspect>
</aop:config>

输出:

侦听志愿者的心声
志愿者心里想的是:演出真精彩!

5. 通过切面引入新的方法

现在假设要给Performer派生类添加一个新的方法,传统做法是找到所有派生类,让后逐个增加新的方法或者实现。这不但很累而且假设第三方实现没有源码的话,这个过程会变得很困难。幸好,通过Spring AOP可以不必入侵性地改变原有地实现。比如,现在要给所有演出者添加一个receiveAward()方法:

新增一个接口Contestant:

public interface Contestant {
    void receiveAward();
}

由OutstandingContestant实现:

public class OutstandingContestant implements Contestant{
    public void receiveAward() {
        System.out.println("参加颁奖典礼");
    }
}

XML:

<aop:config proxy-target-class="true">
    <aop:aspect>
        <aop:declare-parents
                types-matching="com.shiftycat.concert.Performance+"
                implement-interface="com.shiftycat.concert.Contestant"
                default-impl="com.shiftycat.concert.OutstandingContestant"/>
    </aop:aspect>
</aop:config>

或者:

<bean id="contestantDelegate" class="com.shiftycat.concert.OutstandingContestant"/>
<aop:config proxy-target-class="true">
    <aop:aspect>
        <aop:declare-parents
                types-matching="com.shiftycat.concert.Performance+"
                implement-interface="com.shiftycat.concert.Contestant"
                delegate-ref="contestantDelegate"/>
    </aop:aspect>
</aop:config>
  • types-matching指定所要添加新方法的派生类实现的接口
  • implement-interface指定要实现新的接口
  • default-impl指定这个接口的实现类。

测试:

@Test
public void performXMLTest() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");
    Performance performance = (Performance) context.getBean("rocketStar");
    performance.perform();
    Contestant contestant = (Contestant) context.getBean("rocketStar");
    contestant.receiveAward();
}

输出:

观众入座了
关闭手机了
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
参加颁奖典礼

三、使用注解配置AOP切面

1. 注解切面

除了使用XML配置AOP切面,我们还可以使用更简洁的注解配置。现使用注解修改Audience类:

@Aspect
public class Audience {
    // 声明切点
    @Pointcut("execution(* com.shiftycat.concert.Performance.perform(..))")
    public void performance(){

    }
    // 表演之前
    @Before("performance()")
    public void takeSeats(){
        System.out.println("观众入座");
    }

    // 表演之前
    @Before("performance()")
    public void turnOffCellPhones(){
        System.out.println("关闭手机");
    }

    // 表演之后
    @After("performance()")
    public void applaud(){
        System.out.println("鼓掌: 啪啪啪啪啪");
    }

    // 表演失败后
    @AfterThrowing("performance()")
    public void failure(){
        System.out.println("坑爹,退钱!");
    }
}

@Aspect使得Audience成为了切面。

为了让Spring识别改注解,我们还需在XML中添加<aop:aspectj-autoproxy/><aop:aspectj-autoproxy/>将在Spring应用上下文中创建一个AnnotationAwareAspectJAutoProxyCreator类,它会自动代理@Aspect标注的Bean:

<aop:aspectj-autoproxy proxy-target-class="true" />

测试输出:

观众入座
关闭手机
梁博演唱《出现又离开》
鼓掌: 啪啪啪啪啪

2. 注解环绕通知

使用@Around注解环绕通知:

@Aspect
public class Audience {
    // 声明切点
    @Pointcut("execution(* com.shiftycat.concert.Performance.perform(..))")
    public void performance(){

    }
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("观众入座了");
            System.out.println("关闭手机了");
            joinPoint.proceed();
            System.out.println("鼓掌了: 啪啪啪啪啪");
            joinPoint.proceed();
            System.out.println("鼓掌了: 啪啪啪啪啪");
        } catch (Throwable e) {
            System.out.println("坑爹,退钱!");
        }
    }
}
观众入座了
关闭手机了
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪

3. 注解传递参数

@Aspect
public class Magician implements MindReader{
    private String thoughts;

    // 声明参数化切点
    @Pointcut("execution(* com.shiftycat.concert."
            + "Thinker.thinkOfSomething(String)) && args(thoughts)")
    public void thinking(String thoughts) {

    }
    // 把参数传递给通知
    @Before("thinking(thoughts)")
    public void interceptThoughts(String thoughts) {
        System.out.println("侦听志愿者的心声");
        this.thoughts=thoughts;
    }
    public String getThoughts() {
        return thoughts;
    }
}

<aop:pointcut>变为@Pointcut注解,<aop:before>变为@Before注解。

测试:

@Test
public void TestIntercept() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");
    Volunteer volunteer = context.getBean(Volunteer.class);
    volunteer.thinkOfSomething("演出真精彩!");
    Magician magician = (Magician) context.getBean("magician");
    System.out.println("志愿者心里想的是:"+magician.getThoughts());
}

输出:

侦听志愿者的心声
志愿者心里想的是:演出真精彩!

4. 通过注解引入新的方法

之前通过在XML中配置AOP切面的方法为Bean引入新的方法,现在改用注解的方式来实现:

新建一个ContestantIntroducer类:

@Aspect
public class ContestantIntroducer {
    @DeclareParents(
            value = "com.shiftycat.concert.Performance+",
            defaultImpl = OutstandingContestant.class)
    public static Contestant contestant;
}

@DeclareParents注解代替了之前的<aop:declare-parents>标签。 @DeclareParents注解由三个部分组成:

  1. value属性等同于<aop:declare-parents>types-matching属性。它标识应该被引入指定接口的Bean。

  2. defaultImpl属性等同于<aop:declare-parents>default-impl属性。它标识该类所引入接口的实现。

  3. @DeclareParents注解所标注的static属性制订了将被引入的接口。

<bean class="com.shiftycat.concert.ContestantIntroducer" />

测试:

@Test
public void performXMLTest() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");
    Performance performance = (Performance) context.getBean("rocketStar");
    performance.perform();
    Contestant contestant = (Contestant) context.getBean("rocketStar");
    contestant.receiveAward();
}

输出:

观众入座了
关闭手机了
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
参加颁奖典礼

《Spring实战(第四版)》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值