教学视频中采用的是Eclipse作为IDE,先是安装Tomcat9,然后是Eclipse,之后是在Eclipse中配置Tomcat部署。我用的是IntelliJ IDEA 2018 Ultimate 2018.3,就记录一下自己的方式吧。

1 Tomcat 和 IntelliJ的配置与连接

这一步之前的Java EE的时候已经做过了,但是没有系统总结过,就重新做一下吧。

首先Tomcat 9 下载压缩包,然后配置JAVA_HOME和JRE_HOME环境变量,之后是CATALINA_HOME变量,然后到Tomcat的bin目录下启动tomcat,到localhost:8080检测一下就完毕了。

如果不动Tomcat的高深配置,一般就这么简单。

之后是在Intellij里配置Tomcat。其实Intellij并不是从项目中集成Tomcat,而是将Tomcat作为一个构建要素。只要符合目录结构的项目,引入了Tomcat,都可以部署成Web应用。

由于Spring并不一定需要符合Web要素的目录,而是在任何项目中都能够作为一个库导入,所以目前可以采用先创建Web项目,配置Tomcat服务器,然后安装Spring作为第三方库的方式。具体步骤如下:

  1. New Project–>左侧选择Java Enterprise,旁边选择JDK8,然后添加Tomcat服务器,选择安装路径即可,然后勾上下边选项中的Web Application,创建一个Web应用。
  2. 在右上角的Tomcat运行的地方选择配置,可以自定义项目的路径名称和tomcat服务器启动时候的一系列参数。此时可以配置一下Web.xml,测试一个最简单的Web应用是否工作正常。
  3. 导入Spring有两种方式,一种就是像新建一个普通项目的时候一样,创建一个lib目录,然后将Spring的Jar文件放入其中,然后设置成库文件即可,也可以将其配置成External库,通过在Program Structure中添加库。我采用的是直接创建lib目录的方式。Spring自身也可以当成一个普通库使用,所以Web项目里也可以引入。
  4. Spring的下载地址从 spring.io进去显得不够直观,这里放上直接下载地址:http://repo.spring.io/release/org/springframework/spring/,进去之后选择下载和教学视频中一样的5.0.2版本,和很多库一样,有包,文档和schema三个zip文件,只需要-dist.zip即可,然而文档也是个好东西。

下边就来看看Spring容器最基础的两个理论:控制反转和依赖注入。

2 控制反转 IOC

假设我们的应用MyApp.java需要使用如下的三个类/接口(依赖于这三个类):

  1. Coach.java 一个接口,表示了所有的教练需要实现的共通方法
  2. BaseballCoach.java
  3. TrackCoach.java

传统的做法是先创建一个MyApp.java类,BaseballCoach和TrackCoach都继承Coach接口

public interface Coach {
    public String getDailyWorkout();
}

public class BaseballCoach implements Coach {
    @Override
    public String getDailyWorkout() {
        return "I am BaseballCoach";
    }
}

public class TrackCoach implements Coach {
    @Override
    public String getDailyWorkout() {
        return "I am TrackCoach";
    }
}

public class MyApp {
    public static void main(String[] args) {
        //创建依赖的对象
        BaseballCoach theCoach = new BaseballCoach();
        //调用对象的方法
        System.out.println(theCoach.getDailyWorkout());
    }
}

那么现在MyApp如果需要与TrackCoach进行交互,则不得不修改MyApp的源代码,将其中的BaseballCoach对象全部替换成TrackCoach对象,因为这里写死了一个具体的对象。

如果对象的依赖发生改变,则需要手工修改全部使用该对象的代码,这样导致MyApp只能和一个具体的实现绑定在一起。

在Spring中的做法不是修改源代码,而是修改配置,这样就增加了非常高的灵活性,也解耦了具体绑定。

Spring容器使用Bean的整体流程是:配置Bean–根据配置创建容器–通过容器获得并使用Bean,为此,需要先了解如何配置Bean。

