《Spring实战》学习笔记-第四章:面向切面的Spring

分布于应用中多处的功能称为横切关注点,通过这些横切关注点在概念上是与应用的业务逻辑相分离的,但其代码往往直接嵌入在应用的业务逻辑之中。将这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的。

什么是面向切面编程

切面实现了横切关注点的模块化
切面实现了横切关注点的模块化

面向切面编程中,通过声明的方式定义通用功能(安全、事务等)以何种方式在何处应用,而无需修改受影响的类(CourseService、StudentService等)。

AOP术语

通知(Advice):何种功能、何时

切面的工作被称为通知,同时通知还要解决何时执行这个工作的问题。Spring切面可以应用5种类型的通知:

  • Before:在方法被调用之前调用通知;
  • After:在方法调用之后调用通知;
  • After-returning:在方法成功执行后;
  • After-throwing:在方法抛出异常后;
  • Around:在方法调用之前和之后都会调用通知;

连接点(Joinpoint):能够应用通知的点

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

切点(Pointcut):何处,应用通知的连接点的集合

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

切面(Aspect)

切面是通知和切点的结合,即何时在何处完成何种功能。

引入(Introduction)

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

织入(Weaving)

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

  • 编译期:需要特殊的编译器,AspectJ的织入编译器就是这种方式;
  • 类加载期:在目标类加载到JVM时被织入,需要特殊的类加载器。
  • 运行期:在应用运行的某个时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是这种方式。

Spring对AOP的支持

  • 基于代理的经典AOP;
  • @AspectJ注解驱动的切面;
  • 纯POJO切面;
  • 注入式AspectJ切面(适合Spring个版本);

Spring是在运行期将切面织入到所管理的Bean中的,如图所示,代理类封装了目标类,当拦截到方法调用时,在调用目标Bean的方法之前,代理会执行切面逻辑。真正应用需要被代理的Bean时,Spring才会创建代理对象。Spring的切面由包裹了目标对象的代理类实现,代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。

Spring的切面由包裹了目标对象的代理类实现,代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。
Spring的切面由包裹了目标对象的代理类实现,代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。

Spring只支持方法连接点,缺少对字段连接点的支持,例如拦截对象字段的修改。也不支持构造器连接点,也就无法在Bean创建时应用通知。

使用切点选择连接点

Spring AOP中,需要使用AspectJ的切点表达式来定义切点。

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

编写切点

使用AspectJ切点表达式来定位
使用AspectJ切点表达式来定位

这里使用了execution()指示器来选择Instrument的play()方法。表达式以*开头表示不关心返回值的类型,然后指定了全限定类名和方法名,使用..作为方法的参数列表,表示可以是任意的入参。

使用&&将execution()和within()进行连接,那么也就可以使用||(或)和!(非)。

使用within()指示器限制切点范围
使用within()指示器限制切点范围

使用Spring的bean()指示器

bean()使用Bean id来作为参数,从而限制切点只匹配特定的Bean,如:

execution(* com.springinaction.springidol.Instrument.play()) and bean(eddie)

这里,表示在执行Instrument的play()方法时应用通知,但限定Bean的id为eddie。

在XML中声明切面

AOP配置元素 描述
<aop:advisor> 定义AOP通知器
<aop:after> 定义AOP后置通知(不管该方法是否执行成功)
<aop:after-returning> 在方法成功执行后调用通知
<aop:after-throwing> 在方法抛出异常后调用通知
<aop:around> 定义AOP环绕通知
<aop:aspect> 定义切面
<aop:aspect-autoproxy> 定义@AspectJ注解驱动的切面
<aop:before> 定义AOP前置通知
<aop:config> 顶层的AOP配置元素,大多数的<aop:*>包含在<aop:config>元素内
<aop:declare-parent> 为被通知的对象引入额外的接口,并透明的实现
<aop:pointcut> 定义切点

下面定义一个观众类:

package com.springinaction.springidol;

public class Audience {

    // 表演之前
    public void takeSeats() {
        System.out.println("The audience is taking their seats.");
    }

