Spring MVC

🚀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 后,背后发生了一场精彩的接力赛:

  1. DispatcherServlet (前端控制器) : 就像前台接待,接收所有请求。
  2. HandlerMapping (处理器映射器) : 就像导航员,根据 URL 找到对应的 Controller 方法 (Handler)。
  3. HandlerAdapter (处理器适配器) : 就像执行者,真正去调用那个 Controller 方法。
  4. Controller (你的代码) : 处理业务,返回 ModelAndViewString
  5. ViewResolver (视图解析器) : 就像翻译官 ,把逻辑名 "success" 翻译成真实路径 /WEB-INF/pages/success.jsp
  6. 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"/>

相关推荐
追风林2 小时前
idea支持本地 的 服务器 远程debug
java·服务器·intellij-idea
美团技术团队2 小时前
美团 BI 在指标平台和分析引擎上的探索和实践
后端
凸头2 小时前
AI 流式聊天接口实现(WebFlux+SSE)
java·人工智能
简宸~2 小时前
VS Code + LaTex + SumatraPDF联合使用指南
java·vscode·latex·sumatrapdf
弦有三种苦难2 小时前
CCF-202412-T3缓存模拟90分
java·开发语言·spring
青槿吖2 小时前
SpringMVC通关秘籍(下):日期转换器、拦截器与文件上传的奇幻冒险
java·开发语言·数据库·sql·mybatis·状态模式
JimmtButler2 小时前
我用 Claude Code 给 Claude Code 做了一个 DevTools
后端·claude
weixin_456321643 小时前
Java架构设计:Redis AOF持久化深度解析(原理+实战+避坑)
java·开发语言·redis
leaves falling3 小时前
数据结构-堆学习
java·数据结构·学习