Skip to content

16. Spring MVC 入门

16.1. Spring MVC 在 Web 应用程序中的作用

让我们将 Spring MVC 置于 Web 应用程序的开发背景中。通常情况下,Web 应用程序会基于如下所示的多层架构构建:

  • [Web] 层是与 Web 应用程序用户进行交互的层。用户通过浏览器中显示的网页与 Web 应用程序进行交互。Spring MVC 仅位于这一层,且仅限于这一层;
  • [业务]层实现应用程序的业务逻辑,例如计算工资或生成发票。该层通过[Web]层获取用户数据,并通过[DAO]层获取来自DBMS的数据;
  • [DAO](数据访问对象)层、[ORM](对象关系映射器)层以及 JDBC 驱动程序负责管理对 DBMS 中数据的访问。[ORM] 层充当 [DAO] 层处理的对象与关系型数据库中表的行和列之间的桥梁。 一种名为 JPA(Java 持久化 API)的规范允许您忽略所使用的 ORM,只要它实现了这些规范。本教程中正是如此,因此从现在起我们将把 ORM 层称为 JPA 层;
  • 这些层的集成由 Spring 框架负责;

16.2. Spring MVC 开发模型

Spring MVC 通过以下方式实现 MVC(模型-视图-控制器)架构模式:

客户端请求的处理流程如下:

  1. 请求 - 请求的 URL 通常采用 http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... 的形式。[前端控制器] 会通过配置文件或 Java 注解,根据 URL 中的 [Action] 字段,将请求“路由”到正确的控制器及其内部的相应操作。 URL 的其余部分 [/param1/param2/...] 由可选参数组成,这些参数将传递给 Action。此处 MVC 中的“C”指代 [前端控制器、控制器、Action] 这一链条。如果没有任何控制器能处理所请求的 Action,Web 服务器将返回“未找到所请求的 URL”的响应。
  1. 处理
      • (续)
    • 选定的操作可以使用 [前端控制器] 传递给它的参数。这些参数可能来自以下几个来源:
      • URL 中的 [/param1/param2/...] 路径,
      • URL 的 [p1=v1&p2=v2] 参数
      • 浏览器随请求提交的参数;
    • 在处理用户请求时,操作可能需要调用 [业务] 层 [2b]。一旦处理完客户端的请求,即可触发各种响应。一个经典示例是:
      • 若请求无法正确处理,则返回错误页面
      • 否则则返回确认页面
    • 操作会指示特定的视图进行渲染 [3]。该视图将显示被称为视图模型的数据。这就是 MVC 中的“M”。操作会创建该视图模型 [2c] 并指示视图进行渲染 [3];
  1. 响应 - 选定的视图 V 使用操作生成的模型 M 来初始化其必须发送给客户端的 HTML 响应中的动态部分,然后发送此响应。

对于 Web 服务 / JSON,上述架构会稍作调整:

  • 在 [4a] 中,该模型(即一个 Java 类)通过 JSON 库转换为 JSON 字符串;
  • 在 [4b] 中,该 JSON 字符串被发送至浏览器;

现在,让我们澄清 MVC Web 架构与分层架构之间的关系。根据模型的定义方式不同,这两个概念可能相关,也可能无关。考虑一个单层的 Spring MVC Web 应用程序:

如果我们使用 Spring MVC 实现 [Web] 层,那么我们确实拥有了 MVC Web 架构,但并非多层架构。在此情况下,[Web] 层将处理所有事务:呈现、业务逻辑和数据访问。这些工作将由 Action 类来完成。

现在,让我们考虑一种多层Web架构:

Web 层可以在不使用框架且不遵循 MVC 模型的情况下实现。这样我们就得到了一个多层架构,但 Web 层并未采用 MVC 模型。

例如,在 .NET 环境中,上述 [Web] 层可以使用 ASP.NET MVC 来实现,从而形成一个具有 MVC 风格 [Web] 层的分层架构。完成这一步后,我们可以将这个 ASP.NET MVC 层替换为经典的 ASP.NET 层(WebForms),同时保持其余部分(业务逻辑、DAO、ORM)不变。 这样,我们就得到了一种分层架构,其[Web]层不再基于MVC。

在 MVC 中,我们曾提到 M 模型即 V 视图所呈现的数据集。这里给出 MVC 中 M 模型的另一种定义:

许多作者认为,位于 [Web] 层右侧的部分构成了 MVC 中的 M 模型。为避免歧义,我们可以将:

  • 在提及[Web]层右侧的所有内容时,称之为领域模型
  • 当指代视图 V 所显示的数据时,称之为视图模型

下文中,“M模型”一词将专指视图V的模型

16.3. 基于 Spring MVC 的 Web/JSON 项目