    // 表演之前
    public void turnOffCellPhones() {
        System.out.println("The audience is turning off their cellphones");
    }

    // 表演之后
    public void applaud() {
        System.out.println("CLAP CLAP CLAP CLAP CLAP");
    }

    // 表演失败之后
    public void demandRefund() {
        System.out.println("Boo! We want our money back!");
    }
}

声明前置和后置通知

<?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:aop="http://www.springframework.org/schema/aop"
    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">

    <bean id="eddie" class="com.springinaction.springidol.Instrumentalist">
        <property name="instrument">
            <bean class="com.springinaction.springidol.Guitar" />
        </property>
        <property name="song" value="my love" />
    </bean>

    <bean id="audience" class="com.springinaction.springidol.Audience" />

    <aop:config>
        <aop:aspect ref="audience"><!-- 引用audience Bean -->
            <!-- 声明切入点 -->
            <aop:pointcut id="performance"
                expression="execution(* com.springinaction.springidol.Performer.perform(..))" />
            <!-- 表演之前 -->
            <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="demandRefund" />
        </aop:aspect>
    </aop:config>
</beans>

<aop:config>中,可以声明一个或多个通知器、切面或者切点。pointcut属性定义了通知所引用的切点。最终的通知逻辑如何织入到业务逻辑中:

Audience切面包含4中通知,这些通知把通知=逻辑织入到匹配的切面的切点方法中
Audience切面包含4中通知,这些通知把通知=逻辑织入到匹配的切面的切点方法中

测试代码:

    @Test
    public void testBeforeAndAfter() throws PerformanceException{
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-idol.xml");
        Performer performer = (Performer) context.getBean("eddie");
        performer.perform();
    }

测试结果:

The audience is taking their seats.
The audience is turning off their cellphones
Playing my love : Guitar Guitar Guitar
CLAP CLAP CLAP CLAP CLAP

声明环绕通知

前置通知和后置通知之间共享消息需要使用成员变量,而Audience是单例,使用成员变量有可能存在线程安全问题。使用环绕通知可以完成之前前置和后置所实现的相同功能,而且只需一个方法。

package com.springinaction.springidol;

import org.aspectj.lang.ProceedingJoinPoint;

public class AroundAudience {
    public void watchPerformance(ProceedingJoinPoint joinpoint) {
        try {
            // 表演之前
            System.out.println("The audience is taking their seats.");
            System.out.println("The audience is turning off their cellphones");
            long start = System.currentTimeMillis();

            // 执行被通知的方法
            joinpoint.proceed();

            // 表演之后
            long end = System.currentTimeMillis();
            System.out.println("CLAP CLAP CLAP CLAP CLAP");
            System.out.println("The performance took " + (end - start) + " milliseconds.");
        } catch (Throwable t) {
            // 表演失败之后
            System.out.println("Boo! We want our money back!");
        }
    }
}

ProceedingJoinPoint作为入参,从而可以在通知里调用被通知的方法。

XML配置:

<bean id="audience" class="com.springinaction.springidol.AroundAudience" />

<aop:config>
    <aop:aspect ref="audience"><!-- 引用audience Bean -->
        <!-- 声明切入点 -->
        <aop:pointcut id="performance"
            expression="execution(* com.springinaction.springidol.Performer.perform(..))" />
        <aop:around method="watchPerformance" pointcut-ref="performance" />
    </aop:aspect>
</aop:config>

为通知传递参数

读心者:

package com.springinaction.springidol;

public interface MindReader {
    void interceptThoughts(String thoughts);

    String getThoughts();
}

Magician是MindReader 接口的一个简单实现:

package com.springinaction.springidol;

public class Magician implements MindReader {
    private String thoughts;

    @Override
    public void interceptThoughts(String thoughts) {
        System.out.println("Intercepting volunteer's thoughts");
        this.thoughts = thoughts;
    }

    @Override
    public String getThoughts() {
        return thoughts;
    }

}

下面是一个志愿者,供读心者去截取他的内心感应:

package com.springinaction.springidol;

public interface Thinker {
    void thinkOfSomething(String thoughts);
}
package com.springinaction.springidol;

