1.1.3 应用切面

DI 能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。

面向切面编程往往被定义为促使软件系统实现关注点的分离一项技术。系统由许多不同的组件组成,每一个组件各负责一块特定功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责。诸如日志、事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们会跨越系统的多个组件。

如果将这些关注点分散到多个组件中去,你的代码将会带来双重的复杂性。

  • 实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果你要改变这些关注点的逻辑,必须修改各个模块中的相关实现。即使你把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法的调用还是会重复出现在各个模块中。

  • 组件会因为那些与自身核心业务无关的代码而变得混乱。一个向地址簿增加地址条目的方法应该只关注如何添加地址,而不应该关注它是不是安全的或者是否需要支持事务。

图 1.2 展示了这种复杂性。左边的业务对象与系统级服务结合得过于紧密。每个对象不但要知道它需要记日志、进行安全控制和参与事务,还要亲自执行这些服务。

AOP 能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自身的业务,完全不需要了解涉及系统服务所带来复杂性。总之,AOP 能够确保 POJO 的简单性。

如图 1.3 所示,我们可以把切面想象为覆盖在很多组件之上的一个外壳。应用是由那些实现各自业务功能的模块组成的。借助 AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,你的核心应用甚至根本不知道它们的存在。这是一个非常强大的理念,可以将安全、事务和日志关注点与核心业务逻辑相分离。

为了示范在 Spring 中如何应用切面,让我们重新回到骑士的例子,并为它添加一个切面。

AOP 应用

每一个人都熟知骑士所做的任何事情,这是因为吟游诗人用诗歌记载了骑士的事迹并将其进行传唱。假设我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。程序清单 1.9 展示了我们会使用的 Minstrel 类。

程序清单 1.9 吟游诗人是中世纪的音乐记录器
package sia.knights;

import java.io.PrintStream;

public class Minstrel {

  private PrintStream stream;
  
  public Minstrel(PrintStream stream) {
    this.stream = stream;
  }

  public void singBeforeQuest() {
    stream.println("Fa la la, the knight is so brave!");
  }

  public void singAfterQuest() {
    stream.println("Tee hee hee, the brave knight " +
    		"did embark on a quest!");
  }

}

正如你所看到的那样,Minstrel 是只有两个方法的简单类。在骑士执行每一个探险任务之前,singBeforeQuest() 方法会被调用;在骑士完成探险任务之后,singAfterQuest() 方法会被调用。在这两种情况下,Minstrel 都会通过一个 PrintStream 类来歌颂骑士的事迹,这个类是通过构造器注入进来的。

把 Minstrel 加入你的代码中并使其运行起来,这对你来说是小事一桩。我们适当做一下调整从而让 BraveKnight 可以使用 Minstrel。程序清单 1.10 展示了将 BraveKnight 和 Minstrel 组合起来的第一次尝试。

程序清单 1.10 BraveKnight 必须要调用 Minstrel 的方法
package com.springinaction.knights;

public class BraveKnight implements Knight {

  private Quest quest;
  private Minstrel minstrel;
  
  public BraveKnight(Quest quest, Minstrel minstrel) {
    this.quest = quest;
    this.minstrel = minstrel;
  }
  
  public void embarkOnQuest() throws QuestException {
    minstrel.singBeforeQuest();
    quest.embark();
    minstrl.singAfterQuest();
  }
  
} 

这应该可以达到预期效果。现在,你所需要做的就是回到 Spring 配置中,声明 Minstrel bean 并将其注入到 BraveKnight 的构造器之中。但是,请稍等……

我们似乎感觉有些东西不太对。管理他的吟游诗人真的是骑士职责范围内的工作吗?在我看来,吟游诗人应该做他份内的事,根本不需要骑士命令他这么做。毕竟,用诗歌记载骑士的探险事迹,这是吟游诗人的职责。为什么骑士还需要提醒吟游诗人去做他份内的事情呢?

此外,因为骑士需要知道吟游诗人,所以就必须把吟游诗人注入到 BarveKnight 类中。这不仅使 BraveKnight 的代码复杂化了,而且还让我疑惑是否还需要一个不需要吟游诗人的骑士呢?如果 Minstrel 为 null 会发生什么呢?我是否应该引入一个空值校验逻辑来覆盖该场景?

