Spring的作用

Spring是为了简化企业级应用开发而创建的。

Spring是如何简化Java开发的

它采取了以下4种关键策略:
1、基于POJO的轻量级和最小侵入性编程
2、通过依赖注入和面向接口实现松耦合
3、基于切面和惯例进行声明式编程
4、通过切面和模板减少样板式代码

基于POJO的轻量级和最小侵入性编程

Spring竭力避免因自身的API而弄乱应用代码,最多使用下Spring的注解。
Spring赋予POJO魔力的方式之一是通过依赖注入装配它们。

依赖注入

如果不使用依赖注入

任何一个有实际意义的应用,都会由两个或者更多的类组成,这些类相互协作来完成特定的业务逻辑。按照传统的做法,每个对象负责管理与自己相互协作的对象的引用,这将会导致高度耦合和难以测试的代码。
举个例子:

package com.springinaction.knights;
//骑士救公主
public class DamselRescuingKnight implements Knight{

    private RescueDamselQuest quest;

    public DamselRescuingKnight() {
        //构造方法是骑士救公主探险的方法
         this.quest = new RescueDamselQuest();

    }
    //骑士即将探险
    public void embarkOnQuest() {
        quest.embark();
    }

    public static void main(String[] args) {
        DamselRescuingKnight knight = new DamselRescuingKnight();
        knight.embarkOnQuest();
    }
}

可以看到DamselRescuingKnight在其构造函数中new了个RescueDamselQuest,这使得它们两个类紧密耦合在一起。这限制了骑士的执行探险的能力,骑士现在可以执行救援公主的探险,但是如果一条恶龙需要杀掉,就爱莫能助了。
更糟糕的是我们为DamselRescuingKnight编写单元测试非常困难,因为你必须保证当embarkOnQuest()方法被调用时,embark()方法也被调用。但是没有一个简单明了的方式可以实现这一点。
耦合具有两面性,一方面,紧密耦合的代码难以测试、难以复用、难以理解;另一方面,一定程度的耦合又是必须的,因为完全没有耦合的代码什么也做不了。

如果使用依赖注入

通过依赖注入,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系。依赖关系将被自动注入到需要它们的对象当中去。如下代码所示:

public class BraveKnight implements Knight {
    private Quest quest;

    //Quest被注入进来
    public BraveKnight(Quest quest) {
        this.quest = quest;
    }

    @Override
    public void embarkOnQuest() {
        quest.embark();
    }
}

我们可以看到,BraveKnight没有自行创建探险任务,而是在构造器中将探险任务作为参数传入。这是依赖注入的方式之一,即构造器注入。
更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。所以BeaveKnight能够响应Quest的任意实现类。

这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说,被要求挑战的探险任务只要实现了Quest接口,具体是什么类型的探险无关紧要。这就是依赖注入所带来的最大收益——松耦合。如果一个对象只通过接口来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。

对依赖进行替换的最常用方法就是在测试的时候使用mock实现。我们无法充分地测试DamselRescuingKnight,因为它是紧耦合的,但是可以轻松地测试BraveKnight,只需要给它一个Quest接口的mock实现即可。

public class BraveKnightTest {
    @Test
    public void knightShouldEmbarkOnQuest() {
        //创建mockQuest
        Quest mockQuest = mock(Quest.class);
        //注入mockQuest
        BraveKnight braveKnight = new BraveKnight(mockQuest);
        braveKnight.embarkOnQuest();
        verify(mockQuest, times(1)).embark();

        DamselRescuingKnight mockDa = mock(DamselRescuingKnight.class);
        mockDa.embarkOnQuest();
    }
}

你可以使用mock框架Mockito去创建一个Quest接口的mock实现。通过这个mock对象,就可以创建一个新的BraveKnight实例,并通过构造器注入这个mock Quest。当调用embarkOnQuest()方法时,你可以要求Mockito框架验证Quest的mock实现的embark()方法仅仅被调用了一次。

将Quest注入到Knight中

现在BraveKnight类可以接受你传递给它的任意一种Quest的实现,但如何传入特定的Quest实现类呢?
通过如下代码我们可以看到,KillDragonQuest实现了Quest接口,所以可以注入到BraveKnight中去。不同的是,KillDragonQuest没有使用System.out.println(),而是在构造方法中使用了更为通用的PrintStream。这里最大的问题在于,我们该如何将KillDragonQuest交给BraveKnight呢?又如何将PrintStream交给KillDragonQuest呢?

public class KillDragonQuest implements Quest {

    private PrintStream stream;

    public KillDragonQuest(PrintStream stream) {
        this.stream = stream;
    }

    public void embark() {
        stream.println("即将进行杀龙探险");
    }
}

创建应用组件之间协作的行为通常称为装配
Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。如下代码展示了一个简单的Spring配置文件:knights.xml,该配置文件将BraveKnight、PrintStream和KillDragonQuest装配到了一起。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="knight" class="com.springinaction.knights.BraveKnight">
        <constructor-arg ref="quest"/>                        <!--注入Quest bean-->
    </bean>
    <bean id="quest" class="com.springinaction.knights.KillDragonQuest">     <!--创建KillDragonQuest-->
        <constructor-arg value="#{T(System).out}"/>
    </bean>
</beans>

在这里,BraveKnight和KillDragonQuest被声明为Spring中的bean。就BraveKnight bean来讲,他在构造时传入对KillDragonQuest bean的引用,将其作为构造器参数。同时,KillDragonQuest bean 的声明使用了Spring表达式语言(Spring Expression Language),将System.out(这是一个PrintStream)传入到了KillDragonQuest的构造器中。
Spring还支持使用Java来描述配置。如下代码所示:

@Configuration
public class KnightConfig {
    @Bean
    public Knight knight() {
        return new BraveKnight(quest());
    }
    
    @Bean
    public Quest quest() {
        return new KillDragonQuest(System.out);
    }
}

不管你使用哪种配置方式,依赖注入带来的收益相同。尽管BraveKnight依赖于Quest,但它不知道传递给它的是什么类型的Quest。与之类似,KillDragonQuest依赖于PrintStream,但是在编码的时候不需要知道PrintStream是什么样子的。只有Spring通过它的配置,了解是如何装配的。这样就可以不改变所依赖的类的情况下,修改依赖关系。

观察装配如何工作

Spring通过应用上下文(Application context) 装载bean的定义,并把它们组装起来。Spring应用上下文全权负责对象的创建个组装,Spring自带了多种应用上下文的实现,他们之间的主要区别仅仅在于如何加载配置。

因为knights.xml中的bean是使用XML文件进行配置的,所以选择ClassPathXmlApplicationContext作为应用上下文相对是比较合适的。该类加载位于应用程序类路径下的一个或多个Xml配置文件。如下代码,main()方法调用ClassPathXmlApplicationContext加载Knights.xml,并获得Knight对象的引用。

public class KnightMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("knights.xml");    //加载Sprinig应用上下文
        Knight knight = context.getBean(Knight.class);                                       //获取knight bean
        knight.embarkOnQuest();                                                              //使用knight调用方法
        context.close();                                                                     //关闭应用上下文
    }
}

这里的main()方法基于knight.xml文件创建了spring应用上下文。随后他调用该应用上下文获取一个ID为knighht的bean。得到Knighht对象的引用后,只需要简单调用embarkOnQuest方法就可以执行所赋予的探险任务了。只有knights.xml知道哪个骑士执行力哪种探险任务。