Spring MVC

spring

MVC是Spring框架中最重要的模块之一。
它构建在强大的Spring IoC容器上,并广泛使用容器特性来简化其配置。

模型-视图-控制器(MVC)是UI设计中常见的设计模式。
它将模型、视图和控制器的角色分离到应用程序中,从而将业务逻辑与ui分离开来。
模型负责为要显示的视图封装应用程序数据。视图应该只显示此数据,而不包含任何业务逻辑。
控制器负责接收用户的请求并调用后端服务进行业务处理。
处理后,后端服务可以返回一些数据以供视图显示。
控制器收集这些数据并为视图准备模型。
MVC模式的核心思想是将业务逻辑与ui分离开来,使其能够独立地进行更改,而不会相互影响。

在Spring MVC应用程序中,模型通常由域对象组成,这些域对象由服务层处理并由持久化层保存。
视图通常是用Java标准标记库(JSTL)编写的JSP模板。
但是,也可以将视图定义为PDF文件、Excel文件、RESTful web服务,甚至是Flex接口,后者通常被称为富Internet应用程序(ria)。
完成本章后,您将能够使用Spring MVC开发Java web应用程序。
您还将了解Spring MVC的通用控制器和视图类型,其中包括自Spring 3.0发布以来用于创建控制器的注解。
此外,您将了解Spring MVC的基本原则,它将作为后续章节中更高级主题的基础。

使用Spring MVC开发一个简单的Web应用程序

问题

您希望使用Spring MVC开发一个简单的web应用程序,以了解这个框架的基本概念和配置。

解决方案

Spring MVC的中心组件是前端控制器。在最简单的Spring MVC应用程序中,这个控制器是惟一需要在Java web部署描述符(即web.xml文件或ServletContainerInitializer)中配置的servlet。Spring MVC控制器(通常称为dispatcher servlet)实现了Sun的核心Java EE设计模式之一,称为Front Controller。它充当Spring MVC框架的前端控制器,每个web请求都必须经过它,以便能够管理整个请求处理过程。

当web请求被发送到Spring MVC应用程序时,控制器首先接收请求。然后,它将组织在Spring的web应用程序上下文中配置的不同组件或控制器本身中存在的注释,这些都是处理请求所需的。图3-1显示了Spring MVC中请求处理的主要流程。

图3-1

要在Spring中定义一个控制器类,必须用@Controller或@RestController注释标记一个类。
当@Controller带注释的类(即controller类)接收到请求时,它会寻找合适的处理程序方法来处理请求。这要求控制器类通过一个或多个处理程序映射将每个请求映射到处理程序方法。为此,使用@RequestMapping注释修饰controller类的方法,使它们成为处理程序方法。
这些处理程序方法的签名(您可以从任何标准类中看到)是开放的。可以为处理程序方法指定任意名称,并定义各种方法参数。同样,处理程序方法可以返回一系列值中的任何一个(例如,String或void),这取决于它执行的应用程序逻辑。随着这本书的深入,您将会遇到各种方法参数,这些参数可以使用@RequestMapping注释在处理程序方法中使用。下面只是有效参数类型的部分列表,仅供您了解。

  • HttpServletRequest或HttpServleResponse
  • 请求任意类型的参数,用@RequestParam注释
  • 任意类型的模型属性,用@ModelAttribute标注。
  • 在传入请求中包含的Cookie值,用@CookieValue标注。
  • 映射或ModelMap,用于处理方法向模型添加属性。
  • 处理程序方法访问命令对象的绑定和验证结果的错误或BindingResult
  • SessionStatus,用来通知处理程序方法完成会话处理

