一、视图解析器

通过 HelloWorld 程序我们看到了 handler 方法的返回值表示:

请求处理完成后,请 SpringMVC 执行一个请求转发。转发的地址就是 handler 方法的返回值。

假设我们在/WEB-INF/pages 目录下有一组 JSP 页面,那么访问它们的路径应该如下表所示:

文件名转发路径
apple.jsp/WEB-INF/pages/apple.jsp
grape.jsp/WEB-INF/pages/grape.jsp
banana.jsp/WEB-INF/pages/banana.jsp
orange.jsp/WEB-INF/pages/orange.jsp
watermelon.jsp/WEB-INF/pages/watermelon.jsp
…………

这很明显是有规律的,转发路径都是以“/WEB-INF/pages/”开头,以“.jsp”结尾。

基于这样一种情况,SpringMVC 做了一种设计:它允许我们把转发路径中前面的固定部分和后面的固定部分以前缀后缀的形式写到配置文件中,然后我们的 handler 方法就仅仅指定中间不一样的部分即可。中间部分和前缀、后缀做字符串拼接。这就是 SpringMVC 提供的视图解析器。

我们在 SpringMVC 的配置文件(spring-mvc.xml)中额外加入下面的配置,视图解析器就生效了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">

<!-- 根本原则:前缀、中间部分、后缀进行字符串拼接,只要字符串最终拼好是正确的转发地址即可 -->
<!-- 相关概念:不带前缀后缀的中间部分又称为『逻辑视图』 -->
<!-- 相关概念:拼好后完整的转发地址又称为『物理视图』 -->

<!-- /WEB-INF/pages/apple1.jsp -->
<!-- /WEB-INF/pages/apple2.jsp -->
<!-- /WEB-INF/pages/apple3.jsp -->
<!-- /WEB-INF/pages/apple4.jsp -->
<!-- /WEB-INF/pages/apple5.jsp -->
<!-- /WEB-INF/pages/apple6.jsp -->

<!-- 配置前缀 -->
<property name="prefix" value="/WEB-INF/pages/"/>

<!-- 配置后缀 -->
<property name="suffix" value=".jsp"/>
</bean>

有了视图解析器,handler 方法的返回值就简单了:

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("/hello")
public String sayHello() {
System.out.println("嘿!我来了!");

// 现在我们在SpringMVC的配置文件中配置了前后缀
// 前缀:/WEB-INF/pages/
// 后缀:.jsp
// 中间部分:由当前方法的返回值提供,『逻辑视图』
// 最终它们拼接的字符串:/WEB-INF/pages/target.jsp,『物理视图』
return "target";
}

SpringMVC 会使用上面方法的返回值“result”和前缀后缀做字符串拼接,从而得到转发路径。

1
"/WEB-INF/pages/"+"target"+".jsp"→→→→→→→→→→"/WEB-INF/pages/target.jsp"

然后按照拼接得到的结果转发请求。

二、@RequestMapping 注解

假设我们在同一个模块有下面几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.mvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class UserHandler {

@RequestMapping("/user/login")
public String login() {
return "target";
}

@RequestMapping("/user/register")
public String register() {
return "target";
}

@RequestMapping("/user/logout")
public String logout() {
return "target";
}
}

它们在@RequestMapping 注解中的映射地址都是以/user 开头,能否统一提取出来呢?很简单,在类上再使用一个@RequestMapping 注解把/user 部分提取出来即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Controller
// 写在类上的@RequestMapping注解能够把方法上@RequestMapping注解中URL地址前面重复的部分抽取出来
// 往往前面被抽取出来的部分代表当前的模块
@RequestMapping("/user/")
public class UserHandler {

@RequestMapping("login")
public String login() {
return "target";
}

@RequestMapping("register")
public String register() {
return "target";
}

@RequestMapping("logout")
public String logout() {
return "target";
}
}

测试的 index.jsp:

1
2
3
4
5
6
7
<h3>测试@RequestMapping注解放在类上</h3>
<a href="${pageContext.request.contextPath}/user/login">[用户模块登录功能]</a
><br />
<a href="${pageContext.request.contextPath}/user/register">[用户模块注册功能]</a
><br />
<a href="${pageContext.request.contextPath}/user/logout">[用户模块退出功能]</a
><br />

三、获取原生 Servlet API 对象

1、提出问题

在 Servlet 的 doGet()方法中,我们可以拿到原生、本真的 HttpServletRequest 和 HttpServletResponse 这样的对象,那么在 SpringMVC 的 handler 方法中能够拿到吗?

1
2
3
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
……
}

完全可以,直接从 handler 方法的参数位置传入即可。