所谓配置,就是一个配置文件,可以采取各种形式。Spring的具体配置形式有三种,一种是XML配置,一种是注解方式,还有一种是Java源代码模式,Spring in Action 4把注解方式也叫做隐式发现+自动装配机制。根据Spring in Action 4,最优先应当使用隐式发现+自动装配,其次是Java源代码配置方式,只有需要使用XML命名空间而无法用Java配置方式时,才需要用到XML,这三种模式都会学习到。

2.1 XML配置Bean

XML方式下,先要在Spring的XML文件中配置Bean。Spring的XML配置文件一般叫做applicationContext.xml,而在Spring中提到applicationContext,指的就是Spring的核心容器。

applicationContext.xml文件长这个样子:

<?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:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">

	
    <!-- Define your beans here -->

    <!-- define the dependency -->


</beans>

去掉XML的头部和beans标签中的一系列信息,我们需要做的是定义Bean和定义Bean的依赖关系。

在beans标签内部采用bean标签,配合一些属性来配置一个bean,来配置一下BaseballCoach这个类:

<bean id="myCoach" class="iocdemo1.BaseballCoach"></bean> 

bean标签的id属性表示别名,可以理解为bean的变量名,class是需要加载的类的全称,用于通过反射寻找类并且加载。

配置好了第一个bean,现在来创建Spring容器,再次注意,ApplicationContext在Spring里指的就是Spring核心容器。

这个核心容器有几个根据不同的配置文件来加载的实现方式:

  1. ClassPathXmlApplicationContext
  2. AnnotationConfigApplicationContext
  3. GenericWebApplicationContext
  4. Others..

这些都会慢慢学到,现在把applicationContext.xml放到src的目录之下,然后使用ClassPathXmlApplicationContext,即直接从类目录下加载配置文件的方式创建容器,然后使用容器的getBean方式来获取Bean,修改MyApp.java的代码:

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyApp {
    public static void main(String[] args) {
        //创建容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //获取Bean
        Coach theCoach = context.getBean("myCoach", Coach.class);
        //尝试使用Bean
        System.out.println(theCoach.getDailyWorkout());
        //关闭容器
        context.close();
    }
}

在创建容器的时候,由于applicationContext.xml就在src目录下,因此无需加具体路径。

获取bean的时候,采用了重载的方法,第一个参数是字符串形式的bean的名称,也就是bean的id属性值,第二个参数是所实现的接口的class类文件,所以获取Bean的类型也采用了Coach接口类型。

最后就是直接以多态的方式调用方法,来看看程序执行的结果:

三月 04, 2019 10:55:58 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3d71d552: startup date [Mon Mar 04 22:55:58 CST 2019]; root of context hierarchy
三月 04, 2019 10:55:58 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [applicationContext.xml]
I am BaseballCoach

一开始是Spring框架的启动和加载信息(之后省略),最后发现成功的调用了BaseballCoach的方法,然而这还不算真正完成了第一个Spring程序。如果此时我们需要使用TrackCoach,还需要修改MyApp.java吗?

完全不需要,只需在applicationContext.xml中修改一下bean的内容:

<bean id="myCoach" class="iocdemo1.TrackCoach"></bean>

再次执行MyApp,就发现结果变成了:

I am TrackCoach

可见,Spring IOC神奇的无需修改MyApp.java代码,仅通过修改配置,就能够控制MyApp依赖于哪个对象,在这个简短的程序里,无论是接口还是两个实现都可以单独进行测试,而MyApp.java也可以通过不同的配置进行测试,无需修改源代码,这就是控制反转的例子,也是一般学习Spring的第一个程序。初次体验Bean之后,下一个就是依赖注入了。

3 依赖注入 DI

这里要先说说依赖是什么意思,所谓依赖,就是能够帮助当前对象完成工作的对象。而依赖注入可以看原始的MyApp.java:

public class MyApp {
    public static void main(String[] args) {
        //创建依赖的对象
        BaseballCoach theCoach = new BaseballCoach();
        //调用对象的方法
        System.out.println(theCoach.getDailyWorkout());
    }
}

可见MyApp.java执行的时候,必须依靠BaseballCoach才能完成我们想做的工作,则BaseballCoach就是帮助MyApp完成工作的对象,也可以说MyApp依赖于BaseballCoach对象。