一旦控制器类选择了一个适当的处理程序方法,它将调用处理程序方法的逻辑与请求。通常,控制器的逻辑调用后端服务来处理请求。
此外,处理程序方法的逻辑可能会从众多输入参数(例如HttpServletRequest、Map、Errors或SessionStatus)中添加或删除信息,这些参数将构成正在进行的Spring MVC流的一部分。
处理完请求后,它将控件委托给一个视图,该视图被表示为处理程序方法的返回值。要提供一种灵活的方法,处理程序方法的返回值并不表示视图的实现(例如,user.jsp report.pdf)。而是逻辑视图(例如,user或report)——注意文件扩展的缺乏。
处理程序方法的返回值可以是表示逻辑视图名的字符串,也可以是void,在这种情况下,默认逻辑视图名是根据处理程序方法或控制器的名称确定的。
要将信息从控制器传递到视图,处理程序的方法返回逻辑视图名称字符串或voip是不相关的,因为处理程序方法的输入参数将对视图可用。例如,如果处理程序方法以Map和SessionStatus对象作为输入参数——在处理程序方法的逻辑中修改它们的内容——这些相同的对象将被处理程序方法返回的视图访问。
当控制器类接收到一个视图时,它将逻辑视图名解析为一个特定的视图实现(例如,user)。通过视图解析器来实现jsp或report.pdf。视图解析器是在实现ViewResolver接口的web应用程序上下文中配置的bean。它的职责是为逻辑视图名返回特定的视图实现(HTML、JSP、PDF或其他)。
一旦控制器类将视图名解析为视图实现,根据视图实现的设计,它将呈现控制器处理程序方法传递的对象(例如HttpServletRequest、Map、Errors或SessionStatus)。视图的职责是向用户显示在处理程序方法的逻辑中添加的对象。

它是如何工作的

假设你要为一个体育中心开发一个法庭预约系统。此应用程序的ui是基于web的,以便用户可以通过Internet在线预订。您希望使用Spring MVC开发这个应用程序。首先,在域子包中创建以下域类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.apress.springrecipes.court.domain;

public class Reservation {
private String courtName;
private Date date;
private int hour;
private Player player;
private SportType sportType;
// Constructors, Getters and Setters
...
}

package com.apress.springrecipes.court.domain;

public class Player {
private String name;
private String phone;
// Constructors, Getters and Setters
...
}

package com.apress.springrecipes.court.domain;
public class SportType {
private int id;
private String name;
// Constructors, Getters and Setters
...
}

然后,在服务子包中定义以下服务接口,为表示层提供预订服务:

1
2
3
4
5
6
7
8
package com.apress.springrecipes.court.service;

import com.apress.springrecipes.court.domain.Reservation;
import java.util.List;

public interface ReservationService {
public List<Reservation> query(String courtName);
}

在生产应用程序中,应该使用数据库持久性实现这个接口。但是为了简单起见,您可以将预订记录存储在一个列表中,并为测试目的硬编码多个预订。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.apress.springrecipes.court.service;

import com.apress.springrecipes.court.domain.Player;
import com.apress.springrecipes.court.domain.Reservation;
import com.apress.springrecipes.court.domain.SportType;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
public class ReservationServiceImpl implements ReservationService {
public static final SportType TENNIS = new SportType(1, "Tennis");
public static final SportType SOCCER = new SportType(2, "Soccer");

private final List<Reservation> reservations = new ArrayList<>();

public ReservationServiceImpl() {
reservations.add(new Reservation("Tennis #1", LocalDate.of(2008, 1, 14), 16, new Player("Roger", "N/A"), TENNIS));
reservations.add(new Reservation("Tennis #2", LocalDate.of(2008, 1, 14), 20, new Player("James", "N/A"), TENNIS));
}

@Override
public List<Reservation> query(String courtName) {
return this.reservations.stream()
.filter(reservation -> Objects.equals(reservation.getCourtName(), courtName)).collect(Collectors.toList());
}
}

设置一个Spring MVC应用程序