该网站 [http://spring.io/guides] 提供了入门教程,帮助您探索 Spring 生态系统。我们将参考其中一个教程,了解 Spring MVC 项目所需的 Maven 配置。

16.3.1. 演示项目

  • 在 [1] 中,我们引入了其中一份 Spring 指南;
  • 在 [2] 中,我们选择 [Rest Service] 示例;
  • 在 [3] 中,我们选择了 Maven 项目;
  • 在 [4] 中,我们选择指南的最终版本;
  • 在 [5] 中,我们确认;
  • 在 [6] 中,导入项目;

通过标准 URL 访问并返回 JSON 数据的 Web 服务通常被称为 REST(表征状态转移)服务。如果一个服务遵循某些规则,则被称为 RESTful 服务。

现在让我们来检查这个导入的项目,首先从它的 Maven 配置开始。

16.3.2. Maven 配置

[pom.xml] 文件内容如下:


<?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>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <start-class>hello.Application</start-class>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>
</project>
  • 第 6–8 行:Maven 项目属性。缺少一个指定 Maven 构建所生成文件类型的 [<packaging>] 标签。在缺少该标签的情况下,默认使用 [jar] 类型。因此,该应用程序是一个基于控制台的可执行应用程序,而非 Web 应用程序(如果是 Web 应用程序,则应使用 [war] 类型);
  • 第 10–14 行:该 Maven 项目有一个父项目 [spring-boot-starter-parent]。它定义了该项目的大部分依赖项。这些依赖项可能已足够(此时不会添加额外依赖),也可能不足(此时会添加缺失的依赖);
  • 第 17–20 行:[spring-boot-starter-web] 构建产物包含 Spring MVC Web 服务项目所需的库,该项目不生成视图。该构建产物包含大量库,其中包括用于嵌入式 Tomcat 服务器的库。应用程序将在该服务器上运行;

此配置中包含的库数量众多:

上图中,我们可以看到三个 Tomcat 服务器压缩包。

16.3.3. Spring [Web/JSON] 服务的架构

对于 Web/JSON 服务,Spring MVC 通过以下方式实现 MVC 模型:

  • 在 [4a] 中,模型(即一个 Java 类)通过 JSON 库被转换为 JSON 字符串;
  • 在[4b]中,该JSON字符串被发送至浏览器;

16.3.4. C 控制器

  

导入的应用程序包含以下控制器:


package hello;
 
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class GreetingController {
 
    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();
 
    @RequestMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
}
  • 第 9 行:[@RestController] 注解将 [GreetingController] 类定义为 Spring 控制器,这意味着其方法会被注册以处理 URL。我们之前见过类似的 [@Controller] 注解。该控制器方法的返回类型是 [String],即要显示的视图名称。而这里则有所不同。 [@RestController] 的方法返回的对象会被序列化后发送至浏览器。具体的序列化类型取决于 Spring MVC 的配置。在此处,它们将被序列化为 JSON。正是项目依赖中包含的 JSON 库,促使 Spring Boot 自动以这种方式配置项目;
  • 第 14 行:[@RequestMapping] 注解指定了该方法处理的 URL,本例中为 [/greeting];
  • 第 15 行:我们已经解释过 [@RequestParam] 注解。该方法返回的结果是一个 [Greeting] 类型的对象。
  • 第 12 行:一个原子类型的长整型。这意味着它支持并发访问。多个线程可能希望同时递增 [counter] 变量。系统将妥善处理这种情况。只有当当前正在修改计数器的线程完成修改后,其他线程才能读取计数器的值。

16.3.5. M 模型

前一种方法生成的 M 模型是以下 [Greeting] 对象:

  

package hello;
 
public class Greeting {
 
    private final long id;
    private final String content;
 
    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }
 
    public long getId() {
        return id;
    }
 
    public String getContent() {
        return content;
    }
}

对该对象进行 JSON 转换后,将生成字符串 {"id":n,"content":"text"}。最终,控制器方法生成的 JSON 字符串将呈现为以下形式:

{"id":2,"content":"Hello, World!"}

{"id":2,"content":"Hello, John!"}

16.3.6. 执行

  

[Application.java] 类是该项目的可执行类。其代码如下:


package hello;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
 
@ComponentScan
@EnableAutoConfiguration
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
 
}

我们在前面的示例中已经遇到并解释过这段代码。现在让我们运行该项目:

 

我们得到了以下控制台日志:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.9.RELEASE)