在上边初步感受IOC的过程中,我们想到了另外一个提高灵活性的办法。既然要求不能把对象写死,那我是不是按照结构化编程思维的提取,把对象作为一个变量,当成参数传给MyApp的构造器,只要规定了变量类型是Coach接口,那么我的MyApp.java就可以通过实例化来接受所有实现Coach接口的对象。

于是我们新建一个MyApp2.java如下:

public class MyApp2 {
    Coach myCoach;

    MyApp2(Coach myCoach) {
        this.myCoach = myCoach;
    }

    public void doDailyWork() {
        System.out.println(myCoach.getDailyWorkout());
    }
}

这样在每次实例化的时候确实提高了灵活性,我们把依赖的对象通过构造器注入给了MyApp2。当然,这样还是耦合程度太高,因为写死了接口的类型,也写死了调用的方法,虽然灵活性看起来是提高了,但没有根本性的变化。

Spring又站出来了,依然通过配置文件而不是实际编写构造器注入代码,就可以完成注入,还可以规定调用什么方法。下边就来看一看Spring的依赖注入。

Spring的注入有很多,最常用的是两种:构造器注入和Setter注入,先来看构造器注入。

3.1 构造器注入 Constructor Injection

构造器注入的步骤如下:

  1. 创建依赖的接口和类
  2. 在Spring文件中配置依赖注入
  3. 在被注入的类中创建构造器

这里的第二步和第三步其实可以颠倒,或者说是紧密联系在一起,因为一般IDE会根据配置文件去进行自动创建构造器工作。

第一步,创建需要的接口和类。

现在我们的教练需要一个助手来提供DailyFortunes服务,可以说教练依赖助手完成这项工作,于是我们先来定义一个提供FortuneService接口和具体实现类HappyFortuneService。

public interface FortuneService {
    String getFortune();
}

public class HappyFortuneService implements FortuneService {
    public String getFortune() {
        return "Today is your lucky day!";
    }
}

第二步,配置依赖注入

给BaseballCoach增加了一个域private FortuneService fortuneService; 然后创建一个带参构造器,传入依赖对象。可以想象,如果一个依赖关系有多个层次,修改源代码是一场灾难。现在换成用Spring来配置依赖关系,让BaseballCoach类保持原状不做任何修改,修改applicationContext.xml:

<bean id="myCoach" class="iocdemo1.BaseballCoach">
    <constructor-arg ref="myFortuneService"/>
</bean>
    
<bean id="myFortuneService" class="iocdemo1.HappyFortuneService"></bean>

这里首先要把myFortuneService类也定义为一个Bean,起好id名称,然后在之前配置BaseballCoach的bean标签内部,加上一个constructor-arg自闭合标签,其中的ref属性就是myFortuneService的id名称,这表示把这个Bean当成构造器参数传递给BaseballCoach的构造器。

第三步,添加构造器

在Intellij中,如果在bean标签内部按Alt+Insert,在弹出的自动完成菜单中选择Constructor Dependency,会让你选择一个依赖类,然后会自动配置好XML和在类中生成构造器。

不使用这种方法的话,则需要手工在BaseballCoach中添加一个构造器方法:public BaseballCoach(FortuneService fortuneService) {}

在这三步完成之后,我们还需要在Coach接口中定义一个新方法,用来使用FortuneService:

public interface Coach {
    String getDailyWorkout();
    //新定义的方法
    String getDailyFortune();
}

然后让BaseballCoach和TrackCoach都实现该方法,同时用接口类型配置好构造器和域:以BaseballCoach为例:

public class BaseballCoach implements Coach {
    // 定义接受注入对象的变量
    private FortuneService fortuneService;
    // 定义接受注入的构造器
    public BaseballCoach(FortuneService fortuneService) {
        this.fortuneService = fortuneService;
    }

    @Override
    public String getDailyWorkout() {
        return "I am BaseballCoach";
    }
    // 重写新方法
    @Override
    public String getDailyFortune() {
        return "I am BaseballCoach " + fortuneService.getFortune();
    }
}

然后修改MyApp.java来调用新的方法:

public class MyApp {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Coach theCoach = context.getBean("myCoach", Coach.class);
        System.out.println(theCoach.getDailyWorkout());
        //添加一行代码调用Coach的新方法
        System.out.println(theCoach.getDailyFortune());
        context.close();
    }
}