简单的 BraveKnight 类开始变得复杂,如果你还需要应对没有吟游诗人时的场景,那代码会变得更复杂。但利用 AOP,你可以声明吟游诗人必须歌颂骑士的探险事迹,而骑士本身并不用直接访问 Minstrel 的方法。

要将 Minstrel 抽象为一个切面,你所需要做的事情就是在一个 Spring 配置文件中声明它。程序清单 1.11 是更新后的 knights.xml 文件,Minstrel 被声明为一个切面。

程序清单 1.11 将 Minstrel 声明为一个切面
<?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/aop 
  http://www.springframework.org/schema/aop/spring-aop.xsd
  http://www.springframework.org/schema/beans 
  http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="knight" class="sia.knights.BraveKnight">
    <constructor-arg ref="quest" />
  </bean>

  <bean id="quest" class="sia.knights.SlayDragonQuest">
    <constructor-arg value="#{T(System).out}" />
  </bean>

  <bean id="minstrel" class="sia.knights.Minstrel">
    <constructor-arg value="#{T(System).out}" />
  </bean>

  <aop:config>
    <aop:aspect ref="minstrel">
      <aop:pointcut id="embark"
          expression="execution(* *.embarkOnQuest(..))"/>
        
      <aop:before pointcut-ref="embark" 
          method="singBeforeQuest"/>

      <aop:after pointcut-ref="embark" 
          method="singAfterQuest"/>
    </aop:aspect>
  </aop:config>
  
</beans>

这里使用了 Spring 的 aop 配置命名空间把 Minstrel bean 声明为一个切面。首先,需要把 Minstrel 声明为一个 bean,然后在元素中引用该 bean。为了进一步定义切面,声明 (使用)在 embarkOnQuest() 方法执行前调用 Minstrel 的 singBeforeQuest() 方法。这种方式被称为前置通知(before advice)。同时声明(使用)在 embarkOnQuest() 方法执行后调用 singAfterQuest() 方 法。这种方式被称为后置通知(after advice)。

在这两种方式中,pointcut-ref 属性都引用了名字为 embark 的切入点。该切入点是在前边的元素中定义的,并配置 expression 属性来选择所应用的通知。表达式的语法采用的是 AspectJ 的切点表达式语言。

现在,你无需担心不了解 AspectJ 或编写 AspectJ 切点表达式的细节, 我们稍后会在第 4 章详细地探讨 Spring AOP 的内容。现在你已经知道,Spring 在骑士执行探险任务前后会调用 Minstrel 的 singBeforeQuest() 和 singAfterQuest() 方法,这就足够了。

这就是我们需要做的所有的事情!通过少量的 XML 配置,就可以把 Minstrel 声明为一个 Spring 切面。如果你现在还没有完全理解,不必担心,在第 4 章你会看到更多的 Spring AOP 示例,那将会帮助你彻底弄清楚。现在我们可以从这个示例中获得两个重要的观点。

首先,Minstrel 仍然是一个 POJO,没有任何代码表明它要被作为一个切面使用。当我们按照上面那样进行配置后,在 Spring 的上下文中,Minstrel 实际上已经变成一个切面了。

其次,也是最重要的,Minstrel 可以被应用到 BraveKnight 中,而 BraveKnight 不需要显式地调用它。实际上,BraveKnight 完全不知道 Minstrel 的存在。

必须还要指出的是,尽管我们使用 Spring 魔法把 Minstrel 转变为一 个切面,但首先要把它声明为一个 Spring bean。能够为其他 Spring bean 做到的事情都可以同样应用到 Spring 切面中,例如为它们注入依赖。 应用切面来歌颂骑士可能只是有点好玩而已,但是 Spring AOP 可以做很多有实际意义的事情。在后续的各章中,你还会了解基于 Spring AOP 实现声明式事务和安全(第 9 章和第 14 章)。

但现在,让我们再看看 Spring 简化 Java 开发的其他方式。

Last updated