⭐表示重要。
第一章:概述
1.1 SpringMVC 的优势
- SpringMVC 是 Spring 作为表现层开发提供的一整套完备的解决方案。
- 在表现层框架历经 Struts1.x 、WebWork、Struts2.x 等诸多产品的历代更迭之后,目前业界普通选择了 SpringMVC 作为 Java EE 项目表现层开发的
首选方案。 - 之所以做到这一点,是因为 SpringMVC 具备如下的优势:
- ①
Spring家族的原生产品,和 IOC 容器等基础设施无缝对接。 - ② 表现层各个细分领域需要解决的问题
全方位覆盖,提供全面的解决方案。 - ③
代码清新简洁,大幅度提高开发效率。 - ③ 内部组件化程序高,可插拔式组件
即插即用,需要什么功能配置相应的组件即可。 - ④
性能卓越,尤其适合现代大型、超大型互联网项目的需求。
- ①
1.2 表现层框架要解决的基本问题
- 请求映射。
- 数据输入。
- 视图界面。
- 请求分发。
- 表单回显。
- 会话控制。
- 过滤拦截。
- 异步交互。
- 文件上传。
- 文件下载。
- 数据校验。
- 类型转换。
1.3 原生 Servlet 和 SpringMVC 对比
1.3.1 基于原生 Servlet API 开发代码片段
package com.github.fairy.era.servlet;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/*** @author 许大仙* @version 1.0* @since 2021-11-11 09:12*/@WebServlet(value = "/hello")public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {// 解决请求参数乱码req.setCharacterEncoding("utf-8");String username = req.getParameter("username");System.out.println("username = " + username);}}
1.3.2 基于 SpringMVC 开发代码片段
package com.github.fairy.era.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;/*** @author 许大仙* @version 1.0* @since 2021-11-11 09:15*/@Controller@RequestMapping(value = "/hello")public class HelloController {@RequestMapping("/user/login")public String index(@RequestParam("username") String username) {System.out.println("username = " + username);return "index";}}
第二章:入门案例(⭐)
2.1 功能需求
Thymeleaf 是服务器端渲染引擎,和 Vue 以及 React 不同的是,浏览器不能识别,所以 Thymeleaf 必须通过服务器渲染生成页面发送给浏览器,浏览器才能识别。
- ① 访问首页。

- ② 在首页上点击超链接。

2.2 环境准备
- IDEA 2021+。
- Maven 3.8。
- JDK 11+。
2.3 导入依赖
- pom.xml
<!-- SpringMVC --><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.3.12</version></dependency><!-- 日志 --><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.6</version></dependency><!-- ServletAPI --><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version><scope>provided</scope></dependency><!-- Spring5和Thymeleaf整合包 --><dependency><groupId>org.thymeleaf</groupId><artifactId>thymeleaf-spring5</artifactId><version>3.0.12.RELEASE</version></dependency>
- 由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其它靠传递性导入。

2.4 日志的配置文件
- logback.xml
<?xml version="1.0" encoding="UTF-8"?><configuration debug="true"><!-- 指定日志输出的位置 --><appender name="STDOUT"class="ch.qos.logback.core.ConsoleAppender"><encoder><!-- 日志输出的格式 --><!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 --><pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern><charset>UTF-8</charset></encoder></appender><!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR --><!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 --><root level="INFO"><!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender --><appender-ref ref="STDOUT" /></root><!-- 根据特殊需求指定局部日志级别 --><logger name="org.springframework.web.servlet.DispatcherServlet" level="DEBUG" /></configuration>
2.5 配置 web.xml
- web.xml
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 --><servlet><servlet-name>DispatcherServlet</servlet-name><!-- DispatcherServlet的全类名 --><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!-- 通过初始化参数指定SpringMVC配置文件位置 --><init-param><!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 --><param-name>contextConfigLocation</param-name><!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 --><param-value>classpath:springmvc.xml</param-value></init-param><!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 --><!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 --><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>DispatcherServlet</servlet-name><!-- 对DispatcherServlet来说,url-pattern有两种方式配置 --><!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 --><!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 --><url-pattern>/</url-pattern></servlet-mapping></web-app >
2.6 SpringMVC 的配置文件
- springmvc.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 https://www.springframework.org/schema/context/spring-context.xsd"><!-- 自动扫描包 --><context:component-scan base-package="com.github.fairy.era.mvc.handler" use-default-filters="false"><context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><!-- 配置视图解析器 --><bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver"><property name="order" value="1"/><property name="characterEncoding" value="UTF-8"/><property name="templateEngine"><bean class="org.thymeleaf.spring5.SpringTemplateEngine"><property name="templateResolver"><bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"><!-- 物理视图:视图前缀+逻辑视图+视图后缀 --><!-- 视图前缀 --><property name="prefix" value="/WEB-INF/templates/"/><!-- 视图后缀 --><property name="suffix" value=".html"/><property name="templateMode" value="HTML5"/><property name="characterEncoding" value="UTF-8" /></bean></property></bean></property></bean></beans>
2.7 创建请求处理器
2.7.1 请求处理器的名称
- SpringMVC 对处理请求的类并没有特殊要求,只要是 POJO 即可。我们自己习惯上有两种命名方式:
- ①
XxxController:Xxx控制器的意思。 - ②
XxxHandler:Xxx处理器的意思。
- ①
- 这只是一个命名的习惯,不是语法要求,所以往往将
处理请求的类称为Handler类,处理请求的方法称为Handler方法。
2.7.2 创建请求处理器

package com.github.fairy.era.mvc.handler;import org.springframework.stereotype.Controller;/*** @author 许大仙* @version 1.0* @since 2021-11-11 10:36*/@Controllerpublic class Demo01HelloHandler {}
2.8 实现访问首页
2.8.1 创建 handler 方法
package com.github.fairy.era.mvc.handler;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 10:36*/@Controllerpublic class Demo01HelloHandler {// @RequestMapping注解在请求地址和Java方法之间建立映射关系@RequestMapping("/index.html")public String showPortal(){return "portal";}}
2.8.2 在首页编写超链接

<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><h1>首页</h1><!-- 以后我们会越来越倾向于用一句话来作为请求的URL地址,在这样的一句话中使用“/”分隔各个单词 --><!-- say hello to spring mvc --><!-- /say/hello/to/spring/mvc --><a th:href="@{/say/hello/to/spring/mvc}">HelloWorld</a><br/></body></html>
2.9 实现点击超链接
2.9.1 声明 Handler 方法
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 10:36*/@Controllerpublic class Demo01HelloHandler {private Logger logger = LoggerFactory.getLogger(this.getClass());// @RequestMapping注解在请求地址和Java方法之间建立映射关系@RequestMapping("/")public String showPortal() {return "portal";}// 以后我们会越来越倾向于用一句话来作为请求的URL地址// 在这样的一句话中使用“/”分隔各个单词@RequestMapping("/say/hello/to/spring/mvc")public String sayHello() {// 方法内部打印日志,证明 SpringMVC 确实调用了这个方法来处理请求logger.info("我是 SpringMVC 的 Hello world。");return "target";}}
2.9.2 创建目标页面

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>目标页面</title></head><body><h1>目标页面</h1><a th:href="@{/}">回首页</a></body></html>
2.10 整体流程解析

第三章:@RequestMapping 注解(⭐)
3.1 概述
- 从注解的名称上,我们可以知道
@RequestMapping注解就是让请求的URL地址和处理请求的方法(handler 方法)关联起来,建立映射关系。 - SpringMVC 接收到指定的请求,就会去映射关系中找对应的方法来处理这个请求。
3.2 @RequestMapping 注解源码
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//package org.springframework.web.bind.annotation;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import org.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Mappingpublic @interface RequestMapping {String name() default "";@AliasFor("path")String[] value() default {};@AliasFor("value")String[] path() default {};RequestMethod[] method() default {};String[] params() default {};String[] headers() default {};String[] consumes() default {};String[] produces() default {};}
3.3 匹配方式
3.3.1 精确匹配
在
@RequestMapping注解指定的URL地址时,不使用任何通配符,按照请求进行精确匹配。示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><!-- 精确匹配 --><a th:href="@{/say/hello/to/spring/mvc}">HelloWorld</a><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 10:36*/@Controllerpublic class Demo01HelloHandler {private Logger logger = LoggerFactory.getLogger(this.getClass());// 精确匹配@RequestMapping("/say/hello/to/spring/mvc")public String sayHello() {// 方法内部打印日志,证明 SpringMVC 确实调用了这个方法来处理请求logger.info("我是 SpringMVC 的 Hello world。");return "target";}}
3.3.2 模糊匹配(不常用)
在
@RequestMapping注解指定的URL地址时,通过使用通配符,匹配多个类似的地址。示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><!-- 模糊匹配 --><a th:href="@{/fruit/apple}">苹果</a><a th:href="@{/fruit/banana}">香蕉</a><a th:href="@{/fruit/pear}">梨子</a></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 10:36*/@Controllerpublic class Demo01HelloHandler {private Logger logger = LoggerFactory.getLogger(this.getClass());/*** 模糊匹配** @return*/@RequestMapping("/fruit/*")public String fruit() {logger.info("fruit");return "target";}}
3.4 在类上标记
3.4.1 仅在方法上进行标记
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>超链接的HTML标签</title></head><body><a th:href="@{/user/login}">用户登录</a><br/><a th:href="@{/user/register}">用户注册</a><br/><a th:href="@{/user/logout}">用户退出</a><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:01*/@Controllerpublic class UserHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 用户登录** @return*/@RequestMapping("/user/login")public String login() {logger.info("UserHandler.login");return "login";}/*** 用户注册** @return*/@RequestMapping("/user/register")public String register() {logger.info("UserHandler.register");return "register";}/*** 用户退出** @return*/@RequestMapping("/user/logout")public String logout() {logger.info("UserHandler.logout");return "logout";}}
3.4.2 在类和方法上分别标记
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>超链接的HTML标签</title></head><body><a th:href="@{/user/login}">用户登录</a><br/><a th:href="@{/user/register}">用户注册</a><br/><a th:href="@{/user/logout}">用户退出</a><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:01*/@Controller@RequestMapping("/user") // 在类级别:抽取各个方法上@RequestMapping注解地址中前面重复的部分public class UserHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 用户登录** @return*/@RequestMapping("/login") // 在方法级别:省略被类级别抽取的部分public String login() {logger.info("UserHandler.login");return "login";}/*** 用户注册** @return*/@RequestMapping("/register") // 在方法级别:省略被类级别抽取的部分public String register() {logger.info("UserHandler.register");return "register";}/*** 用户退出** @return*/@RequestMapping("/logout") // 在方法级别:省略被类级别抽取的部分public String logout() {logger.info("UserHandler.logout");return "logout";}}
3.5 匹配请求方式
3.5.1 请求方式
- HTTP 协议定义了 8 种请求方式,在 SpringMVC 中封装到了下面的枚举类中。
package org.springframework.web.bind.annotation;public enum RequestMethod {GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,TRACE;private RequestMethod() {}}
3.5.2 @RequestMapping 附加请求方式
前面的代码中,只要求匹配请求地址即可,现在有了附加请求方式之后,还要求请求方式也必须匹配。
示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><h3>测试@RequestMapping注解限定请求方式</h3><a th:href="@{/emp}">同地址GET请求</a><br/><form method="post" th:action="@{/emp}"><button type="submit">同地址POST请求</button></form><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:17*/@Controllerpublic class EmpHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** Get请求** @return*/@RequestMapping(value = "/emp", method = RequestMethod.GET)public String empGet() {logger.info("EmpHandler.empGet:get请求");return "target";}/*** Post请求** @return*/@RequestMapping(value = "/emp", method = RequestMethod.POST)public String empPost() {logger.info("EmpHandler.empPost:post请求");return "target";}}
3.5.3 衍生注解
| 原始注解 | 衍生注解 |
|---|---|
| @RequestMapping(value = “/emp”, method = RequestMethod.GET) |
@GetMapping(“/emp”) |
| @RequestMapping(value = “/emp”, method = RequestMethod.POST) |
@PostMapping(“/emp”) |
- 除了
@GetMapping、@PostMapping还有下面几个类似的注解:@PutMapping。@DeleteMapping。@PatchMapping。- ……
注意:衍生注解的这几个注解是从 4.3 版本才开始有,低于 4.3 版本无法使用。
3.5.4 Ambiguous mapping 异常(模糊映射异常)
出现原因:多个 handler 方法映射了同一个地址,导致 SpringMVC 在接收到这个地址的请求时不知道找哪个 handler 方法处理。
示例:模拟 Ambiguous mapping 异常
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:17*/@Controllerpublic class EmpHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** Get请求** @return*/@RequestMapping(value = "/emp")public String emp1() {logger.info("EmpHandler.emp1");return "target";}/*** Post请求** @return*/@RequestMapping(value = "/emp")public String emp2() {logger.info("EmpHandler.emp2");return "target";}}

第四章:获取请求参数(⭐)
4.1 一名一值
4.1.1 概述
- 此种场景使用 @RequestParam 注解。
4.1.2 最基本的用法
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><a th:href="@{/param/one/name/one/value(userName='tom')}">一个名字一个值的情况</a><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/param/one/name/one/value")public String oneNameOneValue(// 使用@RequestParam注解标识handler方法的形参,// SpringMVC会将获取到的请求参数从形参位置给我们传递进来@RequestParam("userName") String userName) {logger.info("ParamHandler.oneNameOneValue的请求参数是:{}", userName);return "target";}}
4.1.3 @RequestParam 注解省略的情况
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><a th:href="@{/param/one/name/one/value(userName='tom')}">一个名字一个值的情况</a><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/param/one/name/one/value")public String oneNameOneValue(// 当请求参数名和形参名一致,可以省略@RequestParam("userName")注解// 但是,省略后代码可读性下降而且将来在SpringCloud中不能省略,所以建议还是不要省略String userName) {logger.info("ParamHandler.oneNameOneValue的请求参数是:{}", userName);return "target";}}
4.1.4 必须的参数没有提供
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><a th:href="@{/param/one/name/one/value}">一个名字一个值的情况</a><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/param/one/name/one/value")public String oneNameOneValue(@RequestParam(name = "userName") String userName) {logger.info("ParamHandler.oneNameOneValue的请求参数是:{}", userName);return "target";}}

- 页面信息说明:
- 响应状态码:400(在 SpringMVC 环境下,400通常和数据注入相关)。
- 说明信息:必需的 String 请求参数 ‘userName’ 不存在。
- 原因:参考
@RequestParam注解的required属性:默认值为true,表示请求参数默认必须提供。
package org.springframework.web.bind.annotation;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import org.springframework.core.annotation.AliasFor;@Target({ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RequestParam {@AliasFor("name")String value() default "";@AliasFor("value")String name() default "";// 表示请求参数默认必须提供boolean required() default true;String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";}
4.1.5 关闭请求参数必须
- required 属性设置为 false,表示这个请求参数可有可无:
@RequestParam(value = "userName", required = false)
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><a th:href="@{/param/one/name/one/value}">一个名字一个值的情况</a><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/param/one/name/one/value")public String oneNameOneValue(// required 属性设置为 false 表示这个请求参数可有可无@RequestParam(name = "userName", required = false) String userName) {logger.info("ParamHandler.oneNameOneValue的请求参数是:{}", userName);return "target";}}
4.1.6 给请求参数设置默认值
- 使用 defaultValue 属性给请求参数设置默认值:
@RequestParam(value = "userName", required = false, defaultValue = "missing")
- 此时 required 属性可以继续保持默认值:
@RequestParam(value = "userName", defaultValue = "missing")
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><a th:href="@{/param/one/name/one/value}">一个名字一个值的情况</a><br/></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/param/one/name/one/value")public String oneNameOneValue(// 使用 defaultValue 属性给请求参数设置默认值:@RequestParam(name = "userName", defaultValue = "没有名字?") String userName) {logger.info("ParamHandler.oneNameOneValue的请求参数是:{}", userName);return "target";}}
4.2 一名多值
4.2.1 概述
- 此种场景使用 @RequestParam 注解。
4.2.3 应用示例
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><form method="post" th:action="@{/param/one/name/multi/value}">请选择你最喜欢的球队:<input name="team" type="checkbox" value="Brazil"/>巴西<input name="team" type="checkbox" value="German"/>德国<input name="team" type="checkbox" value="French"/>法国<input name="team" type="checkbox" value="Holland"/>荷兰<input name="team" type="checkbox" value="Italian"/>意大利<input name="team" type="checkbox" value="China"/>中国<br/><input type="submit" value="保存"/></form></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;import java.util.List;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@PostMapping("/param/one/name/multi/value")public String oneNameMultiValue(// 在服务器端 handler 方法中,使用一个能够存储多个数据的容器就能接收一个名字对应的多个值请求参数@RequestParam(name = "team") List<String> team) {logger.info("ParamHandler.oneNameMultiValue:{}", team);return "target";}}
4.3 表单对应模型
4.3.1 表单
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><form method="post" th:action="@{/emp/save}">姓名:<input name="empName" type="text"/><br/>年龄:<input name="empAge" type="text"/><br/>工资:<input name="empSalary" type="text"/><br/><input type="submit" value="保存"/></form></body></html>
4.3.2 实体类
package com.github.fairy.era.mvc.bean;import java.io.Serializable;/*** @author 许大仙* @version 1.0* @since 2021-11-11 15:37*/public class Emp implements Serializable {private String empName;private Integer empAge;private Double empSalary;public String getEmpName() {return empName;}public void setEmpName(String empName) {this.empName = empName;}public Integer getEmpAge() {return empAge;}public void setEmpAge(Integer empAge) {this.empAge = empAge;}public Double getEmpSalary() {return empSalary;}public void setEmpSalary(Double empSalary) {this.empSalary = empSalary;}@Overridepublic String toString() {return "Emp{" +"empName='" + empName + '\'' +", empAge=" + empAge +", empSalary=" + empSalary +'}';}}
4.3.3 handler
package com.github.fairy.era.mvc.handler;import com.github.fairy.era.mvc.bean.Emp;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PostMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@PostMapping(value = "/emp/save")public String formToEntity(// SpringMVC 会自动调用实体类中的 setXxx() 注入请求参数Emp emp) {logger.info("ParamHandler.formToEntity的请求参数是:{}", emp);return "target";}}
4.3.4 POST 请求的字符乱码问题解决
- 到 web.xml 中配置 CharacterEncodingFilter 即可
<!-- 配置过滤器解决 POST 请求的字符乱码问题 --><filter><filter-name>CharacterEncodingFilter</filter-name><filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class><!-- encoding参数指定要使用的字符集名称 --><init-param><param-name>encoding</param-name><param-value>UTF-8</param-value></init-param><!-- 请求强制编码 --><init-param><param-name>forceRequestEncoding</param-name><param-value>true</param-value></init-param><!-- 响应强制编码 --><init-param><param-name>forceResponseEncoding</param-name><param-value>true</param-value></init-param></filter><filter-mapping><filter-name>CharacterEncodingFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping>
- 完整的 web.xml :
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://xmlns.jcp.org/xml/ns/javaee"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><!-- 配置过滤器解决 POST 请求的字符乱码问题 --><filter><filter-name>CharacterEncodingFilter</filter-name><filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class><!-- encoding参数指定要使用的字符集名称 --><init-param><param-name>encoding</param-name><param-value>UTF-8</param-value></init-param><!-- 请求强制编码 --><init-param><param-name>forceRequestEncoding</param-name><param-value>true</param-value></init-param><!-- 响应强制编码 --><init-param><param-name>forceResponseEncoding</param-name><param-value>true</param-value></init-param></filter><filter-mapping><filter-name>CharacterEncodingFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 --><servlet><servlet-name>dispatcherServlet</servlet-name><!-- DispatcherServlet的全类名 --><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!-- 通过初始化参数指定SpringMVC配置文件位置 --><init-param><!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 --><param-name>contextConfigLocation</param-name><!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 --><param-value>classpath:springmvc.xml</param-value></init-param><!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 --><!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 --><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>dispatcherServlet</servlet-name><!-- 对DispatcherServlet来说,url-pattern有两种方式配置 --><!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 --><!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 --><url-pattern>/</url-pattern></servlet-mapping></web-app>
注意:
- 在较低版本的 SpringMVC 中,forceRequestEncoding 属性、forceResponseEncoding 属性没有分开,它们是一个 forceEncoding 属性,这里需要注意一下。
- 由于 CharacterEncodingFilter 是通过 request.setCharacterEncoding(encoding); 来设置请求字符集,所以在此操作前不能有任何的 request.getParameter() 操作。在设置字符集之前获取过请求参数,那么设置字符集的操作将无效。
4.4 表单对应实体类包含级联属性(不常用)
4.4.1 实体类
- School.java
package com.github.fairy.era.mvc.bean;import java.io.Serializable;/*** @author 许大仙* @version 1.0* @since 2021-11-11 16:23*/public class School implements Serializable {private String schoolName;public String getSchoolName() {return schoolName;}public void setSchoolName(String schoolName) {this.schoolName = schoolName;}@Overridepublic String toString() {return "School{" +"schoolName='" + schoolName + '\'' +'}';}}
- Subject.java
package com.github.fairy.era.mvc.bean;import java.io.Serializable;/*** @author 许大仙* @version 1.0* @since 2021-11-11 16:24*/public class Subject implements Serializable {private String subjectName;public String getSubjectName() {return subjectName;}public void setSubjectName(String subjectName) {this.subjectName = subjectName;}@Overridepublic String toString() {return "Subject{" +"subjectName='" + subjectName + '\'' +'}';}}
- Teacher.java
package com.github.fairy.era.mvc.bean;import java.io.Serializable;/*** @author 许大仙* @version 1.0* @since 2021-11-11 16:24*/public class Teacher implements Serializable {private String teacherName;public String getTeacherName() {return teacherName;}public void setTeacherName(String teacherName) {this.teacherName = teacherName;}@Overridepublic String toString() {return "Teacher{" +"teacherName='" + teacherName + '\'' +'}';}}
- Student.java
package com.github.fairy.era.mvc.bean;import java.io.Serializable;import java.util.*;/*** @author 许大仙* @version 1.0* @since 2021-11-11 16:25*/public class Student implements Serializable {private String stuName;private School school;private List<Subject> subjectList;private Subject[] subjectArray;private Set<Teacher> teacherSet;private Map<String, Double> scores;public Student() {//在各种常用数据类型中,只有Set类型需要提前初始化//并且要按照表单将要提交的对象数量进行初始化//Set类型使用非常不便,要尽可能避免使用SetteacherSet = new HashSet<>();teacherSet.add(new Teacher());teacherSet.add(new Teacher());teacherSet.add(new Teacher());teacherSet.add(new Teacher());teacherSet.add(new Teacher());}public String getStuName() {return stuName;}public void setStuName(String stuName) {this.stuName = stuName;}public School getSchool() {return school;}public void setSchool(School school) {this.school = school;}public List<Subject> getSubjectList() {return subjectList;}public void setSubjectList(List<Subject> subjectList) {this.subjectList = subjectList;}public Subject[] getSubjectArray() {return subjectArray;}public void setSubjectArray(Subject[] subjectArray) {this.subjectArray = subjectArray;}public Set<Teacher> getTeacherSet() {return teacherSet;}public void setTeacherSet(Set<Teacher> teacherSet) {this.teacherSet = teacherSet;}public Map<String, Double> getScores() {return scores;}public void setScores(Map<String, Double> scores) {this.scores = scores;}@Overridepublic String toString() {return "Student{" +"stuName='" + stuName + '\'' +", school=" + school +", subjectList=" + subjectList +", subjectArray=" + Arrays.toString(subjectArray) +", teacherSet=" + teacherSet +", scores=" + scores +'}';}}
4.4.2 表单
- 表单项中的 name 属性值必须严格按照级联对象的属性来设定:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><!-- 提交数据的表单 --><form method="post" th:action="@{/param/cascad}">stuName:<input name="stuName" type="text" value="tom"/><br/>school.schoolName:<input name="school.schoolName" type="text" value="atguigu"/><br/>subjectList[0].subjectName:<input name="subjectList[0].subjectName" type="text" value="java"/><br/>subjectList[1].subjectName:<input name="subjectList[1].subjectName" type="text" value="php"/><br/>subjectList[2].subjectName:<input name="subjectList[2].subjectName" type="text" value="javascript"/><br/>subjectList[3].subjectName:<input name="subjectList[3].subjectName" type="text" value="css"/><br/>subjectList[4].subjectName:<input name="subjectList[4].subjectName" type="text" value="vue"/><br/>subjectArray[0].subjectName:<input name="subjectArray[0].subjectName" type="text" value="spring"/><br/>subjectArray[1].subjectName:<input name="subjectArray[1].subjectName" type="text" value="SpringMVC"/><br/>subjectArray[2].subjectName:<input name="subjectArray[2].subjectName" type="text" value="mybatis"/><br/>subjectArray[3].subjectName:<input name="subjectArray[3].subjectName" type="text" value="maven"/><br/>subjectArray[4].subjectName:<input name="subjectArray[4].subjectName" type="text" value="mysql"/><br/>tearcherSet[0].teacherName:<input name="tearcherSet[0].teacherName" type="text" value="t_one"/><br/>tearcherSet[1].teacherName:<input name="tearcherSet[1].teacherName" type="text" value="t_two"/><br/>tearcherSet[2].teacherName:<input name="tearcherSet[2].teacherName" type="text" value="t_three"/><br/>tearcherSet[3].teacherName:<input name="tearcherSet[3].teacherName" type="text" value="t_four"/><br/>tearcherSet[4].teacherName:<input name="tearcherSet[4].teacherName" type="text" value="t_five"/><br/>scores['Chinese']:<input type="text" name="scores['Chinese']" value="100"/><br/>scores['English']:<input name="scores['English']" type="text" value="95"/><br/>scores['Mathematics']:<input name="scores['Mathematics']" type="text" value="88"/><br/>scores['Chemistry']:<input name="scores['Chemistry']" type="text" value="63"/><br/>scores['Biology']:<input name="scores['Biology']" type="text" value="44"/><br/><input type="submit" value="保存"/></form></body></html>
4.4.3 handler
package com.github.fairy.era.mvc.handler;import com.github.fairy.era.mvc.bean.Student;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PostMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@PostMapping(value = "/param/cascad")public String formToNestedEntity(// SpringMVC 自己懂得注入级联属性,只要属性名和对应的getXxx()、setXxx()匹配即可Student student) {logger.info("ParamHandler.formToNestedEntity的请求参数是:{}", student);return "target";}}
4.5 要发送的数据是 List(不常用)
4.5.1 实体类
- Emp.java
package com.github.fairy.era.mvc.bean;import java.io.Serializable;/*** @author 许大仙* @version 1.0* @since 2021-11-11 15:37*/public class Emp implements Serializable {private String empName;private Integer empAge;private Double empSalary;public String getEmpName() {return empName;}public void setEmpName(String empName) {this.empName = empName;}public Integer getEmpAge() {return empAge;}public void setEmpAge(Integer empAge) {this.empAge = empAge;}public Double getEmpSalary() {return empSalary;}public void setEmpSalary(Double empSalary) {this.empSalary = empSalary;}@Overridepublic String toString() {return "Emp{" +"empName='" + empName + '\'' +", empAge=" + empAge +", empSalary=" + empSalary +'}';}}
- EmpParam.java
package com.github.fairy.era.mvc.bean;import java.util.List;/*** @author 许大仙* @version 1.0* @since 2021-11-11 16:39*/public class EmpParam {private List<Emp> empList;public List<Emp> getEmpList() {return empList;}public void setEmpList(List<Emp> empList) {this.empList = empList;}@Overridepublic String toString() {return "EmpParam{" +"empList=" + empList +'}';}}
4.5.2 表单
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><!-- 提交数据的表单 --><form method="post" th:action="@{/param/list/emp}">1号员工姓名:<input name="empList[0].empName" type="text"/><br/>1号员工年龄:<input name="empList[0].empAge" type="text"/><br/>1号员工工资:<input name="empList[0].empSalary" type="text"/><br/>2号员工姓名:<input name="empList[1].empName" type="text"/><br/>2号员工年龄:<input name="empList[1].empAge" type="text"/><br/>2号员工工资:<input name="empList[1].empSalary" type="text"/><br/><button type="submit">保存</button></form></body></html>
4.5.3 handler
package com.github.fairy.era.mvc.handler;import com.github.fairy.era.mvc.bean.EmpParam;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PostMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@PostMapping(value = "/param/list/emp")public String saveEmpList(// SpringMVC 访问这里实体类的setEmpList()方法注入数据EmpParam empParam) {logger.info("ParamHandler.saveEmpList的请求参数是:{}", empParam);return "target";}}
第五章:@RequestHeader 注解
3.1 概述
- 通过这个注解获取请求消息头中的具体数据。
3.2 应用示例
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><!-- 提交数据的表单 --><a th:href="@{/request/header}">@RequestHeader</a></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestHeader;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/request/header")public String getRequestHeader(// 使用 @RequestHeader 注解获取请求消息头信息// name 或 value 属性:指定请求消息头名称// defaultValue 属性:设置默认值@RequestHeader(name = "Accept", defaultValue = "missing") String accept) {logger.info("ParamHandler.getRequestHeader的请求参数是:{}", accept);return "target";}}
第六章:@CookieValue 注解
6.1 概述
- 通过这个注解获取获取当前请求中的 Cookie 数据。
6.2 应用示例
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><!-- 提交数据的表单 --><a th:href="@{/request/cookie}">@CookieValue</a></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.CookieValue;import org.springframework.web.bind.annotation.GetMapping;import javax.servlet.http.HttpSession;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class ParamHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/request/cookie")public String getCookie(// 使用 @CookieValue 注解获取指定名称的 Cookie 数据// name 或 value 属性:指定Cookie 名称// defaultValue 属性:设置默认值@CookieValue(value = "JSESSIONID", defaultValue = "missing") String cookieValue,// 形参位置声明 HttpSession 类型的参数即可获取 HttpSession 对象HttpSession session) {logger.info("cookieValue = " + cookieValue);return "target";}}
第七章:页面跳转(⭐)
7.1 准备工作
7.1.1 概述
- 准备一个地址在前后缀范围之外的页面。
- 让这个页面能够成功访问。
7.1.2 创建范围之外的页面

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title></head><body>other</body></html>
7.1.3 配置访问静态资源
- springmvc.xml
<mvc:annotation-driven/><mvc:default-servlet-handler/>
- 完整的 spingmvc.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xmlns="http://www.springframework.org/schema/beans"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"><!-- 自动扫描包 --><context:component-scan base-package="com.github.fairy.era.mvc.handler" use-default-filters="false"><context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><!-- 配置视图解析器 --><bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver"><property name="order" value="1"/><property name="characterEncoding" value="UTF-8"/><property name="templateEngine"><bean class="org.thymeleaf.spring5.SpringTemplateEngine"><property name="templateResolver"><bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"><!-- 物理视图:视图前缀+逻辑视图+视图后缀 --><!-- 视图前缀 --><property name="prefix" value="/WEB-INF/templates/"/><!-- 视图后缀 --><property name="suffix" value=".html"/><property name="templateMode" value="HTML5"/><property name="characterEncoding" value="UTF-8"/></bean></property></bean></property></bean><mvc:annotation-driven/><mvc:default-servlet-handler/></beans>
7.2 使用指令
7.2.1 转发指令
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><!-- 提交数据的表单 --><a th:href="@{/test/forward/command}">forward</a></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class DemoHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/test/forward/command")public String forwardCommand() {// 需求:要转发前往的目标地址不在视图前缀指定的范围内,// 通过返回逻辑视图、拼接前缀后缀得到的物理视图无法达到目标地址// 转发到指定的地址:return "forward:/other.html";}}
7.2.2 重定向指令
- 示例:
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>首页</title></head><body><!-- 提交数据的表单 --><a th:href="@{/test/redirect/command}">redirect</a></body></html>
package com.github.fairy.era.mvc.handler;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-11 14:58*/@Controllerpublic class DemoHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/test/redirect/command")public String redirectCommand() {// 重定向到指定的地址:// 这个地址由 SpringMVC 框架负责在前面附加 contextPath,所以我们不能加,我们加了就加多了// 框架增加 contextPath 后:/demo/other.html// 我们多加一个:/demo/demo/other.htmlreturn "redirect:/other.html";}}