接下来,您需要创建一个Spring MVC应用程序布局。通常,使用Spring MVC开发的web应用程序的设置方式与标准Java web应用程序相同,只是需要添加一些配置文件和特定于Spring MVC的必需库。
Java EE规范定义了由web archive (WAR文件)组成的Java web应用程序的有效目录结构。例如,您必须提供一个web部署描述符(即:在WEB-INF根或一个或多个实现ServletContainerInitializer的类中。这个web应用程序的类文件和JAR文件应该分别放在WEB-INF/classes 和 WEB-INF/lib目录中。
对于您的法庭预订系统,您将创建以下目录结构。注意,突出显示的文件是spring特定的配置文件。

注意,要使用Spring MVC开发web应用程序,您必须将所有普通的Spring依赖项(更多信息请参阅第1章)以及Spring web和Spring MVC依赖项添加到您的类路径中。如果您正在使用Maven,请向您的Maven项目添加以下依赖项:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>

如果您使用的是gradle,请添加以下内容:

1
2
3
dependencies {
compile "org.springframework:spring-webmvc:$springVersion"
}

WEB-INF目录之外的文件可以通过url直接访问,因此CSS文件和图像文件必须放在那里。在使用Spring MVC时,JSP文件充当模板。它们被用于生成动态内容的框架读取,因此应该将JSP文件放到WEB-INF目录中,以防止直接访问它们。但是,有些应用服务器不允许WEB-INF内部的文件被web应用程序内部读取。在这种情况下,您只能将它们放在WEB-INF目录之外。

创建配置文件

web部署描述符(web.xml或ServletContainerInitializer是Java web应用程序的基本配置文件)。在这个文件中,定义应用程序的servlet以及web请求如何映射到它们。对于Spring MVC应用程序,您必须只定义一个单独的DispatcherServlet实例,它充当Spring MVC的前端控制器,尽管您可以在需要时定义多个。
在大型应用程序中,可以方便地使用多个DispatcherServlet实例。这允许将DispatcherServlet实例指定为特定的url,从而使代码管理更加容易,并允许各个团队成员在不妨碍彼此的情况下处理应用程序的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.apress.springrecipes.court.web;

import com.apress.springrecipes.court.config.CourtConfiguration;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import java.util.Set;

public class CourtServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(CourtConfiguration.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
ServletRegistration.Dynamic courtRegistration = ctx.addServlet("court", dispatcherServlet);
courtRegistration.setLoadOnStartup(1);
courtRegistration.addMapping("/");
}
}

在这个CourtServletContainerInitializer,您定义一个类型为DispatcherServlet的servlet。
这是Spring MVC中的核心servlet类,它接收web请求并将它们分派给适当的处理程序。您将此servlet的名称设置为court,并使用斜杠(/)映射所有url,斜杠表示根目录。注意,可以将URL模式设置为更细粒度的模式。在较大的应用程序,
在各种servlet之间委托模式可能更有意义,但为了简单起见,应用程序中的所有url都委托给单个法庭servlet。
要检测到CourtServletContainerInitializer,还必须在META-INF/services目录中添加一个名为javax.servlet.ServletContainerInitializer的文件。文件的内容应该是CourtServletContainerInitializer的全名。此文件由servlet容器加载,用于引导应用程序。

1
com.apress.springrecipes.court.web.CourtServletContainerInitializer

最后,添加CourtConfiguration类,它是一个简单的@Configuration类。

1
2
3
4
5
6
7
8
package com.apress.springrecipes.court.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.apress.springrecipes.court")
public class CourtConfiguration {}

这定义了一个@ComponentScan注释,它将扫描com.apress.springrecipes.court包(和子包)并注册所有检测到的bean(在本例中,是ReservationServiceImpl和尚未创建的@Controller带注释类)。

创建Spring MVC控制器

基于注解的控制器类可以是一个不实现特定接口或扩展特定基类的任意类。您可以使用@Controller注释它。可以在控制器中定义一个或多个处理程序方法来处理单个或多个操作。处理程序方法的签名足够灵活,可以接受一系列参数。
@RequestMapping注释可以应用到类级别或方法级别。第一个映射策略是将特定的URL模式映射到控制器类,然后将特定的HTTP方法映射到每个处理程序方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.apress.springrecipes.court.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.Date;

@Controller
@RequestMapping("/welcome")
public class WelcomeController {

@RequestMapping(method = RequestMethod.GET)
public String welcome(Model model) {

Date today = new Date();
model.addAttribute("today", today);
return "welcome";
}
}