2、传入 handler 方法,获取原生 Servlet API 对象

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
@Controller
public class OriginalObjectHandler {

//[2]使用@Autowired注解装配
@Autowired
private ServletContext servletContext;

@RequestMapping("/original/object")
public String originalObject(
HttpServletRequest request,
HttpServletResponse response,
HttpSession session
) {

System.out.println("request = " + request);
System.out.println("response = " + response);
System.out.println("session = " + session);

//[1]通过HttpSession对象获取
ServletContext servletContext1 = session.getServletContext();
System.out.println("servletContext = " + servletContext1);

return "target";
}

@RequestMapping("/original/servlet/context")
public String originalServletContext() {

System.out.println("servletContext = " + servletContext);

return "target";
}
}

3、获取 ServletContext 对象的两种方式

① 通过 HttpSession 对象获取

1
session.getServletContext()

② 使用@Autowired 注解装配

1
2
@Autowired
private ServletContext servletContext;

测试的 index.jsp:

1
2
3
4
5
6
7
<h3>测试获取原生的Servlet API</h3>
<a href="${pageContext.request.contextPath}/original/object"
>测试获取HttpServletRequest对象、HttpServletResponse对象、HttpSession对象</a
><br />
<a href="${pageContext.request.contextPath}/original/servlet/context"
>测试获取ServletContext对象</a
><br />

返回的结果:

1
2
3
4
5
6
request = org.apache.catalina.connector.RequestFacade@50727615
response = org.apache.catalina.connector.ResponseFacade@28312413
session = org.apache.catalina.session.StandardSessionFacade@300f8e6c
servletContext = org.apache.catalina.core.ApplicationContextFacade@2959d9e6

servletContext = org.apache.catalina.core.ApplicationContextFacade@2959d9e6

分析:两个方法获取到的 servletContex 的 hashCode 相同。

一个 web 应用只能有一个 servletContext,用不同的方式拿到的是同一个应用。

四、获取请求参数

1、什么是请求参数?

比如说在 url 地址的后面写上 ?键值对 告诉后端程序要删除谁

1
<a href="emp/remove?empId=3">删除</a>

再比如说下面的表单,用 name 属性的值带上请求参数。

1
2
3
4
5
6
<form action="emp/save" method="post">
姓名:<input type="text" name="empName" /><br />
年龄:<input type="text" name="empAge" /><br />
工资:<input type="text" name="empSalary" /><br />
<input type="submit" value="保存" />
</form>

2、请求参数的四种情况

① 一名一值

1
<a href="emp/remove?empId=3">删除</a>

在 handler 方法的参数上使用@RequestParam 注解。

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
package com.atguigu.mvc.controller;

@Controller
public class ParamHandler {

// /param/one/name/one/value?userName=tom
@RequestMapping("/param/one/name/one/value")
public String paramOneNameOneValue(
// 使用@RequestParam注解接收请求中包含的请求参数
// SpringMVC会把获取到的请求参数从形参位置传入

// @RequestParam注解的required属性默认为true表示当前请求参数必须提供
// 如果没有提供会看到:HTTP Status 400 - Required String parameter 'userName' is not present错误
// 如果这个参数可有可无,那么可以把required属性设置为false,此时不提供参数也不会报错,会返回变量的默认值

// @RequestParam注解的defaultValue属性用于给这个请求参数设置默认值
// 当请求中不包含这个请求参数时,把默认值从形参位置传入
// 当我们设置了defaultValue属性提供了默认值,即使在required = true的情况下请求参数没有提供也不会报错,会返回我们设置的默认值
// @RequestParam(value = "userName", required = true, defaultValue = "dog") String userName

// 当前请求参数名和形参名一致时,可以省略@RequestParam,但是从代码可读性角度来说最好还是写上
// 即省略@RequestParam("userName"),如下
// String userName

@RequestParam("userName") String userName
) {

System.out.println("userName = " + userName);

return "target";
}
}

测试的 index.jsp

1
2
3
4
5
<h3>测试发送请求参数</h3>
<a
href="${pageContext.request.contextPath}/param/one/name/one/value?userName=Tom"
>一个名字一个值</a
><br />

② 一名多值

测试的 index.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<form
action="${pageContext.request.contextPath}/param/one/name/multi/value"
method="post"
>
请选择你最喜欢的球队:
<input type="checkbox" name="team" value="Brazil" />巴西
<input type="checkbox" name="team" value="German" />德国
<input type="checkbox" name="team" value="French" />法国
<input type="checkbox" name="team" value="Holland" />荷兰
<input type="checkbox" name="team" value="Italian" />意大利
<input type="checkbox" name="team" value="China" />中国
<br />
<input type="submit" value="保存" />
</form>

使用 List 或数组来接收。

