面向切面的 Spring

一 绪论

前面的章节讲解的主要是 DI 依赖注入。这部分是 Spring AOP 与 AspectJ 两个部分。其核心的主旨都是面向切面编程。

软件中散布于应用之中的多处功能被称为 横切关注点 ,一般来说横切关注点是与应用的业务逻辑相分离的。把这些横切关注点与业务逻辑分离是面向切面编程所要关注和处理的地方。比如 消息/安全/事务/日志等等 功能,都属于横切关注点,这些内容与业务逻辑没有关联,散布在工程的各个角落。如果是硬编码,我们需要在每一个使用到这些功能的地方添加新的代码,但是我们是在做重复的工作,因为每一个地方添加的代码大体上都是一致的,这种重复枯燥的代码充斥工程的感觉很烦。

AOP_introduction.jpg

面对上述的场景,最直接的处理方式是使用 继承,但是继承会导致应用面对统一的基类代码,对象体系在演变过程中很容易变得复杂难以处理。

二 AOP 术语

2.1 通知(Advice)

通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。

Spring 切面可以应用 5 种类型的通知:

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

2.2 连接点(Join Point)

在应用之中有很多时机可以应用通知,这些时机被称为连接点。连接点是应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时或者是修改一个字段时。切面代码可以利用这些点插入到应用的正常流程中,并加载新的行为。

2.3 切点(Pointcut)

如果说通知定义了切面的“什么”和“何时”,那么切点就是定义了“何处”。 切点的定义会匹配通知所要织入的一个或多个连接点。 我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

2.4 切面(Aspect)

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

2.5 引入(Introduction)

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

2.6 织入(Weaving)

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

  • 编译期:切面在目标类编译时被织入,这种方式需要 特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器。它可以在目标类被引入应用之前增强该目标类的字节码。依然是 AspectJ 的加载时织入支持这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态的创建一个代理对象。Spring AOP 就是以这种方式织入切面的。

三 Spring 对 AOP 的支持

AOP 框架之间是有差异的,有些支持在字段修饰符级别应用通知,有些只支持与方法调用相关的连接点。

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

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

前面三种都是 Spring AOP 实现的变体,Spring AOP 构建在 动态代理 基础之上,因此,Spring 对 AOP 的支持局限于方法拦截。

基于 Spring 的通知是使用 Java 编写的,上手很快。不过与之对应的 AspectJ 有自己特有的 AOP 语言。它提供更强大的和细粒度的控制,以及更丰富的 AOP 工具集。

3.1 Spring 在运行时通知对象

proxy.jpg

通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。

直到应用需要被代理的 bean 时,Spring 才创建代理对象。

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

3.2 通过切点来选择连接点

切点用于准确定位应该在什么地方应用切面的通知。通知和切点是切面最基本的元素

Spring AOP 要使用 AspectJ 的切点表达式语言来定义切点。这里需要注意的是 Spring 仅支持 AspectJ 切点指示器的一个子集。
AspectJ 的切点表达式语言:

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

当我们查看到上述的 Spring 支持的指示器时,注意只有 execution 指示器是实际执行匹配的,而其他的指示器是用来限制匹配的。这说明 execution 指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器限制所匹配的切点。

@Aspect
@Component
public class Audience {

    /**
     * 切点表达式中: execution 指示器表示在方法执行时触发;* 表示我们不关心方法返回值的类型;然后指定了全限定类名和方法名;对于方法参数列表,
     * 使用两个点号 (..) 表明切点要选择任意的 perform 方法
     */
    @Before("execution(* com.doctorwork.aop.Performance.perform(..))")
    public void silenceCellPhones() {
        System.out.println("audience silence cell phones.");
    }

    /**
     * 切点表达式同上,只是这个方法是在监控的方法执行结束之后再执行。
     */
    @After("execution(* com.doctorwork.aop.Performance.perform(..))")
    public void applause() {
        System.out.println("audience applause.");
    }

    /**
     * 自定义切点
     */
    @Pointcut("execution(* com.doctorwork.aop.impl.Broadway.perform(..))")
    public void performance() {}

    /**
     * 环绕通知
     */
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {
        try {
            System.out.println("broadway is beautiful.");
            jp.proceed();
            System.out.println("We will go back..");
        } catch (Throwable e) {
            System.out.println("something is wrong.");
        }
    }

}

上述就是一个基于注解的 切面 类。切面类中的方法上都带了注解,类似 @After 之类的都是 通知;注解之后的表达式就是 切点;所谓的 连接点,在一般的 Spring 通知上面都是 方法执行前/后。

四 简单测试

4.1 基于显式的 JavaConfig 配置

要将上述代码转换为 Spring 中的一个切面类,需要进一步的配置。如果使用 JavaConfig,可以在配置类级别上使用注解

// 开启自动代理功能
@EnableAspectJAutoProxy

如下为配置类

/**
 * @author gaopeng@doctorwork.com
 * @description
 *    @EnableAspectJAutoProxy 注解用于开启 aspectJ 自动代理
 *    @ComponentScan 用于组件扫描的注解,默认的、无附加信息的这个注解只会扫描配置类所在的包以及子包
 * @date 2018-05-30 20:33
 **/
@EnableAspectJAutoProxy
@ComponentScan
public class PerformanceConfig {
}

以下为目标类接口和简单的实现

public interface Performance {

    void perform();
}
==================================
/**
 * @author gaopeng@doctorwork.com
 * @description
 * @date 2018-05-30 19:59
 **/
@Primary
@Component
public class Woodstock implements Performance{

    public void perform() {
        System.out.println("woodstock!");
    }
}

以下为测试类

/**
 * @author gaopeng@doctorwork.com
 * @description
 *  这里使用了 @Resource 注解,这个注解与前面使用的 @Autowired 注解的主要区别是,resource 是按名称装配,另一个是默认按类型装配
 *  配合此处,一个接口有多个实现类,所以此处使用 @Resource 是更合适的。这个注解还可以制定 bean id
 *      @Resouorce(name = "woodstock")
 * @date 2018-05-30 20:32
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = PerformanceConfig.class)
public class AOPTest {

    @Resource
    private Performance woodstock;

    @Test
    public void aopTest() {
        woodstock.perform();
    }
}

以下为测试输出结果

audience silence cell phones.
woodstock!
audience applause.

4.2 基于 XML 配置的声明方式

    <!--<context:component-scan base-package="com.doctorwork.aop" />-->

    <!-- 启用 aspectJ 切面自动代理 -->
    <aop:aspectj-autoproxy />

    <!-- 切面类 bean 注入 -->
    <bean id="audience" class="com.doctorwork.aop.XMLAudience" />
    <!-- 被代理类 bean 注入 -->
    <bean id="waterCube" class="com.doctorwork.aop.impl.WaterCube" />

    <!-- 将注入的 bean 声明为一个切面 -->
    <aop:config>
        <!-- 引用目标 bean 并将其声明为切面类 -->
        <aop:aspect ref="audience">
            <aop:before method="silenceCellPhones"
                        pointcut="execution(* com.doctorwork.aop.Performance.perform(..))" />
            <aop:after method="applause"
                       pointcut="execution(* com.doctorwork.aop.Performance.perform(..))" />
        </aop:aspect>
    </aop:config>

去掉对应切面类中的各种纷杂的注解,我们可以通过 XML 配置文件的方式完成切面声明和 bean 的注入。这种方式在企业开发中更加常见。免配置的方式整体上会比较简单简洁,但是在实际的开发过程中我们都更多的是使用 XML 配置来处理。

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

推荐阅读更多精彩内容