2014-11-28 15:22:55.005  INFO 3152 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 3152 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\gs-rest-service)
2014-11-28 15:22:55.046  INFO 3152 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62e136d3: startup date [Fri Nov 28 15:22:55 CET 2014]; root of context hierarchy
2014-11-28 15:22:55.762  INFO 3152 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-11-28 15:22:56.567  INFO 3152 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-11-28 15:22:56.738  INFO 3152 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-11-28 15:22:56.740  INFO 3152 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.56
2014-11-28 15:22:56.869  INFO 3152 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-11-28 15:22:56.870  INFO 3152 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1827 ms
2014-11-28 15:22:57.478  INFO 3152 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-11-28 15:22:57.481  INFO 3152 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-11-28 15:22:57.685  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.879  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-11-28 15:22:57.884  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.885  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.906  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.907  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:58.231  INFO 3152 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-11-28 15:22:58.318  INFO 3152 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-11-28 15:22:58.319  INFO 3152 --- [           main] hello.Application                        : Started Application in 3.788 seconds (JVM running for 4.424)
  • 第 13 行:Tomcat 服务器在端口 8080 上启动(第 12 行);
  • 第 17 行:存在 [DispatcherServlet] Servlet;
  • 第 20 行:已发现方法 [GreetingController.greeting];

要测试该 Web 应用程序,请访问 URL [http://localhost:8080/greeting]:

 

我们收到了预期的 JSON 字符串。查看服务器发送的 HTTP 头部信息可能会很有趣。为此,我们将使用名为 [Advanced Rest Client] 的 Chrome 扩展程序(Chrome / Ctrl-T / [应用程序] 菜单 / [Advanced Rest Client] - 参见附录第 23.11 段):

  • 在 [1] 中,请求的 URL;
  • 在 [2] 中,使用了 GET 方法;
  • 在 [3] 中,JSON 响应;
  • 在 [4] 中,服务器表明其将发送 JSON 格式的响应;
  • 在 [5] 中,请求了相同的 URL,但这次使用的是 POST 请求;
  • 在 [7] 中,信息以 [urlencoded] 格式发送至服务器;
  • 在 [6] 中,包含 name 参数及其值;
  • 在 [8] 中,浏览器告知服务器将发送 [urlencoded] 数据;
  • 在 [9] 中,服务器的 JSON 响应;

16.3.7. 创建可执行归档文件

现在我们将创建一个可执行归档文件:

  • 在 [1] 中:我们运行一个 Maven 目标;
  • 在 [2] 中:有两个目标:[clean] 用于从 Maven 项目中删除 [target] 文件夹,[package] 用于重新生成该文件夹;
  • 在 [3] 中:生成的 [target] 文件夹将位于此文件夹中;
  • 在 [4] 中:目标已生成;

在控制台显示的日志中,务必确认 [spring-boot-maven-plugin] 已列出。这是用于生成可执行归档文件的插件(参见下方的 [pom.xml]):


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
</build>
[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

在命令提示符下,导航至生成的文件夹:


D:\Temp\wksSTS\gs-rest-service\target>dir
 ...
11/06/2014  15:30    <DIR>          classes
11/06/2014  15:30    <DIR>          generated-sources
11/06/2014  15:30        11 073 572 gs-rest-service-0.1.0.jar
11/06/2014  15:30             3 690 gs-rest-service-0.1.0.jar.original
11/06/2014  15:30    <DIR>          maven-archiver
11/06/2014  15:30    <DIR>          maven-status
...
  • 第 5 行:生成的归档文件;

该归档文件的执行方式如下:


D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
 
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.0.RELEASE)
 
2014-06-11 15:32:47.088  INFO 4972 --- [           main] hello.Application
                  : Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...

现在 Web 应用程序已运行,您可以通过浏览器访问它:

 

16.3.8. 在 Tomcat 服务器上部署应用程序

与上一个项目一样,我们将 [pom.xml] 文件修改如下:


<?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>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>
 
    ...
</project>
  • 第 9 行:您必须指定正在生成 WAR(Web Archive)文件;

还必须配置 Web 应用程序。如果不存在 [web.xml] 文件,则通过继承 [SpringBootServletInitializer] 的类来完成此配置:

  

[ApplicationInitializer] 类的定义如下:


package hello;
 
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
 
public class ApplicationInitializer extends SpringBootServletInitializer {
 
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }
 
}
  • 第 6 行:[ApplicationInitializer] 类继承自 [SpringBootServletInitializer] 类;
  • 第 9 行:重写了 [configure] 方法(第 8 行);
  • 第 10 行:提供了用于配置该项目的类;

要运行该项目,请按以下步骤操作:

  • 在 [1-2] 中,在 Eclipse IDE 中注册的某台服务器上运行该项目;

完成上述操作后,您可以在浏览器中访问 URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell]:

 

16.4. 结论

我们已经介绍了一种Spring MVC项目,其中Web应用程序会向浏览器发送JSON数据流。接下来,我们将开发一个Web/JSON应用程序,以便在前几章中研究的[dbproduitscategories]数据库能在Web上公开访问。