运行之后可以看到新方法调用成功。用同样的方法,把TrackCoach也进行改造:

public class TrackCoach implements Coach {

    private FortuneService fortuneService;

    public TrackCoach(FortuneService fortuneService) {
        this.fortuneService = fortuneService;
    }

    @Override
    public String getDailyWorkout() {
        return "I am TrackCoach";
    }

    @Override
    public String getDailyFortune() {
        return "I am TrackCoach " + fortuneService.getFortune();
    }
}

如果此时把id为myCoach的Bean配置成TrackCoach,可以看到两个方法均通过TrackCoach调用。也就是说,我们可以任意装配两个Bean之间的依赖关系,只要他们符合组装条件。

Spring的构造器依赖注入,配合Java的接口规范,就可以让Bean自动装配。

3.2 Setter注入 Setter Injection

看过了构造器依赖注入,Setter注入可想而知,就是让Spring调用Setter方法来注入了,Setter注入分两步:

  1. 在类中创建setter方法接收注入
  2. 在Spring配置文件中配置依赖注入

与构造方法不同,换成了使用setter方法,XML文件的配置也需要做一点变更。我们这次再新来一个教练叫做CricketCoach,然后用setter注入的方式给这个教练添加FortuneService。

第一步:先创建一个CricketCoach类,实现Coach接口,加上依赖变量和对应的setter方法:

public class CricketCoach implements Coach {

    // 定义好接受依赖对象的变量
    private FortuneService fortuneService;

    // 添加setter方法
    public void setFortuneService(FortuneService fortuneService) {
        this.fortuneService = fortuneService;
    }

    @Override
    public String getDailyWorkout() {
        return "I am CricketCoach";
    }

    @Override
    public String getDailyFortune() {
        return "I am CricketCoach " + fortuneService.getFortune();
    }
}

第二步自然就是配置注入,先把CricketCoach也做成一个Bean,然后在其中配置属性和对应的对象:

<bean id="myCricketCoach" class="iocdemo1.CricketCoach">
    <property name="fortuneService" ref="myFortuneService"/>
</bean>

这里要注意的是使用了property标签,其中的name一定要是和类中定义的变量名称一样(setter方法则必须按照变量名前边加set+驼峰命名),而ref属性中就是bean的名称,与对应的bean的id一样。

这么配置好之后,实际上Spring就会自动创建一个myFoutuneService的对象,然后通过setter方法将其赋给myCricketCoach中的fortuneService属性。

我们在MyApp.java里获取myCricketCoach名称的Bean,其他代码不变,可以发现,写了一个新类和配置了setter依赖,原来的应用依然可以执行,而且依赖对象变成了新的CricketCoach对象,这样就完成了装配实现Coach接口和实现FortuneService接口的Bean的组装。

除了myCricketCoach之外,另外一个Bean也是可以获取对应的对象的,最后我们把MyApp.java的代码修改成如下,分别获取两个Bean并且操作:

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyApp {
    public static void main(String[] args) {
        //创建容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //获取Coach Bean
        Coach theCoach = context.getBean("myCricketCoach", Coach.class);
        //使用Bean
        System.out.println(theCoach.getDailyWorkout());
        System.out.println(theCoach.getDailyFortune());

        //获取FortuneService Bean
        FortuneService fortuneService = context.getBean("myFortuneService", FortuneService.class);
        System.out.println(fortuneService.getFortune());

        //关闭容器
        context.close();
    }
}

我们这里更改了getBean方法中的Bean id名称,实际上肯定可以想到,如果把原来的XML中的名为myCoach的Bean注释掉,新的CricketCoach Bean的id也叫myCoach,那么MyApp.java一个字都不用修改,只要修改配置文件就行了。

是的,这就是Spring通过依赖注入实现的解耦方式,第一篇文章里我提到Web层里需要传入一个Service层对象,Service层里需要传入一个DAO对象,如果需要更换Service和DAO对象,涉及的具体代码量非常大。现在换成Spring依赖注入之后,只需要做好架构,让Service和DAO都实现某个接口,需要切换的时候,只需要关闭和打开配置就行了,完全不需要进行任何修改源代码的工作。

