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"/>

相关推荐
用户219916797039116 小时前
基于.Net的NetCoreKevin框架中AgentFramework实现AI智能体Skill和工具动态管理和加载
后端
日月云棠16 小时前
6 高级配置:Spring Boot整合、泛化调用与配置指南
java·后端
SE_NAK16 小时前
go-zero 两个限流器都踩了坑,最后自行实现了一个分布式令牌桶
后端
云烟成雨TD16 小时前
Spring AI Alibaba 1.x 系列【58】Spring AI Alibaba Builtin Nodes 模块介绍
java·人工智能·spring
wyu7296116 小时前
SpringBoot学习记录,一个小项目实战
java
苏三说技术16 小时前
Durid和HikariCP,哪个连接池更好?
后端
思考着亮16 小时前
1.DDL(数据定义语言)
后端
小江的记录本16 小时前
【Java基础】反射与注解:核心原理、自定义注解、注解解析方式(附《思维导图》+《面试高频考点清单》)
java·数据结构·python·mysql·spring·面试·maven
她的男孩16 小时前
Spring Boot 3 后台框架的自动配置设计:少写配置,多做组合
后端
ch.ju16 小时前
Java Programming Chapter 4——Composition of classes
java·开发语言