1
2
3
4
5
6
7
8
9
@RequestMapping("/param/one/name/multi/value")
// 一个名字多个值的情况使用一个能够接收多个值的类型作为形参即可
public String paramOneNameMultiValue(@RequestParam("team") List<String> teamList) {

for (String team : teamList) {
System.out.println("team = " + team);
}
return "target";
}

③ 表单数据正好对应一个实体类

测试的 index.jsp:

1
2
3
4
5
6
<form action="${pageContext.request.contextPath}/param/entity" method="post">
姓名:<input type="text" name="empName" /><br />
年龄:<input type="text" name="empAge" /><br />
工资:<input type="text" name="empSalary" /><br />
<input type="submit" value="保存" />
</form>

对应的实体类:

1
2
3
4
5
6
7
8
9
public class Employee {

private Integer empId;
private String empName;
private int empAge;
private double empSalary;

//get,set方法
……

直接使用和表单对应的实体类类型接收

1
2
3
4
5
6
7
@RequestMapping("/param/entity")
public String paramEntity(Employee employee) {

System.out.println("employee = " + employee);

return "target";
}

结果:

分析:

Spring MVC 是使用请求参数的名字(上面 name 的属性值)去对应的实体类(Employee)找对应的 set 方法

Spring MVC 只会对可以对应上的 set 方法给注入进去。

④ 表单对应的实体类包含级联属性

[1]创建对应的测试用例:

School:

1
2
3
4
5
6
7
8
package com.atguigu.mvc.entity;

public class School {

private String schoolName;

//get,set方法
……

Subject:

1
2
3
4
5
6
7
package com.atguigu.mvc.entity;

public class Subject {

private String subjectName;

……

Teacher:

1
2
3
4
5
6
7
8
package com.atguigu.mvc.entity;

public class Teacher {

private String teacherName;

//get,set方法
……

建立 Student

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
……public class Student {

private String stuName;
private School school;
private List<Subject> subjectList;
private Subject[] subjectArray;
private Set<Teacher> tearcherSet;
private Map<String, Double> scores;

public Student() {
//在各种常用数据类型中,只有Set类型需要提前初始化
//并且要按照表单将要提交的对象数量进行初始化
//Set类型使用非常不便,要尽可能避免使用Set
tearcherSet = new HashSet<>();
tearcherSet.add(new Teacher());
tearcherSet.add(new Teacher());
tearcherSet.add(new Teacher());
tearcherSet.add(new Teacher());
tearcherSet.add(new Teacher());
}

//get,set方法
……

[2]测试的 index.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
34
35
36
<form action="${pageContext.request.contextPath}/param/nested" method="post">
学生姓名:<input type="text" name="stuName" /><br />
学校名称:<input type="text" name="school.schoolName" /><br />
List类型学科名称[0]:<input
type="text"
name="subjectList[0].subjectName"
/><br />
List类型学科名称[1]:<input
type="text"
name="subjectList[1].subjectName"
/><br />
List类型学科名称[2]:<input
type="text"
name="subjectList[2].subjectName"
/><br />
数组类型学科名称[0]:<input
type="text"
name="subjectArray[0].subjectName"
/><br />
数组类型学科名称[1]:<input
type="text"
name="subjectArray[1].subjectName"
/><br />
数组类型学科名称[2]:<input
type="text"
name="subjectArray[2].subjectName"
/><br />
tearcherSet[0]:<input type="text" name="tearcherSet[0].teacherName" /><br />
tearcherSet[1]:<input type="text" name="tearcherSet[1].teacherName" /><br />
tearcherSet[2]:<input type="text" name="tearcherSet[2].teacherName" /><br />
语文成绩:<input type="text" name="scores['chinese']" /><br />
英语成绩:<input type="text" name="scores['english']" /><br />
数学成绩:<input type="text" name="scores['math']" /><br />

<button type="submit">保存</button>
</form>

[3]测试类:ParamHandler

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
@RequestMapping("/param/nested")
public String paramNested(Student student) {

String stuName = student.getStuName();
System.out.println("stuName = " + stuName);

String schoolName = student.getSchool().getSchoolName();
System.out.println("schoolName = " + schoolName);

List<Subject> subjectList = student.getSubjectList();
for (Subject subject : subjectList) {
System.out.println("subject = " + subject.getSubjectName());
}

Subject[] subjectArray = student.getSubjectArray();
for (Subject subject : subjectArray) {
System.out.println("subject = " + subject.getSubjectName());
}

Set<Teacher> tearcherSet = student.getTearcherSet();
for (Teacher teacher : tearcherSet) {
System.out.println("teacher = " + teacher.getTeacherName());
}

Map<String, Double> scores = student.getScores();
Set<String> keySet = scores.keySet();
for (String key : keySet) {

Double value = scores.get(key);
System.out.println(key + " = " + value);
}
return "target";
}