3.3 Setter注入字面量

刚才我们的关注点在于注入依赖对象,而Setter方法还经常用来修改成员变量的值,所以Spring也提供了注入字面量的方法。注入字面量也是两个步骤:在类中创建setter方法和对应的成员变量,然后配置注入关系。

现在我们给CricketCoach注入电子邮件和team两个字符串常量,很显然,先要修改CricketCoach来增加变量和setter方法:

public class CricketCoach implements Coach {

    private FortuneService fortuneService;
    // 定义好接受依赖对象的变量
    private String emailAddress;
    private String team;

    @Override
    public String getDailyWorkout() {
        return "I am CricketCoach";
    }

    @Override
    public String getDailyFortune() {
        return "I am CricketCoach " + fortuneService.getFortune();
    }

    public void setFortuneService(FortuneService fortuneService) {
        this.fortuneService = fortuneService;
    }

    //新的setter方法
    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }
    //新的setter方法
    public void setTeam(String team) {
        this.team = team;
    }

    //getter方法
    public String getEmailAddress() {
        return emailAddress;
    }
    //getter方法
    public String getTeam() {
        return team;
    }

}

然后在myCricketCoach的Bean中,继续添加一系列property标签:

<bean id="myCricketCoach" class="iocdemo1.CricketCoach">
    <property name="fortuneService" ref="myFortuneService"/>
    <property name="emailAddress" value="conyli@vip.sina.com"/>
    <property name="team" value="Shanghai World Foreign Language Primary School"/>
</bean>

这里要注意的是,name依然是类的属性名,但是配置为字面量的时候,不再是ref属性表示引用,而是用value属性表示值。

在MyApp.java里修改一下getBean方法那行,因为使用的不是接口方法,需要将类型变为具体的CricketCoach类型,然后加上两行来测试一下:

public class MyApp {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        //获取Coach Bean
        CricketCoach theCoach = context.getBean("myCricketCoach", CricketCoach.class);

        //使用Bean的get方法检测是否设置成功
        System.out.println(theCoach.getTeam());
        System.out.println(theCoach.getEmailAddress());

        //关闭容器
        context.close();
    }
}

可以发现成功的获取到了设置在Spring配置文件中的值,字面量也是重在解耦,将属性与配置的值相分离,尽可能减少在代码中硬编码各种值。除了在配置文件中注入之外,还有一种常用的方法是从Properties文件中注入。

3.3.1 通过Properties文件注入字面量

分为如下步骤:

  1. 创建Properties文件
  2. 通过Spring配置文件加载Properties文件
  3. 从文件中配置属性

Properties文件是Java开发中经常使用的文件,可以通过Java集合中的Properties集合操作。其本身是一个文本文件,用等号隔开键和值。

第一步:创建一个Properties 文件叫做sport.properties,将其放在和xml文件一起都直接放在src目录下,:

foo.email=conyli@vip.sina.com
foo.team=SFWLPS

第二步:用Properties类来加载这个文件,而是通过Spring来加载,这里要使用beans标签内部,和bean标签同级的context标签来加载:

<?xml version="1.0" encoding="UTF-8"?>
    <beans......>

    <context:property-placeholder location="classpath:sport.properties"/>
    ......

</beans>

这个配置表示使用上下文的加载文件配置,location表示以classpath来加载sport.properties,所以前边就将sport.properties和xml文件直接放在src目录之下。

第三步:在文件中通过Spring表达式来配置属性,修改XML配置文件中两行配置字面量为如下:

<property name="emailAddress" value="${foo.email}"/>
<property name="team" value="${foo.team}"/>

这里使用到了Spring表达式,${变量名称}来从上边context获取的Properties文件中按照变量来取值,这个取值表达式需要放在引号中。

配置好了之后,可以发现,无需修改源代码,需要进行变更,大量的工作都可以在配置文件中进行,无论是修改属性,还是修改依赖,都可以。当然,这里只实现了CricketCoach的属性字面量注入,也可以把其他的一并实现。

第一个采用了IOC和依赖注入的Spring程序就编写完了。