该控制器创建一个java.util.Date对象来检索当前日期,然后将其作为属性添加到输入模型对象中,以便目标视图可以显示它。
由于您已经激活了对com.apress.springrecipes.court 包的注释扫描,因此在部署时将检测控制器类的注释。
@Controller注释将类定义为Spring MVC控制器。@RequestMapping注释更有趣,因为它包含属性,可以在类或处理程序方法级别声明。这个类中使用的第一个值—(“/welcome”)—用于指定控制器可操作的URL,这意味着/welcome URL上收到的任何请求都由WelcomeController类处理。
一旦控制器类处理了请求,它将调用委托给控制器中声明的默认HTTP GET处理程序方法。这种行为的原因是URL上的每个初始请求都属于HTTP GET类型。因此,当控制器处理/welcome URL上的请求时,它随后委托给默认的HTTP GET处理程序方法进行处理。
注释@RequestMapping(method = RequestMethod.GET)用于将welcome方法修饰为控制器的默认HTTP GET处理程序方法。值得一提的是,如果没有声明默认的HTTP GET处理程序方法,就会抛出ServletException。因此,对于Spring MVC控制器来说,至少有一个URL路由和默认的HTTP GET处理程序方法是很重要的。
这种方法的另一种变体是在方法级别使用的@RequestMapping注释中声明value - url路由和默认的HTTP GET处理程序。下面说明本声明:

1
2
3
4
5
@Controller
public class WelcomeController {
@RequestMapping(value = "/welcome", method=RequestMethod.GET)
public String welcome(Model model) { ... }
}

这个声明与前面的声明是等价的。值属性指示到其中的URL
处理程序方法被映射,方法属性定义处理程序方法作为控制器的默认HTTP GET方法。最后,还有一些方便的注释,如@GetMapping、@PostMapping等等,以最小化配置。下面的映射将与前面提到的声明一样:

1
2
3
4
5
@Controller
public class WelcomeController {
@GetMapping("/welcome")
public String welcome(Model model) { ... }
}

@GetMapping注释使类更短,可能更容易阅读。
最后一个控制器演示了Spring MVC的基本原理。但是,典型的控制器可能调用后端服务进行业务处理。例如,您可以创建一个控制器来查询某一法院的保留如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.apress.springrecipes.court.web;

import com.apress.springrecipes.court.domain.Reservation;
import com.apress.springrecipes.court.service.ReservationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;

@Controller
@RequestMapping("/reservationQuery")
public class ReservationQueryController {

private final ReservationService reservationService;

public ReservationQueryController(ReservationService reservationService) {
this.reservationService = reservationService;
}

@GetMapping
public void setupForm() {}

@PostMapping
public String sumbitForm(@RequestParam("courtName") String courtName, Model model) {
List<Reservation> reservations = java.util.Collections.emptyList();
if (courtName != null) {
reservations = reservationService.query(courtName);
}
model.addAttribute("reservations", reservations);

return "reservationQuery";
}
}

如前所述,控制器然后查找默认的HTTP GET处理程序方法。由于公共void setupForm()方法为此分配了必要的@RequestMapping注释,因此它被称为next。
与前面的默认HTTP GET处理程序方法不同,请注意,该方法没有输入参数,没有逻辑,并且具有一个空返回值。这意味着两件事。由于没有输入参数和逻辑,视图只显示实现模板(例如JSP)中硬编码的数据,因为没有
控制器正在添加数据。如果返回值为空,则使用基于请求URL的默认视图名;因此,由于请求URL是/reservation查询,因此假设有一个名为reservationQuery的返回视图。

其余的处理程序方法使用@PostMapping注释进行修饰。乍一看,只有类级/reservation查询URL语句的两个处理程序方法可能会令人困惑,但实际上非常简单。当在/reservation查询URL上发出HTTP GET请求时,将调用一个方法;当HTTP POST请求在同一个URL上进行时,调用另一个。
web应用程序中的大多数请求都是HTTP GET类型,而HTTP POST类型的请求通常是在用户提交HTML表单时发出的。因此,揭示更多应用程序的视图(我们将很快描述它),在初始加载HTML表单时调用一个方法(即:,而另一个在提交HTML表单时被调用(例如。HTTP POST)。
仔细查看HTTP POST默认处理程序方法,请注意这两个输入参数。首先注意@RequestParam(“courtName”)字符串courtName声明,用于提取名为courtName的请求参数。在这种情况下,HTTP POST请求以表单/reservation查询的形式出现?courtName = <价值>;此声明使方法中的变量courtName下的值可用。其次,请注意模型声明,它用于定义一个对象,以便将数据传递到返回的视图。
处理程序方法执行的逻辑包括使用控制器的预订服务来使用courtName变量执行查询。从该查询获得的结果被分配给模型对象,模型对象稍后将可用于显示的返回视图。
最后,注意该方法返回一个名为reservationQuery的视图。这个方法也可以返回void,就像默认的HTTP GET一样,并且由于请求URL而被分配给相同的reservationQuery默认视图。这两种方法都是相同的。
现在您已经了解了Spring MVC控制器是如何构成的,现在是时候探索控制器的处理程序方法将其结果委托给哪些视图了。

创建JSP视图

Spring MVC为不同的表示技术支持多种类型的视图。它们包括jsp、HTML、PDF、Excel工作表(XLS)、XML、JSON、Atom和RSS提要、JasperReports和其他第三方视图实现。
在Spring MVC应用程序中,视图通常是用JSTL编写的JSP模板。当应用程序的web.xml文件中定义的dispatcherservlet接收处理程序返回的视图名称时,它将逻辑视图名称解析为视图实现以进行呈现。例如,可以在web应用程序上下文的CourtConfiguration中配置InternalResourceViewResolver bean,以便将视图名解析为/WEB-INF/jsp/目录中的JSP文件。

1
2
3
4
5
6
7
@Bean
public InternalResourceViewResolver internalResourceViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}

