Spring的核心
为了降低 Java 开发的复杂性,Spring 采取了以下 4 种关键策略:
- 基于 POJO 的轻量级和最小侵入性编程;
- 通过依赖注入和面向接口实现松耦合;
- 基于切面和惯例进行声明式编程;
- 通过切面和模板减少样板式代码。
激发 POJO 的潜能
这是一个简单普通的 Java 类 —— POJO。没有任何地方表明它是一个 Spring 组件。Spring 的非侵入编程模型意味着这个类在 Spring 应用和非 Spring 应用中都可以发挥同样的作用。
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest){
this.quest = quest;
}
public void embarkOnQuest(){
quest.embark();
}
}
Spring 赋予 POJO 魔力的方式之一就是通过 DI 来装配它们。让我们看看 DI 是如何帮助应用对象彼此之间保持松散耦合的。
依赖注入(DI)
BraveKnight 足够灵活可以接受任何赋予他的探险任务:
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest){ //构造器参数注入
this.quest = quest;
}
public void embarkOnQuest(){
quest.embark();
}
}
这里的要点是 BraveKnight 没有与任何特定的 Quest 实现发生耦合。对它来说,被要求挑战的探险任务只要实现了 Quest 接口,那么具体是哪种类型的探险就无关紧要了。这就是 DI 所带来的最大收益 —— 松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
SlayDragonQuest 是要注入到 BraveKnight 中的 Quest 实现:
public class SlayDragonQuest implements Quest {
private PrintStream stream;
public SlayDragonQuest(PrintStream stream){
this.stream = stream;
}
public void embark() {
stream.println("Embarking on quest to slay the dragon!");
}
}
这里最大的问题在于,我们该如何将 SlayDragonQuest 交给 BraveKnight 呢?又如何将 PrintStream 交给 SlayDragonQuest 呢?
创建应用组件之间协作的行为通常称为装配(wiring)。
Spring 提供了基于 Java 的配置,可作为 XML 的替代方案:
@Configuration //相当于spring的配置文件XML
public class KnightConfig {
@Bean //声明为 Spring 中的 bean,bean 的各种名称……虽然 Spring 用 bean 或者 JavaBean 来表示应用组件,但并不意味着 Spring 组件必须要遵循 JavaBean 规范。一个 Spring 组件可以是任何形式的 POJO。
public Knight knight(){
return new BraveKnight(quest());
}
@Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}
尽管 BraveKnight 依赖于 Quest,但是它并不知道传递给它的是什么类型的 Quest,也不知道这个 Quest 来自哪里。与之类似,SlayDragonQuest 依赖于 PrintStream,但是在编码时它并不需要知道这个 PrintStream 是什么样子的。只有 Spring 通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改变所依赖的类的情况下,修改依赖关系。
启动程序:
public class KnightMain {
public static void main(String[] args) {
KnightConfig knightConfig = new KnightConfig();
Knight knight = knightConfig.knight();
knight.embarkOnQuest();
}
}
得到 Knight 对象的引用后,只需简单调用 embarkOnQuest() 方法就可以执行所赋予的探险任务了。注意这个类完全不知道我们的英雄骑士接受哪种探险任务,而且完全没有意识到这是由 BraveKnight 来执行的。只有KnightConfig知道哪个骑士执行哪种探险任务。
应用切面
DI 能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。
图 1.2 展示了这种复杂性。左边的业务对象与系统级服务结合得过于紧密。每个对象不但要知道它需要记日志、进行安全控制和参与事务,还要亲自执行这些服务。
AOP 能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。
比如看看《Spring in Action》书中的一个例子:使用骑士的例子,这里我们为他添加一个切面,假设我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。
//添加吟游诗人这个服务类
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!");
}
}
我们可以通过DI构造函数来注入这个类。
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();
}
}
但是管理他的吟游诗人并不是骑士职责范围内的工作。毕竟,用诗歌记载骑士的探险事迹,这是吟游诗人的职责。为什么骑士还需要提醒吟游诗人去做他份内的事情呢?
此外,因为骑士需要知道吟游诗人,所以就必须把吟游诗人注入到 BarveKnight 类中。这不仅使 BraveKnight 的代码复杂化了,而且还让我疑惑是否还需要一个不需要吟游诗人的骑士呢?如果 Minstrel 为 null 会发生什么呢?我是否应该引入一个空值校验逻辑来覆盖该场景?
简单的 BraveKnight 类开始变得复杂,如果你还需要应对没有吟游诗人时的场景,那代码会变得更复杂。但利用 AOP,你可以声明吟游诗人必须歌颂骑士的探险事迹,而骑士本身并不用直接访问 Minstrel 的方法。
这里使用了 Spring 的 aop 配置命名空间把 Minstrel bean 声明为一个切面。首先,需要把 Minstrel 声明为一个 bean,然后在元素中引用该 bean。为了进一步定义切面,声明 (使用)在 embarkOnQuest() 方法执行前调用 Minstrel 的 singBeforeQuest() 方法。这种方式被称为前置通知(before advice)。同时声明(使用)在 embarkOnQuest() 方法执行后调用 singAfterQuest() 方 法。这种方式被称为后置通知(after advice)。
这就是我们需要做的所有的事情!通过少量的 XML 配置,就可以把 Minstrel 声明为一个 Spring 切面