🚀Spring MVC
📖 一、Spring MVC 深度解析
1. 为什么抛弃原生 Servlet?😫
在 Spring MVC 出现之前,我们用原生 Servlet 开发 Web 应用简直是"痛苦面具":
- 参数接收太累 :每次都要
request.getParameter(),还要手动类型转换。 - 复用性差:一个 Servlet 实例通常只能处理一种逻辑,代码冗余严重。
- 耦合度高:Servlet 和 HTTP 请求/响应强耦合,测试困难。
✨ Spring MVC 的救赎 :
它基于 MVC 设计模式 ,底层封装了 Servlet,利用反射 和注解,让参数接收自动化、代码结构清晰化!
2. MVC 与三层架构的关系 🏗️
- M (Model): 数据模型 (POJO) + 业务模型 (Service, Mapper)。
- V (View): 视图 (JSP, HTML, Thymeleaf),负责展示。
- C (Controller): 控制器,负责接收请求、调用业务、返回视图。
- 流程 :
用户请求➡️View (发起)➡️Controller (调度)➡️Model (处理)➡️View (渲染)。 - 注意: 三层架构 (Controller/Service/Dao) 是后端逻辑分层,通常不包含 View 层;而 MVC 是包含 View 的整体架构模式。
🛠️ 二、手把手入门案例 (Hello World)
我们来写一个完整的"你好,师姐!"案例,感受全流程!
1. pom.xml 依赖配置
xml
<dependencies>
<!-- Spring 核心上下文 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
<!-- Spring MVC 核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.9</version>
</dependency>
<!-- Servlet API (Tomcat 提供,scope 设为 provided) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- Tomcat7 插件,方便运行 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<port>8080</port>
</configuration>
</plugin>
</plugins>
</build>
2. web.xml 核心配置 (前端控制器)
这是整个应用的入口,所有请求都先经过它!
xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="4.0">
<!-- 配置 DispatcherServlet -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定 Spring MVC 配置文件位置 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- Tomcat 启动时立即加载,数字越小优先级越高 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!--
url-pattern 设置:
"/" : 拦截除 .jsp 外的所有请求 (推荐)
"/*": 拦截所有请求 (包括 .jsp,会导致 JSP 无法访问,慎用)
-->
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 中文乱码过滤器 (针对 POST 请求) -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequest</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponse</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
3. springmvc.xml 配置文件
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 1. IOC 扫描:扫描控制器包 -->
<context:component-scan base-package="com.hg.controller"/>
<!-- 2. 视图解析器:拼接前后缀 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- 3. 开启注解驱动:支持 @RequestMapping, @RequestBody 等 -->
<mvc:annotation-driven/>
<!-- 4. 静态资源映射 (可选,防止静态文件被拦截) -->
<mvc:resources mapping="/css/**" location="/css/"/>
<mvc:resources mapping="/js/**" location="/js/"/>
<mvc:resources mapping="/upload/**" location="/upload/"/>
</beans>
4. Controller 控制器代码
java
package com.hg.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller // 标记为控制器组件
@RequestMapping("/hello") // 类级别映射,窄化路径
public class HelloController {
/**
* 方式一:返回 ModelAndView
* URL: http://localhost:8080/hello/mv
*/
@RequestMapping("/mv")
public ModelAndView testModelAndView() {
ModelAndView mv = new ModelAndView();
// 添加数据到模型 (相当于 request.setAttribute)
mv.addObject("msg", "师姐你好,这是 ModelAndView 方式!");
// 设置逻辑视图名 (会被视图解析器拼成 /WEB-INF/pages/success.jsp)
mv.setViewName("success");
return mv;
}
}
5. 视图页面 (/WEB-INF/pages/success.jsp)
jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>成功页面</title></head>
<body>
<h1>🎉 跳转成功!</h1>
<p>接收到的消息:${msg}</p>
</body>
</html>
🧩 三、核心组件大揭秘 (工作原理)
当你在浏览器输入 URL 后,背后发生了一场精彩的接力赛:
- DispatcherServlet (前端控制器) : 就像前台接待,接收所有请求。
- HandlerMapping (处理器映射器) : 就像导航员,根据 URL 找到对应的 Controller 方法 (Handler)。
- HandlerAdapter (处理器适配器) : 就像执行者,真正去调用那个 Controller 方法。
- Controller (你的代码) : 处理业务,返回
ModelAndView或String。 - ViewResolver (视图解析器) : 就像翻译官 ,把逻辑名
"success"翻译成真实路径/WEB-INF/pages/success.jsp。 - View (视图渲染器): 把数据填充到 JSP 中,生成最终 HTML 返回给用户。
🏷️ 四、@RequestMapping 进阶用法
1. 基本用法与简化
java
@Controller
@RequestMapping("/account") // 类级别:所有方法路径都以 /account 开头
public class AccountController {
// 等价于 @RequestMapping(value="/add", method=RequestMethod.POST)
@PostMapping("/add")
public String add() {
return "addSuccess";
}
// 等价于 @RequestMapping(value="/get", method=RequestMethod.GET)
@GetMapping("/get")
public String get() {
return "getSuccess";
}
// 支持多种请求方式
@RequestMapping(value="/update", method={RequestMethod.PUT, RequestMethod.POST})
public String update() {
return "updateSuccess";
}
}
🔄 五、Handler 返回值详解与代码
1. 返回字符串 (最常用)
java
@GetMapping("/toPage")
public String toPage(Model model) {
// 通过 Model 对象传递数据
model.addAttribute("user", "张三");
model.addAttribute("age", 18);
// 返回逻辑视图名 -> /WEB-INF/pages/userInfo.jsp
return "userInfo";
}
2. 请求转发 (Forward) - 地址栏不变,数据共享
java
@GetMapping("/forwardDemo")
public String forwardDemo() {
// 语法:return "forward:/目标路径";
// 内部会调用 request.getRequestDispatcher().forward()
return "forward:/account/get";
}
3. 重定向 (Redirect) - 地址栏改变,数据不共享
java
@GetMapping("/redirectDemo")
public String redirectDemo() {
// 语法:return "redirect:/目标路径";
// 内部会调用 response.sendRedirect()
// 注意:如果需要传数据,需拼接到 URL 后面或使用 FlashAttribute
return "redirect:/account/login";
}
📥 六、参数接收全攻略 (含代码)
1. 基本类型 & POJO 自动封装
前端表单 (form.jsp):
html
<form action="/account/save" method="post">
姓名:<input type="text" name="name"><br>
年龄:<input type="text" name="age"><br>
余额:<input type="text" name="money"><br>
<input type="submit" value="提交">
</form>
后端 Controller:
java
@PostMapping("/save")
// 参数名必须与表单 name 属性一致,框架自动封装到 Account 对象
public String save(Account account) {
System.out.println("收到账户:" + account);
return "success";
}
2. RESTful 风格 (@PathVariable)
前端链接 : <a href="/account/info/zhangsan/20">查看信息</a>
后端 Controller:
java
// URL: /account/info/{name}/{age}
@GetMapping("/info/{name}/{age}")
public String info(@PathVariable String name, @PathVariable Integer age) {
System.out.println("姓名:" + name + ", 年龄:" + age);
return "success";
}
3. JSON 数据接收 (@RequestBody)
前端 Axios 发送:
javascript
axios.post('/account/jsonSave', {
name: "李四",
age: 25,
money: 1000.5
}).then(res => console.log(res));
后端 Controller:
java
// @RequestBody 将 JSON 字符串自动转换为 Java 对象
@PostMapping("/jsonSave")
@ResponseBody // 返回 JSON 给前端
public Result jsonSave(@RequestBody Account account) {
System.out.println("收到JSON对象:" + account);
return new Result(200, "保存成功", null);
}
🛡️ 七、拦截器 (Interceptor) 实战
用于登录校验、权限控制等。
1. 自定义拦截器
java
package com.hg.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
// 预处理:在 Controller 方法执行前调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println(">>> 拦截器:预处理...");
// 模拟登录校验
Object user = request.getSession().getAttribute("currentUser");
if (user == null) {
// 未登录,重定向到登录页
response.sendRedirect(request.getContextPath() + "/login.jsp");
return false; // 阻断后续操作
}
return true; // 放行
}
// 后处理:Controller 方法执行后,视图渲染前
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println(">>> 拦截器:后处理...");
}
// 完成:视图渲染完成后
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println(">>> 拦截器:完成...");
}
}
2. 配置拦截器 (springmvc.xml)
xml
<mvc:interceptors>
<mvc:interceptor>
<!-- 拦截所有路径 -->
<mvc:mapping path="/**"/>
<!-- 排除登录页面和静态资源 -->
<mvc:exclude-mapping path="/login.jsp"/>
<mvc:exclude-mapping path="/user/login"/>
<mvc:exclude-mapping path="/css/**"/>
<mvc:exclude-mapping path="/js/**"/>
<bean class="com.hg.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
📁 八、文件上传完整示例
1. 依赖 (pom.xml)
xml
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
2. 配置解析器 (springmvc.xml)
注意 : Bean 的 ID 必须 是 multipartResolver!
xml
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 最大上传大小:5MB (单位字节) -->
<property name="maxUploadSize" value="5242880"/>
<!-- 默认编码 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
3. 前端表单
关键点 : enctype="multipart/form-data" 和 method="post"
html
<form action="/account/upload" method="post" enctype="multipart/form-data">
请选择文件:<input type="file" name="headImg"><br>
<input type="submit" value="上传">
</form>
4. 后端 Controller
java
@PostMapping("/upload")
public String upload(MultipartFile headImg, HttpServletRequest request) throws Exception {
if (!headImg.isEmpty()) {
// 获取上传路径 (例如:WebContent/upload/)
String realPath = request.getServletContext().getRealPath("/upload");
File dir = new File(realPath);
if (!dir.exists()) {
dir.mkdirs();
}
// 生成唯一文件名,防止覆盖
String fileName = UUID.randomUUID().toString() + "_" + headImg.getOriginalFilename();
File dest = new File(dir, fileName);
// 执行上传
headImg.transferTo(dest);
System.out.println("文件上传成功:" + fileName);
return "success";
}
return "error";
}
📁 九、中文乱码与日期转换器补充
1. GET 请求乱码处理
虽然 CharacterEncodingFilter 解决了 POST 乱码,但 GET 请求在某些旧版 Tomcat 中仍可能乱码。
手动解决代码:
java
String name = request.getParameter("name");
if (name != null) {
// ISO-8859-1 转 UTF-8
name = new String(name.getBytes("ISO-8859-1"), "UTF-8");
}
(注:Tomcat 8+ 默认已配置 URIEncoding=UTF-8,通常无需手动处理)
2. 自定义日期转换器
如果前端传来的日期格式是 "2023-10-01",而后台是 Date 类型,需要转换器。
定义转换器:
java
public class DateConverter implements Converter<String, Date> {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@Override
public Date convert(String source) {
try {
return sdf.parse(source);
} catch (ParseException e) {
throw new IllegalArgumentException("日期格式错误,应为 yyyy-MM-dd");
}
}
}
注册转换器 (springmvc.xml):
xml
<!-- 定义转换服务工厂 -->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.hg.converter.DateConverter"/>
</set>
</property>
</bean>
<!-- 在 annotation-driven 中引用 -->
<mvc:annotation-driven conversion-service="conversionService"/>