public class Volunteer implements Thinker {
    private String thoughts;

    @Override
    public void thinkOfSomething(String thoughts) {
        System.out.println("Thinker: " + thoughts);
        this.thoughts = thoughts;
    }

    public String getThoughts() {
        return thoughts;
    }
}

通过配置实现将被通知方法的参数传递给通知:

<bean id="volunteer" class="com.springinaction.springidol.Volunteer" />
<bean id="magician" class="com.springinaction.springidol.Magician" />

<aop:config>
    <aop:aspect ref="magician"><!-- 引用magician Bean -->
        <!-- 声明切入点 -->
        <aop:pointcut id="thinking"
            expression="execution(* com.springinaction.springidol.Thinker.thinkOfSomething(String)) and args(thoughts) " />
        <aop:before method="interceptThoughts" pointcut-ref="thinking"
            arg-names="thoughts" />
    </aop:aspect>
</aop:config>

切入点指定了Thinker的thinkOfSomething()方法,指定了String参数,然后在args参数中标识了将thoughts作为参数。

同时,<aop:before>引用了thoughts参数,标识该参数必须传递给magician的interceptThoughts()方法。

注意:

<aop:before>引用的thoughts参数和pointcut标识的thoughts参数,二者名称必须一致!

测试:

    @Test
    public void testBeforeArgs() throws PerformanceException {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-magician.xml");
        Thinker thinker = (Thinker) context.getBean("volunteer");
        MindReader mindReader = (MindReader) context.getBean("magician");
        thinker.thinkOfSomething("晚上吃啥呢?");
        System.out.println("MindReader: " + mindReader.getThoughts());
    }

测试结果:

Intercepting volunteer's thoughts
Thinker: 晚上吃啥呢?
MindReader: 晚上吃啥呢?

通过切面引入新功能

切面可以为SpringBean添加新方法:

<aop:aspect>
    <aop:declare-parents types-matching="com.springinaction.springidol.Performer+"
        implement-interface="com.springinaction.springidol.Contestant"
        default-impl="com.springinaction.springidol.GraciousContestant" />
</aop:aspect>

<aop:declare-parents >声明了此切面所通知的Bean在它的对象层次结构中拥有新的父类,即类型匹配Performer接口(由types-matching指定)的Bean会实现Contestant接口(由implement-interface指定),同时可以指定Contestant的实现(default-impl,也可以用delegate-ref指定一个Spring Bean来实现)。

注解切面:@Aspect、@Pointcut、<aop:aspectj-autoproxy />

采用注解的方式将之前的Audience标注为一个切面:

package com.springinaction.springidol;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectJAudience {

    // 定义切点
    @Pointcut("execution(* com.springinaction.springidol.Performer.perform(..))")
    public void performance() {

    }

    // 表演之前
    @Before("performance()")
    public void takeSeats() {
        System.out.println("The audience is taking their seats.");
    }

    // 表演之前
    @Before("performance()")
    public void turnOffCellPhones() {
        System.out.println("The audience is turning off their cellphones");
    }

    // 表演之后
    @AfterReturning("performance()")
    public void applaud() {
        System.out.println("CLAP CLAP CLAP CLAP CLAP");
    }

    // 表演失败之后
    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("Boo! We want our money back!");
    }

}

@Pointcut注解用于定义一个在@AspectJ切面内可重用的切点,其值是一个AspectJ切点表达式,这里标识该切点必须匹配Performer接口的perform()方法。performance()切点的名称作为参数赋值给了所有的通知注解,从而可以标识每一个通知方法应该应用在哪里。

**AfterReturning 和After 的区别:

  • AfterReturning 增强处理处理只有在目标方法成功完成后才会被织入。
  • After 增强处理不管目标方法如何结束(保存成功完成和遇到异常中止两种情况),它都会被织入。

使用配置注解,首先我们要将切面在spring上下文中声明成自动代理bean,即
<aop:aspectj-autoproxy />

测试代码:

    @Test
    public void testAspectJBeforeAndAfter() throws PerformanceException {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-idol.xml");
        Performer performer = (Performer) context.getBean("eddie");
        performer.perform();
    }