通过使用最后的配置,一个名为reservationQuery的逻辑视图被委托给位于/WEB-INF/jsp/reservationQuery.jsp的视图实现。了解了这一点,您可以为welcome控制器创建以下JSP模板,并将其命名为welcome.jsp并将其放在/WEB-INF/ jsp/目录中:

1
2
3
4
5
6
7
8
9
10
11
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<html>
<head>
<title>Welcome</title>
</head>

<body>
<h2>Welcome to Court Reservation System</h2>
Today is <fmt:formatDate value="${today}" pattern="yyyy-MM-dd" />. </body>
</html>

在这个JSP模板中,您使用JSTL中的fmt标记库将today模型属性格式化为yyyy-MM-dd模式。不要忘记在这个JSP模板的顶部包含fmt标记库定义。
接下来,您可以为预订查询控制器创建另一个JSP模板,并将其命名为reservationQuery.jsp以匹配视图名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<html>
<head>
<title>Reservation Query</title>
</head>

<body>
<form method="post">
Court Name
<input type="text" name="courtName" value="${courtName}" />
<input type="submit" value="Query" />
</form>

<table border="1">
<tr>
<th>Court Name</th>
<th>Date</th>
<th>Hour</th>
<th>Player</th>
</tr>
<c:forEach items="${reservations}" var="reservation">
<tr>
<td>${reservation.courtName}</td>
<td><fmt:formatDate value="${reservation.date}" pattern="yyyy-MM-dd" /></td>
<td>${reservation.hour}</td>
<td>${reservation.player.name}</td>
</tr>
</c:forEach>
</table>
</body>
</html>

在这个JSP模板中,您包含了一个表单,用户可以输入他们想查询的法院名称,然后使用<c:forEach>标记来循环预订的模型属性来生成结果表。

部署Web应用程序

在web应用程序的开发过程中,我们强烈建议安装本地Java EE应用服务器,该服务器附带用于测试和调试的web容器。为了便于配置和部署,我们选择了Apache Tomcat 8.5。x作为web容器。
这个web容器的部署目录位于webapps目录中。默认情况下,Tomcat监听端口8080,并将应用程序部署到应用程序WAR的同名上下文。因此,如果您将应用程序打包到一个名为court的WAR中。war、欢迎控制器和预订查询控制器可以通过以下url访问:

1
2
http://localhost:8080/court/welcome
http://localhost:8080/court/reservationQuery

提示项目还可以使用app. run ../gradlew buildDocker创建一个Docker容器,以获得一个包含tomcat和应用程序的容器。然后,您可以启动一个Docker容器来测试应用程序(docker run -p 8080:8080 spring-recipes-4th/court-web).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.apress.springrecipes.court.web;

import com.apress.springrecipes.court.config.CourtConfiguration;
import org.springframework.web.servlet.support. AbstractAnnotationConfigDispatcherServletInitializer;

public class CourtWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] {CourtConfiguration.class};
}

@Override
protected String[] getServletMappings() {
return new String[] { "/"};
}
}

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2021 朝着牛逼的道路一路狂奔 All Rights Reserved.

访客数 : | 访问量 :