5. [Cours]:Spring框架入门
关键词:多层架构、Spring、依赖注入。
Spring 于 2004 年首次作为对象容器出现。此后,它已发展出多个分支:Spring MVC、Spring Data、Spring Batch 等 [http://spring.io]。 本章仅介绍对象容器。以下是几个关键要点:
- 一个应用程序包含多个类,其中某些类共享必须是唯一的对象(单例)。Spring 负责创建和管理这些单例;
- Spring 将这些单例放置在一个称为“上下文”的结构中;
- 类可以通过名称、类型或两者兼而有之的方式向 Spring 请求,从而访问应用程序中的单例;
- Spring 会创建单例并管理其可能存在的依赖关系:一个单例实际上可能引用一个或多个其他单例。当 Spring 创建单例时,它也会同时创建其依赖关系;
- 当基于 Spring 的应用程序启动时,它可请求 Spring 创建应用程序中的所有单例。这些单例随后将在 Spring 上下文中可用;
- Spring 简化了分层架构和基于接口的编程。在简单情况下,每一层都由一个单例实现,并实现一个接口。如果应用程序使用的是各层的接口而非其实现类,那么便能构建出一种可扩展的架构,该架构凭借以下两个特性,允许在不改变其他层的情况下更换某一层级的实现:
- 应用程序通过层名称获取该层的引用。Spring 向其提供实现该层的类的引用;
- 应用程序将此引用视为该层的接口引用,而非具体类的引用;
单例的声明可通过以下三种方式实现,且可以混合使用:
- 在 XML 文件中,
- 在专门的配置类中;
- 通过注解在任意类中声明;
下面我们将介绍三个配置示例:
- [exemple-01]:在单个文件 XML 中进行集中配置;
- [exemple-02]:配置集中于单个 Java 类中;
- [exemple-03]:配置分布在多个 Java 类中;
最后一个示例 [exemple-04] 关注分层架构的 Spring 配置。这是最重要的示例,本文后续将不断引用它来配置各种架构。
这四个示例为后续内容奠定了基础:
- Spring配置与依赖注入;
- 使用 Maven 管理项目依赖;
- 使用 JUnit 测试项目;
5.1. Support
![]() | ![]() |
文件夹 [support / chap-05] 包含本章的 Eclipse 项目。
5.2. Exemple-01
5.2.1. Eclipse 项目
![]() |
5.2.2. 类 [Personne]
![]() |
package istia.st.spring.core;
public class Personne {
// 字段
private String nom;
private String prenom;
private int age;
// 构造函数
public Personne() {
}
public Personne(String nom, String prénom, int âge) {
this.nom = nom;
this.prenom = prénom;
this.age = âge;
}
// toString
public String toString() {
return String.format("Personne[%s, %s,%d]", prenom, nom, age);
}
// 获取器和设置器
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
this.prenom = prenom;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
注:getter 和 setter 可以按以下方式自动生成 [1-2]:
![]() |
5.2.3. 类 [Appartement]
![]() |
package istia.st.spring.core;
public class Appartement {
// 字段
private Personne proprietaire;
private int surface;
// getter 和 setter
public Personne getProprietaire() {
return proprietaire;
}
public void setProprietaire(Personne proprietaire) {
this.proprietaire = proprietaire;
}
public int getSurface() {
return surface;
}
public void setSurface(int surface) {
this.surface = surface;
}
// toString
public String toString() {
return String.format("Appartement[%s, %s]", proprietaire, surface);
}
}
注:该类没有显式构造函数。在这种情况下,默认情况下始终存在一个不带参数的构造函数,该构造函数不执行任何操作。当创建构造函数时,该默认构造函数将不再隐式存在。因此必须显式定义它:
5.2.4. Spring配置文件
![]() |
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/utilhttp://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 人员 01 -->
<bean id="personne_01" class="istia.st.spring.core.Personne">
<constructor-arg index="0" value="dubois" />
<constructor-arg index="1" value="paul" />
<constructor-arg index="2" value="34" />
</bean>
<!-- 人员 02 -->
<bean id="personne_02" class="istia.st.spring.core.Personne">
<property name="nom" value="martin" />
<property name="prenom" value="micheline" />
<property name="age" value="18" />
</bean>
<!-- 人员列表 -->
<util:list id="club">
<ref bean="personne_01" />
<ref bean="personne_02" />
</util:list>
<!-- 一套公寓 -->
<bean id="appartement" class="istia.st.spring.core.Appartement">
<property name="surface" value="100" />
<property name="proprietaire" ref="personne_01" />
</bean>
</beans>
- 第 2、27 行:单例在 <beans> 标签内定义;
- 第 6-10 行:每个单例由一个 <bean> 标签定义;
- 第 6 行:[id] 是单例的标识符。[class] 是待实例化类的全名;
- 第 7-9 行:传递给 [Personne] 类构造函数的三个参数;
- 第12-16行:首先通过默认构造函数[new Personne()]创建类[Personne]。 然后,对于每个 [property] 标签,都会使用该类的 setter 方法。例如第 13 行,将执行 [setNom("martin")] 方法。因此必须存在 [setNom] 方法。这一点很重要,请务必记住;
- 第 18-21 行:<util:list> 标签用于定义一个作为列表的单例;
- 第 19 行:引用了第 6 行定义的单例 [personne_01]。这被称为依赖注入。有两个属性可用于初始化单例的字段:
- [value]:用于将基本类型值(字符串、数字、日期等)赋值给字段,
- [ref]:用于将 Spring 对象的引用赋值给字段;
注:Spring 配置文件可通过以下方式生成 [1-4]:
![]() |
5.2.5. 可执行类
![]() |
package istia.st.spring.core;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Demo01 {
@SuppressWarnings({ "unchecked", "resource" })
public static void main(String[] args) {
// 获取 Spring 上下文
ApplicationContext ctx = new ClassPathXmlApplicationContext("config-01.xml");
// 获取 Bean
Personne p01 = ctx.getBean("personne_01", Personne.class);
Personne p02 = ctx.getBean("personne_02", Personne.class);
List<Personne> club = ctx.getBean("club", new ArrayList<Personne>().getClass());
Appartement appart01 = ctx.getBean(Appartement.class);
// 显示它们
System.out.println("personnes--------");
System.out.println(p01);
System.out.println(p02);
System.out.println("club--------");
for (Personne p : club) {
System.out.println(p);
}
System.out.println("appartement--------");
System.out.println(appart01);
// 获取到的 Bean 是单例
// 可以多次请求,始终获取到同一个 Bean
Personne p01b = ctx.getBean("personne_01", Personne.class);
System.out.println(String.format("beans [p01,p01b] identiques ? %s", p01b == p01));
}
}
- 第 14 行:创建 Spring 上下文。随后,[config-01.xml] 文件中定义的所有单例都会被实例化;
- 第16行:请求获取由[personne_01]标识的、类型为[Personne]的单例的引用。 该第二个参数是可选的,但若不指定,则会收到一个 [Object] 类型的引用,该引用随后需要转换为 [Personne] 类型;
- 第 19 行:由于 [Appartement] 类型仅有一个单例,因此不使用 Bean 的名称,而仅使用其类型;
- 第 18 行:同时使用了所需单例的标识符和类型。由于只有一个类型为 [new ArrayList<Personne>().getClass()] 的单例,因此标识符是多余的;
- 第 32-33 行:表明如果多次请求同一个单例,确实总是获得相同的引用,从而证明我们确实在处理一个单例。这一点很重要,需要理解;
注:可按以下方式生成可执行类 [1-6]:
![]() |
![]() |
- 只有勾选了 [6],生成的类才会包含一个静态方法 [main],从而使其可执行;
5.2.6. 项目的依赖项
![]() |
- Spring 依赖项:[spring-core, spring-beans, spring-context, spring-expression, commons-logging];
依赖项按以下方式添加到项目中:
![]() |
- 在 [1] 中:右键单击项目 / [Build Path] / [Configure Build Path];
![]() |
- 转换为 [2]:若待添加的 JARs 位于项目文件夹内,则为 [Add JARs]。否则为 [Add External JARs];
![]() |
- 在 [3] 中,选择要添加到项目 ClassPath 中的 JARs(此处它们位于项目内的 [lib] 文件夹中);
定义:项目的 [ClassPath] 是指 JVM(Java 虚拟机)在执行该项目时,为查找该项目引用的类而遍历的所有文件夹。 对于一个 Eclipse 项目,[ClassPath] 由以下元素组成:
- 项目的 [bin] 文件夹;
- 项目中的 [Build Path] 文件;
[bin] 文件夹是由编译 [src] 文件夹生成的。 因此,放置在 [src] 文件夹中的所有内容都会自动成为 [ClassPath] 的一部分(即使不是 .java 文件)。 因此,在上述项目中,位于 [src] 文件夹内的 Spring 配置文件 [config-01.xml],在项目运行时将作为 [Classpath] 的一部分。
5.2.7. 结果
5.3. Exemple-02
5.3.1. Eclipse 项目
![]() |
5.3.2. Spring 配置类
package istia.st.spring.core;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Config {
@Bean
public Personne personne_01() {
return new Personne("Paul", "Dubois", 34);
}
@Bean
public Personne personne_02() {
return new Personne("Martin", "Micheline", 18);
}
@Bean
public List<Personne> club(Personne personne_01, Personne personne_02) {
List<Personne> personnes = new ArrayList<Personne>();
personnes.add(personne_01);
personnes.add(personne_02);
return personnes;
}
@Bean
public Appartement appartement(Personne personne_01) {
Appartement appartement = new Appartement();
appartement.setSurface(200);
appartement.setPropriétaire(personne_01);
return appartement;
}
}
- 第 9 行:注解 [@Configuration] 是 Spring 注解。它表示被注解的类定义了单例。这些单例是通过注解 [@Bean] 定义的。 Spring 将执行所有带有 [@Bean] 注解的方法。这些方法用于创建应用程序的单例;
- 第 12-15 行:定义了一个由 [personne_01] 标识的单例,即该方法的名称。
- 第 23 行:参数 [personne_01, personne_02] 采用单例的名称。Spring 将自动使用这些单例的引用对其进行初始化。这被称为参数注入;
这种配置单例的方式比使用 XML 文件的方式更为明确。实际上,我们自己重现了 Spring 基于 XML 文件所做的隐式操作。
5.3.3. 可执行类
![]() |
package istia.st.spring.core;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Demo02 {
@SuppressWarnings({ "unchecked", "resource" })
public static void main(String[] args) {
// 获取 Spring 上下文
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
// 获取 Bean
Personne p01 = ctx.getBean("personne_01", Personne.class);
Personne p02 = ctx.getBean("personne_02", Personne.class);
List<Personne> club = ctx.getBean("club", new ArrayList<Personne>().getClass());
Appartement appart01 = ctx.getBean(Appartement.class);
// 显示它们
System.out.println("personnes--------");
System.out.println(p01);
System.out.println(p02);
System.out.println("club--------");
for (Personne p : club) {
System.out.println(p);
}
System.out.println("appartement--------");
System.out.println(appart01);
// 获取到的 Bean 是单例
// 可以多次请求,始终获取到同一个 Bean
Personne p01b = ctx.getBean("personne_01", Personne.class);
System.out.println(String.format("beans [p01,p01b] identiques ? %s", p01b == p01));
}
}
- 第 13 行会实例化 [Config] 类中定义的所有 Bean;
- 其余代码保持不变;
5.3.4. 项目的依赖关系
![]() | ![]() |
依赖关系由以下文件 [pom.xml] 确定:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring.core</groupId>
<artifactId>spring-core-02</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-core-02</name>
<description>Introduction à Spring</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.3.RELEASE</version>
</dependency>
</dependencies>
<!-- 插件 -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
当使用那些自身依赖关系不明的 Java 库时,手动管理项目依赖关系会变得令人头疼。例如,负责管理数据库访问的 [Hibernate] 框架就有数十个依赖项。[Maven] 项目解决了这一问题。 只需指定所需的依赖项,系统便会自动在遍布网络的Maven仓库中进行搜索。如果所请求的依赖项本身还有其他依赖,这些依赖也会被自动下载。下载的依赖项将存储在本地仓库中。如果后续其他应用程序需要相同的依赖项,系统将不再重新下载,而是直接从本地仓库中获取。 一个依赖项由以下元素构成:
- 第 17 行:一个 <dependency> 标签;
- 第 18 行:一个 [groupId] 属性,通常用于标识创建该依赖项的公司;
- 第 19 行:[artifactId] 属性,用于标识该依赖项;
- 第 20 行:一个 [version] 属性,用于标识所需的版本;
项目生成过程本身将生成一个由第 4-8 行定义的 Maven 组件:
- 第 4-6 行:即我们刚刚描述的 [ groupId, artifactId,version] 属性;
- 第 7-8 行:为可选属性;
稍后我们将详细讨论第24-40行的作用。要将一个普通的Eclipse项目转换为Maven项目,需要做两件事:
- 创建前述的 [pom.xml] 文件;
- 声明该项目现为 Maven 项目 [1-4]:
![]() |
Maven 项目的图标为 M [4]。字母 S 表示该项目包含 Spring 组件。不建议(如我们刚才所做的那样)将 Eclipse 项目转换为 Maven 项目,因为这样会导致项目结构不符合 Maven 项目的预期规范,有时可能会引发意想不到的问题。
5.3.5. 生成项目的 Maven 工件
我们称项目中的 Maven 工件为文件 [pom.xml] 第 4-6 行所定义的元素:
<groupId>istia.st.spring.core</groupId>
<artifactId>spring-core-02</artifactId>
<version>0.0.1-SNAPSHOT</version>
要生成此工件,文件 [pom.xml] 中必须包含以下第 3-7 行:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
这些行定义了能够生成该项目工件的 Maven 插件。随后按以下步骤操作:
![]() |
生成的工件将存入本地 Maven 仓库。该仓库的位置可在 Eclipse 配置中找到:
![]() |
此时可以验证 Maven 构建产物是否安装正确:
![]() |
现在,另一个本地 Maven 项目可以使用该归档文件。
5.4. Exemple-03
5.4.1. Eclipse 项目
这次我们创建一个名为 [1-8] 的 Maven 项目:
![]() |
![]() |
- 在 [3b] 中:指定一个用于生成项目的空文件夹;
![]() |
- 在 [4] 中:项目所属的 Maven 组标识符;
- 在 [5] 中:生成的 Maven 工件名称:
- 在 [6] 中:其版本号;
- 在 [7] 中:其打包模式(还包括 war、ear、apk 等);
- 在 [8] 中:由此创建的项目;
Maven 项目默认具有明确的目录结构:
- [src / main / java]:项目的源代码。这些源代码编译后的产物将存放在项目的 [target/classes] 文件夹中;
- [src / main / resources]:必须位于项目类路径中但并非 Java 源代码的资源。这些资源将原样复制到项目的 [target/classes] 文件夹中;
- [src / test / java]:项目测试的源代码。这些源代码编译后的产物将放入项目目录下的 [target/test-classes] 文件夹中。这些内容不会被纳入项目的 Maven 归档中;
- [src / test / resources]:测试所需且必须位于项目类路径中(Classpath)的资源,但并非 Java 源代码。这些资源将原样复制到项目的 [target/test-classes] 文件夹中;
我们按以下方式完善该项目:
![]() |
5.4.2. Maven配置
系统默认会生成一个名为 [pom.xml] 的文件。我们将对其进行如下修改:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring.core</groupId>
<artifactId>spring-core-03</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-core-03</name>
<description>Introduction à Spring</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<!-- 父级 Maven 项目 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- Spring 上下文 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
<!-- 插件 -->
<build>
<plugins>
<!-- 用于生成包含其依赖项的项目归档 -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<!-- 用于将项目工件安装到本地 Maven 仓库 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- 第 11 行:项目采用 UTF-8 编码;
- 第 12 行:使用 JDK 1.8 编译该项目;
- 第 16-20 行:对于使用 Spring 库的项目,建议使用名为 [spring-boot-starter-parent] 的父 Maven 项目。该项目定义了各种 Spring 库及其依赖项的版本。这样就无需在依赖项定义中再次指定这些版本。 因此第24-27行无需指定[spring-context]的具体版本。它将采用父项目[spring-boot-starter-parent]中定义的版本。这种方法可避免因依赖项之间版本不兼容而产生的问题。父项目中定义的依赖项彼此兼容;
- 第29-32行:Spring通过日志库在控制台输出大量信息。此处导入了该日志库;
- 第 40-47 行:一个 Maven 插件,我们稍后将详细讨论;
- 第 50-52 行:用于生成项目 Maven 构建产物的插件;
5.4.3. Spring 的配置类
![]() |
[Config] 类如下:
package istia.st.spring.core.config;
import istia.st.spring.core.entities.Personne;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan({ "spring.core.entities" })
public class Config {
@Bean
public Personne personne_01() {
return new Personne("Paul", "Dubois", 34);
}
@Bean
public Personne personne_02() {
return new Personne("Martin", "Micheline", 18);
}
@Bean
public List<Personne> club(Personne personne_01, Personne personne_02) {
List<Personne> personnes = new ArrayList<Personne>();
personnes.add(personne_01);
personnes.add(personne_02);
return personnes;
}
@Bean
public int mySurface() {
return 200;
}
}
- 这里包含一段已注释的代码,并有两处新内容:
- 第 13 行:表明在 [spring.core.entities] 包中还有其他 Bean 需要实例化,
- 第34-37行:一个名为[mySurface]的Bean;
5.4.4. 类 [Appartement]
![]() |
package spring.core.entities;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
public class Appartement {
// 由 Spring 注入的字段
@Autowired
@Qualifier("personne_01")
private Personne propriétaire;
@Autowired
@Qualifier("mySurface")
private int surface;
// getter 和 setter
public Personne getPropriétaire() {
return propriétaire;
}
public void setPropriétaire(Personne propriétaire) {
this.propriétaire = propriétaire;
}
public int getSurface() {
return surface;
}
public void setSurface(int surface) {
this.surface = surface;
}
// toString
public String toString() {
return String.format("Appartement[%s, %s]", propriétaire, surface);
}
}
- 第 7 行:注解 [@Component] 告知 Spring 该类是一个单例,框架需对其进行实例化并管理。这是因为在类 [Config] 中,我们写入了 [@ComponentScan({ "istia.st.spring.core.entities" })],因此该单例将被找到;
- 第 11 行:请求 Spring 将某个单例的引用注入该字段。该引用可通过两种方式定义:
- 通过其标识符(第 12、16 行),
- 通过其类型(如果只有一个具有该类型的单例);
5.4.5. 项目运行
运行项目后,控制台显示以下结果:
- 第1-3行:Spring会生成大量日志,多达数十行。这些日志对于调试无法正常运行的项目非常有用。当项目运行正常时,可通过以下方式减少日志输出:
![]() |
在文件夹 [src / main / resources] 中创建以下文件 [logback.xml]:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 编码器默认被分配为
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志级别控制 -->
<root level="info"> <!-- info、debug、warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- 第 12 行,设置日志级别。[debug] 级别非常详细,[info] 则要简略得多;
以下是使用 [level=info] 生成的结果:
日志仅剩一行。
5.4.6. 生成包含依赖项的项目归档
在上一个项目中创建的归档文件也可用于非 Maven 的 Eclipse 项目。某些项目会使用大量库,要确保不遗漏任何一个可能很棘手。这正是 Maven 大显身手之处,因为只需指定最高级别的依赖,其他较低级别的依赖就会自动添加到项目的类路径中。 当非Maven的Eclipse项目需要使用Maven项目的归档文件时,可以生成包含所有依赖关系的Maven项目工件(这与前一个项目的情况不同)。为此,文件[pom.xml]中必须包含以下第3至10行代码:
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
这些行定义了能够生成包含所有依赖项的项目工件的 Maven 插件。随后按以下方式操作 [1-6]:
![]() |
![]() |
- [4-6] 代表一个 Maven 运行配置;
- 在 [4] 中,输入任意名称;
- 在 [5] 中,指定项目文件夹;
- 在 [6] 中,填写 Maven 目标(goals):
- [clean]:删除项目的 [target] 文件夹;
- [compile]:编译项目。编译产物将放置在重新生成的 [target] 文件夹中;
- [assembly:single]:项目及其依赖项的类被放入位于 [target] 文件夹中的单个 jar 归档文件中;
执行后,得到以下结果:
![]() |
JAR 归档文件是一种压缩文件,因此可以使用解压工具打开。解压上述归档文件后,将得到以下目录结构:
- [8] 文件中包含项目依赖项的类;
- 在 [9] 中,是项目本身的类;
5.5. Exemple-04
5.5.1. 目标
本示例参考了文档 [Introduction à Spring IoC] 中展示的一个示例,该文档展示了 Spring 在多层架构配置中的作用。在原始文档中,该示例采用通过文件 XML 进行的 Spring 配置。 在此,我们将通过Java类和注解进行配置来处理该示例。
我们希望在此为以下架构配置一个 Spring 项目:
![]() |
每个层都提供了一个由两个类实现的接口。我们希望展示:借助 Spring,可以更改某一层级的实现,而对其他层级的代码完全不产生影响。
5.5.2. Eclipse 项目
5.5.2.1. Génération
我们创建一个新类型的项目:
![]() |
![]() |
- 在 [4] 中,输入 Eclipse 项目的名称;
- 在 [5] 中,选择 Maven 项目;
- 在 [6] 中,选择 Java 版本 >=1.7;
- 在 [7] 中,选择提供的 Spring Boot 版本;
- [8-11]中的信息是Maven信息;
- 在 [12] 中,可以选择一个或多个建议的依赖项。这将导致在 Maven 的 [pom.xml] 文件中集成若干依赖项;
![]() |
- 在 [13] 中,指定一个现有的空文件夹来存放该项目;
- 生成名为 [14] 的项目。接下来我们将对其组成部分进行详细解析;
该项目是一个由以下 [pom.xml] 文件配置的 Maven 项目:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring.core</groupId>
<artifactId>spring-core-04</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-core-04</name>
<description>Programmation par interfaces</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath/> <!-- 从仓库查找父类 -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>demo.SpringCore04Application</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 第 6-12 行:包含在项目创建向导中输入的信息;
- 第 14-19 行:父 Maven 项目,其中定义了若干库及其版本。如果其中某个库是该项目的依赖项,则会在 [pom.xml] 文件中列出该库,但不包含其版本;
- 第 23 行:仅当计划生成项目的可执行归档文件时才使用此行。否则该行将被忽略;
- 第 28-31 行:Spring Boot 项目的最低依赖。需注意,我们在复选框中未选择任何依赖;
- 第 33-37 行:管理与 Spring 集成的单元测试所需的依赖项 JUnit [http://junit.org/]。第 36 行表明该依赖项仅用于测试。因此,它不会被包含在项目归档中;
- 第 42-45 行:用于生成项目 Maven 构建产物的插件;
该文件引入的依赖项列表如下:[1]:
![]() |
我们将看到,这些依赖对于我们此处的操作已足够。
5.5.2.2. 可执行类
![]() |
可执行类 [SpringCore04Application] [[2] 如下所示:
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringCore04Application {
public static void main(String[] args) {
SpringApplication.run(SpringCore04Application.class, args);
}
}
- 第6行,注解 [@SpringBootApplication] 是三个注解 [@Configuration, @EnableAutoConfiguration, @ComponentScan] 的简写,这意味着:
- 类 [SpringCore04Application] 是一个 Spring 配置类;
- 要求 Spring Boot 根据项目类路径(此处即 Maven 依赖项)中找到的类进行配置;
- 检查当前目录(即 [SpringCore04Application] 类所在的目录),以查找其中可能存在的其他 Spring 组件;
- 第10行:执行静态方法[SpringApplication.run]。其第一个参数是一个Spring配置类,此处为类[SpringCore04Application]。其第二个参数是传递给方法[main](第9行)的参数列表。 静态方法 [SpringApplication.run] 的作用是创建 Spring 上下文,即创建配置类中或通过注解 [@ComponentScan] 扫描到的文件夹中找到的各个 Bean。 此处的 [main] 方法仅此而已。为了使其功能更丰富,我们将按以下方式对其进行改造:
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class SpringCore04Application {
public static void main(String[] args) {
// Spring 上下文实例化
ConfigurableApplicationContext context = SpringApplication.run(SpringCore04Application.class, args);
// 显示上下文
System.out.println("---------------- Liste des beans Spring");
for (String beanName : context.getBeanDefinitionNames()) {
System.out.println(beanName);
}
// 关闭上下文
context.close();
}
}
- 第 12 行:静态方法 [SpringApplication.run] 返回其构建的 Spring 上下文;
- 第 15-17 行:显示该上下文中所有 Bean 的名称;
可以通过以下方式运行该应用程序:[1-3]。常规方法 [Run As Java Application] 同样有效。
![]() |
将得到以下结果:
- 第 14-28 行:Spring 上下文中的 Bean。我们尚不清楚它们的作用。第 18 行出现了 Bean [springCore04Application],由于其注解 [@SpringBootApplication],该 Bean 自动成为 Spring Bean;
- 其余行是级别为 [INFO] 的 Spring 日志。正如我们之前所见,这些日志可以通过放置在项目 Classpath 中的 [logback.xml] 文件进行控制:
![]() |
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 编码器默认被分配类型
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志级别控制 -->
<root level="warn"> <!-- info、debug、warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
如果将上文第 12 行中的 [info] 替换为 [warn],则会得到以下结果:
日志已消失。仅显示级别为 [warn] 的消息,而此处并未出现该级别消息。
5.5.3. 架构各层的实现
![]() |
现在我们将实现上述架构的三个层:
![]() |
[DAO]层由[spring.core.dao]包实现。它提供以下[IDao]接口:
package spring.core.dao;
public interface IDao {
public int doSomethingInDaoLayer(int a, int b);
}
该接口有两个实现:[Dao1] 和 [Dao2]:
package spring.core.dao;
public class Dao1 implements IDao {
public int doSomethingInDaoLayer(int a, int b) {
return a+b;
}
}
package spring.core.dao;
public class Dao2 implements IDao {
public int doSomethingInDaoLayer(int a, int b) {
return a-b;
}
}
[métier] 层由 [spring.core.metier] 包实现。它提供以下 [IMetier] 接口:
package spring.core.metier;
public interface IMetier {
public int doSomethingInMetierLayer(int a, int b);
}
该接口有两个实现:[Metier1] 和 [Metier2]:
package spring.core.metier;
import spring.core.dao.IDao;
public class Metier1 implements IMetier {
private IDao dao;
public int doSomethingInMetierLayer(int a, int b) {
a++;
b++;
return dao.doSomethingInDaoLayer(a, b);
}
public void setDao(IDao dao) {
this.dao = dao;
}
}
package spring.core.metier;
import spring.core.dao.IDao;
public class Metier2 implements IMetier {
private IDao dao;
public int doSomethingInMetierLayer(int a, int b) {
a--;
b++;
return dao.doSomethingInDaoLayer(a, b);
}
public void setDao(IDao dao) {
this.dao = dao;
}
}
[UI] 层由 [spring.core.ui] 包实现。它具有以下 [IUi] 接口:
package spring.core.ui;
public interface IUi {
public int doSomethingInUiLayer(int a, int b);
}
该接口有两个实现:[Ui1] 和 [Ui2]:
package spring.core.ui;
import spring.core.metier.IMetier;
public class Ui1 implements IUi {
private IMetier metier;
public int doSomethingInUiLayer(int a, int b) {
a++;
b++;
return metier.doSomethingInMetierLayer(a, b);
}
public void setMetier(IMetier metier) {
this.metier = metier;
}
}
package spring.core.ui;
import spring.core.metier.IMetier;
public class Ui2 implements IUi {
private IMetier metier;
public int doSomethingInUiLayer(int a, int b) {
a--;
b++;
return metier.doSomethingInMetierLayer(a, b);
}
public void setMetier(IMetier metier) {
this.metier = metier;
}
}
5.5.4. Spring 项目配置
![]() |
配置类 [Config] 如下:
package spring.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.core.dao.Dao1;
import spring.core.dao.Dao2;
import spring.core.dao.IDao;
import spring.core.metier.IMetier;
import spring.core.metier.Metier1;
import spring.core.metier.Metier2;
import spring.core.ui.IUi;
import spring.core.ui.Ui1;
import spring.core.ui.Ui2;
@Configuration
public class Config {
// -------------- 实现 [Ui1, Metier1, Dao1]
@Bean
public IDao dao1() {
return new Dao1();
}
@Bean
public IMetier metier1(IDao dao1) {
Metier1 metier = new Metier1();
metier.setDao(dao1);
return metier;
}
@Bean
public IUi ui1(IMetier metier1) {
Ui1 ui = new Ui1();
ui.setMetier(metier1);
return ui;
}
// -------------- 实现 [Ui2, Metier2, Dao2]
@Bean
public IDao dao2() {
return new Dao2();
}
@Bean
public IMetier metier2(IDao dao2) {
Metier2 metier = new Metier2();
metier.setDao(dao2);
return metier;
}
@Bean
public IUi ui2(IMetier metier2) {
Ui2 ui = new Ui2();
ui.setMetier(metier2);
return ui;
}
}
- 第 20-23 行:名为 [dao1](方法名)的 Bean 是类 [Dao1](第 22 行)的实例,该类被视为接口 [IDao] (第21行)。因此,Bean [dao1]被视为接口的实例(虽然术语不准确,但意思可以理解),而非类的实例。这一点非常重要。所有其他Bean也将是接口的实例;
- 第 25-30 行:由类 [Metier1] 实现的接口 [IMetier] 的实例;
- 第 32-37 行:由类 [Ui1] 实现的接口 [IUi] 的实例;
- 第 20-37 行:使用 [Ui1, Metier1, Dao1] 实例实现 [UI, Metier, DAO] 层;
- 第 40-57 行:使用 [Ui2, Metier2, Dao2] 实例实现 [UI, Metier, DAO] 层;
5.5.5. [JUnitTest] 单元测试
![]() |
类 [JUnitTest] 被放置在 Maven 项目的 [src / test / java] 分支中。该分支中的元素不会被放入项目的最终归档中。其代码如下:
package spring.core.tests;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.core.config.Config;
import spring.core.dao.IDao;
import spring.core.metier.IMetier;
import spring.core.ui.IUi;
@SpringApplicationConfiguration(classes = { Config.class })
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTest {
...
}
- 第 16 行:注解 [@SpringApplicationConfiguration] 是 Spring Boot Test 项目(第 8 行)的注解。它由文件 [pom.xml] 的以下依赖引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
该注解的参数是用于构建测试所需 Spring 上下文的配置类列表。此处我们使用了前面已介绍的配置类 [Config];
- 第 17 行:注解 [@RunWith] 是注解 JUnit(第 5 行)的子注解。 其参数是负责执行测试的类,该类将取代JUnit框架的默认类。该类是一个Spring类(第9行)。它将使用测试类中存在的Spring注解;
完整的类如下
...
@SpringApplicationConfiguration(classes = { Config.class })
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTest {
// [UI] 层
@Autowired
@Qualifier("ui1")
private IUi ui1;
@Autowired
@Qualifier("ui2")
private IUi ui2;
// [métier] 层
@Autowired
@Qualifier("metier1")
private IMetier metier1;
@Autowired
@Qualifier("metier2")
private IMetier metier2;
// 图层 [dao]
@Autowired
@Qualifier("dao1")
private IDao dao1;
@Autowired
@Qualifier("dao2")
private IDao dao2;
@Test
public void testDao() {
Assert.assertEquals(30, dao1.doSomethingInDaoLayer(10, 20));
Assert.assertEquals(-10, dao2.doSomethingInDaoLayer(10, 20));
}
@Test
public void testMetier() {
Assert.assertEquals(32, metier1.doSomethingInMetierLayer(10, 20));
Assert.assertEquals(-12, metier2.doSomethingInMetierLayer(10, 20));
}
@Test
public void testUI() {
Assert.assertEquals(34, ui1.doSomethingInUiLayer(10, 20));
Assert.assertEquals(-14, ui2.doSomethingInUiLayer(10, 20));
}
}
- 第 8-10 行:注入(第 8 行)名为(第 9 行)[ui1] 的 Bean。请注意第 10 行注入的是接口实例而非类实例;
- 第21-32行:以相同方式注入[Config]类中定义的其他Bean;
- 第 34 行:注解 [@Test] 指定了一个在测试过程中需要执行的方法。其他可能的注解如下:
- [@BeforeClass]:在开始测试前执行的方法;
- [@AfterClass]:所有测试完成后需执行的方法;
- [@Before]:在每次测试前执行的方法;
- [@After]:每次测试结束后需执行的方法;
- 第 36 行:验证调用 [dao1.doSomethingInDaoLayer(10, 20)] 是否确实返回 30。按惯例,第一个参数为预期值,第二个参数为实际值;
- 第36行:测试接口[IDao]的实例[dao1];
- 第 37 行:测试接口 [IDao] 的实例 [dao2];
- 第42行:测试接口[IMetier]的实例[metier1];
- 第 43 行:测试接口 [IMetier] 的实例 [metier2];
- 第 48 行:测试接口 [IUi] 的实例 [ui1];
- 第 36 行:测试接口 [IUi] 的实例 [ui2];
测试方法中可使用的断言如下:
- assertEquals(表达式1, 表达式2):验证两个表达式的值是否相等。支持多种表达式类型(int、String、float、double、boolean、char、short)。如果两个表达式不相等,则抛出类型为 [AssertionFailedError ] 的异常,
- assertEquals(实数1, 实数2, delta):验证两个实数在误差 delta 范围内相等,即 c.a.d abs(实数1-实数2)<=delta。 例如,可以编写 assertEquals(实数1, 实数2, 1E-6) 来验证两个值在 10⁻⁶ 误差范围内相等,
- assertEquals(message, expression1, expression2) 和 assertEquals(message, 实数1, 实数2, delta) 是用于指定错误消息的变体,该消息将与 [AssertionFailedError] 类型的异常相关联,当 [assertEquals] 方法失败时会抛出该异常,
- assertNotNull(Object) 和 assertNotNull(message, Object):验证 Object 引用不为 null,
- assertNull(Object) 和 assertNull(message, Object):验证 Object 引用是否为 null,
- assertSame(Object1, Object2) 和 assertSame(message, Object1, Object2):验证 Object1 和 Object2 引用是否指向同一个对象,
- assertNotSame(Object1, Object2) 和 assertNotSame(message, Object1, Object2):验证引用 Object1 和 Object2 是否指向不同的对象;
要执行此测试,可按以下步骤操作:
![]() |
结果如下:
![]() |
此处所有测试均已通过。那么,这个示例说明了什么?它展示了Spring框架在分层架构配置方面带来的灵活性。我们可以通过简单的配置,决定使用[Ui1, Metier1, Dao1]还是[Ui2, Metier2, Dao2]的实现。 因此,在之前的 JUnit 测试中,如果仅保留 [ui1, metier1, dao1] 的 Bean 注入,则采用的是第一种架构。要更改架构,只需更换被注入的 Bean 即可。这一操作无需修改实现接口的各层代码。 这种编程方式被称为接口编程,因为我们不使用实现各层的类实例,而是使用其接口的实例。
5.6. Conclusion
- Spring 管理单例(仅有一个实例)对象。Spring 还管理那些每次向 Spring 请求实例时都会被实例化的对象。本文档也将介绍这种情况;
- 这些对象可以通过多种方式声明,且这些方式可以混合使用:
- 在 XML 文件中,
- 在带有 [@Configuration] 注解的 Java 类中,
- 使用任何带有 [@Component, @Service, ...] 注解的 Java 类;
- Spring 对象可通过 [@Autowired] 注解注入到另一个 Spring 对象中。这被称为依赖注入(DI:Dependency Injection);
- Spring 结合接口编程范式,在配置分层架构时表现得非常实用;














