测试结果:

The audience is taking their seats.
The audience is turning off their cellphones
Playing my love : Guitar Guitar Guitar
CLAP CLAP CLAP CLAP CLAP

运行测试程序时可能会出错,形如:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'eddie' defined in class path resource [spring-idol.xml]: Cannot create inner bean 'com.springinaction.springidol.Guitar#365d15c6' of type [com.springinaction.springidol.Guitar] while setting bean property 'instrument'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.springinaction.springidol.Guitar#365d15c6' defined in class path resource [spring-idol.xml]: Initialization of bean failed; nested exception is java.lang.IllegalArgumentException: error at ::0 can't find referenced pointcut performance

上网搜了一下,发现是JDK不匹配。
我原来用的JDK1.7匹配的是aspectjrt.1.6.2和aspectjweaver.1.6.2,因此会报错。

如果要使用AspectJ完成注解切面需要注意下面的JDK与AspectJ的匹配:
JDK1.6 —— aspectJ1.6
JDK1.7 —— aspectJ1.7.3+

注解环绕通知:@Around

package com.springinaction.springidol;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectJAroundAudience {

    // 定义切点
    @Pointcut("execution(* com.springinaction.springidol.Performer.perform(..))")
    public void performance() {

    }

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinpoint) {
        try {
            // 表演之前
            System.out.println("The audience is taking their seats.");
            System.out.println("The audience is turning off their cellphones");
            long start = System.currentTimeMillis();

            // 执行被通知的方法
            joinpoint.proceed();

            // 表演之后
            long end = System.currentTimeMillis();
            System.out.println("CLAP CLAP CLAP CLAP CLAP");
            System.out.println("The performance took " + (end - start) + " milliseconds.");
        } catch (Throwable t) {
            // 表演失败之后
            System.out.println("Boo! We want our money back!");
        }
    }
}

不要忘了配置:@Aspect和<aop:aspectj-autoproxy/>
测试代码:

    @Test
    public void testAspectJAround() throws PerformanceException {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-idol-around.xml");
        Performer performer = (Performer) context.getBean("eddie");
        performer.perform();
    }

传递参数给所标注的通知

package com.springinaction.springidol;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

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

    @Pointcut("execution(* com.springinaction.springidol.Thinker.thinkOfSomething(String)) && args(thoughts))")
    public void thinking(String thoughts){
        
    }

    @Override
    @Before("thinking(thoughts)")
    public void interceptThoughts(String thoughts) {
        System.out.println("Intercepting volunteer's thoughts");
        this.thoughts = thoughts;
    }

    @Override
    public String getThoughts() {
        return thoughts;
    }

}

<aop:pointcut> 变为@Pointcut,<aop:before>变为@Before,注解里不需要arg-names属性所对应的注解。


如果觉得有用,欢迎关注我的微信,有问题可以直接交流:

你的关注是对我最大的鼓励!
你的关注是对我最大的鼓励!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容

  • 本章内容: 面向切面编程的基本原理 通过POJO创建切面 使用@AspectJ注解 为AspectJ切面注入依赖 ...
    谢随安阅读 3,132评论 0 9
  • 在生活中,监控用电量是一个很重要的功能,但并不是大多数家庭重点关注的问题。软件系统的一些功能就像家里的电表一样,这...
    yjaal阅读 569评论 0 3
  • 如果说写作是一种疗伤,我更情愿说它像一场大雨,你脱光衣服,冲到暴风之中,把自己赤裸裸的扔到天地之间。你看见我的脆弱...
    不敢阅读 334评论 0 2
  • 很功利地说,我想很多人想要去了解关于爱情的理论,无非是想找到一些靠谱的判断标准,来判断自己的感情里是否有足够的爱情...
    海慧98阅读 572评论 0 4
  • 丑时是人深夜静 最美好时刻 一般玩艺术的人 都是在 人深夜静 无声无息的环境中 构思布局艺术产品的结构 创造出奇迹...
    老武家阅读 472评论 0 2