从零学 Spring MVC:一个请求被处理的全过程

项目实战 · 约 34 分钟

Spring MVC 是什么 · 一张图入门

如果你之前用 Servlet + JSP 写过 Web 项目,或者完全没碰过 Java Web,跟着本文做完一遍,你就能独立写 Spring MVC 项目了。本文是 Spring MVC 核心机制深度解析 的”前置篇”——那篇讲”框架为什么这样设计”,这篇讲”框架怎么从零开始用”。

第 1 步:准备环境

Spring MVC 6.x 要求 JDK 17+,IDE 任意(IntelliJ IDEA、Eclipse、VS Code 都可以)。

# 验证 JDK
java -version   # 期望 17.x 或更高

# 验证 Maven
mvn -v          # 期望 3.8.x 或更高

环境没装好?先去 adoptium.net 装 JDK,到 maven.apache.org 下二进制包解压。Maven 解压后把 bin/ 加进系统 PATH 即可。

小贴士:Maven 下载依赖可能慢,国内可配华为云镜像:

mkdir -p ~/.m2
cat > ~/.m2/settings.xml << 'EOF'
<settings>
  <mirrors>
    <mirror>
      <id>huawei</id>
      <mirrorOf>central</mirrorOf>
      <url>https://repo.huaweicloud.com/repository/maven/</url>
    </mirror>
  </mirrors>
</settings>
EOF

第 2 步:用 Maven 创建项目骨架

建一个空目录,比如 spring-mvc-demo,然后手动放一个 pom.xml:

mkdir -p spring-mvc-demo
cd spring-mvc-demo

pom.xml 内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>spring-mvc-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Spring MVC(包含嵌入式 Tomcat) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 校验注解 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>
</project>

这里我们直接用 Spring Boot,而不是手搭 Spring MVC——因为:

  1. Spring Boot 把 DispatcherServlet、嵌入式 Tomcat、Jackson 全自动装配好了;
  2. 手搭 Spring MVC(配 web.xml + Tomcat)现在已经不是主流做法;
  3. 你学到的注解和模式,在两种方式下完全一样——只是脚手架不同。

如果你坚持想看”裸 Spring MVC + 外部 Tomcat” 怎么搭,告诉我,我单独写一篇。

第 3 步:理解 Spring MVC 是什么

在写代码前,先搞清楚一件事:Spring MVC 到底做了什么?

你写一个 Java 类,标上 @Controller,里面写一个方法,标上 @RequestMapping("/hello")。之后所有的事——接收请求、解析参数、调用你的方法、把返回值转成 HTTP 响应——都交给 Spring 帮你做。

具体流程(见顶部图):

  1. 浏览器发请求 GET /hello 给 Tomcat;
  2. Tomcat 把请求交给 DispatcherServlet(Spring MVC 的唯一入口);
  3. DispatcherServlet 找到匹配 /hello 的方法;
  4. 把 HTTP 参数转成方法需要的 Java 类型;
  5. 调用你的方法;
  6. 你的方法返回一个东西(视图名 / 对象 / 字符串);
  7. DispatcherServlet 把返回值变成 HTTP 响应给浏览器。

作为初学者,你只需要关心一件事:写好你的 Controller 方法。 其他都是框架在做。

第 4 步:写第一个程序

按 Maven 约定建包目录:

mkdir -p src/main/java/com/example/demo
mkdir -p src/main/resources
mkdir -p src/main/resources/templates

src/main/java/com/example/demo/DemoApplication.java——启动类:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication   // 一行注解搞定 Spring Boot 自动配置
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

src/main/java/com/example/demo/web/HelloController.java——第一个 Controller:

package com.example.demo.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller   // 告诉 Spring:这是个控制器
public class HelloController {

    // 浏览器访问 http://localhost:8080/hello 时会走到这里
    @GetMapping("/hello")
    @ResponseBody   // 把返回值直接写回响应(不渲染视图)
    public String hello() {
        return "Hello, Spring MVC!";
    }
}

src/main/resources/application.yml——配置文件:

server:
  port: 8080   # 默认就是 8080,这里显式写出来方便改

跑起来:

mvn spring-boot:run

等控制台出现 Started DemoApplication in 2.x seconds,浏览器打开 http://localhost:8080/hello,看到:

Hello, Spring MVC!

恭喜,第一个 Spring MVC 接口跑通了。 整个过程你只写了两个 Java 类和一个 YAML——其他都是 Spring Boot 帮你做的。

第 5 步:让方法接收参数

实际的接口几乎都需要参数。Spring MVC 提供四种最常用的参数接收方式。

5.1 查询参数:@RequestParam

对应 URL 里的 ?key=value:

@GetMapping("/greet")
@ResponseBody
public String greet(@RequestParam String name) {
    return "你好, " + name + "!";
}

访问 http://localhost:8080/greet?name=阳熙 得到 你好, 阳熙!。

@RequestParam 默认必填——不传就 400。要可选:

@RequestParam(required = false) String name
// 或带默认值
@RequestParam(defaultValue = "访客") String name

5.2 路径变量:@PathVariable

把参数写进 URL 路径里(RESTful 风格):

@GetMapping("/users/{id}")
@ResponseBody
public String getUser(@PathVariable Long id) {
    return "查询用户 " + id;
}

访问 http://localhost:8080/users/42 得到 查询用户 42。

5.3 请求体:@RequestBody(接收 JSON)

@PostMapping("/users")
@ResponseBody
public String createUser(@RequestBody UserDTO user) {
    return "创建用户 " + user.getName() + ", 年龄 " + user.getAge();
}

public static class UserDTO {
    private String name;
    private Integer age;
    // 省略 getter / setter
    // 或者用 record:
    // public record UserDTO(String name, Integer age) {}
}

用 curl 测试:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"阳熙","age":28}'

Spring 会自动用 Jackson 把 JSON 转成 UserDTO。

5.4 多个参数 → 直接绑到对象

如果参数很多,别一个个 @RequestParam,直接定义一个类:

@GetMapping("/search")
@ResponseBody
public String search(SearchQuery q) {
    return "搜索: " + q.getKeyword() + ", 排序: " + q.getSort();
}

public static class SearchQuery {
    private String keyword;
    private String sort = "asc";   // 默认值
    // getter / setter
}

访问 /search?keyword=java&sort=desc 自动绑定。

第 6 步:返回数据

6.1 返回 JSON(最常用)

把 @Controller + @ResponseBody 换成 @RestController:

@RestController   // = @Controller + @ResponseBody(类级别)
public class UserController {

    @GetMapping("/api/users/{id}")
    public Map<String, Object> getUser(@PathVariable Long id) {
        Map<String, Object> user = new HashMap<>();
        user.put("id", id);
        user.put("name", "阳熙");
        user.put("age", 28);
        return user;  // 自动转 JSON
    }
}

访问 /api/users/42:

{"id": 42, "name": "阳熙", "age": 28}

实际项目里通常返回一个包装好的 Result<T>:

public class Result<T> {
    private int code;
    private String message;
    private T data;

    public static <T> Result<T> ok(T data) {
        Result<T> r = new Result<>();
        r.code = 0;
        r.message = "ok";
        r.data = data;
        return r;
    }
    // 省略 getter / setter
}

// Controller
@GetMapping("/api/users/{id}")
public Result<Map<String, Object>> getUser(@PathVariable Long id) {
    Map<String, Object> user = new HashMap<>();
    user.put("id", id);
    user.put("name", "阳熙");
    return Result.ok(user);
}

6.2 返回视图(老派做法)

如果你要渲染 HTML 页面(前后端不分离的项目),用 @Controller + 模板引擎。Spring Boot 推荐 Thymeleaf:

<!-- pom.xml 加一个依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

写一个 Controller 返回视图名:

@Controller
public class PageController {

    @GetMapping("/welcome")
    public String welcome() {
        return "welcome";   // 找 src/main/resources/templates/welcome.html
    }
}

src/main/resources/templates/welcome.html:

<!DOCTYPE html>
<html>
<head><title>欢迎</title></head>
<body>
    <h1 th:text="${message}">默认文字</h1>
</body>
</html>

th:text="${message}" 表示用 message 模型数据替换标签内容。如果 Controller 没传 message,浏览器看到”默认文字”。

传数据给视图:

@GetMapping("/welcome")
public String welcome(Model model) {
    model.addAttribute("message", "Hello from Spring MVC!");
    return "welcome";
}

到 2025 年新项目几乎都是前后端分离(React/Vue 调上面那种 REST 接口),所以你重点掌握 6.1 返回 JSON 就够了;视图渲染了解即可。

第 7 步:数据校验

用户传进来的数据你不能直接信——必须校验。Spring 用 Bean Validation(Jakarta Validation)标准。

给 DTO 字段加注解:

public class UserDTO {
    @NotBlank(message = "姓名不能为空")
    private String name;

    @Min(value = 0, message = "年龄不能小于 0")
    @Max(value = 150, message = "年龄不能大于 150")
    private Integer age;

    @Email(message = "邮箱格式不正确")
    private String email;

    // getter / setter
}

Controller 里在参数前加 @Valid:

@PostMapping("/api/users")
public Result<UserDTO> createUser(@Valid @RequestBody UserDTO user) {
    // 到这里说明校验通过
    return Result.ok(user);
}

校验失败会抛 MethodArgumentNotValidException,浏览器收到 400 错误(默认)。想自定义错误响应,看下一步。

第 8 步:统一异常处理

实际项目里所有 Controller 共用一个异常处理逻辑——用 @RestControllerAdvice + @ExceptionHandler:

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 校验失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage())
            .collect(Collectors.joining("; "));
        return Result.fail(400, msg);
    }

    // 参数缺失
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public Result<Void> handleMissingParam(MissingServletRequestParameterException e) {
        return Result.fail(400, "缺少参数: " + e.getParameterName());
    }

    // 兜底
    @ExceptionHandler(Exception.class)
    public Result<Void> handleUnknown(Exception e) {
        e.printStackTrace();   // 生产环境改成 log.error
        return Result.fail(500, "系统繁忙");
    }
}
// Result 类加一个 fail 静态方法
public static <T> Result<T> fail(int code, String message) {
    Result<T> r = new Result<>();
    r.code = code;
    r.message = message;
    return r;
}

这样所有 Controller 抛出的异常都会自动被翻译成统一格式。

第 9 步:拦截器(可选)

拦截器可以在请求进入 Controller 之前/之后加逻辑——比如登录校验、权限检查:

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        // 假设登录后 session 里放 user
        if (req.getSession().getAttribute("user") == null) {
            res.setStatus(401);
            return false;  // 拦截,不进入 Controller
        }
        return true;
    }
}

注册拦截器:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/api/**")    // 拦截哪些路径
                .excludePathPatterns("/api/login");   // 排除哪些
    }
}

第 10 步:完整项目结构

到这里你已经有了能跑的项目:

spring-mvc-demo/
├── pom.xml
└── src/main/
    ├── java/com/example/demo/
    │   ├── DemoApplication.java            ← 启动类
    │   ├── web/
    │   │   ├── HelloController.java        ← 简单示例
    │   │   ├── UserController.java         ← REST 接口
    │   │   ├── PageController.java         ← 视图渲染(可选)
    │   │   └── dto/
    │   │       ├── UserDTO.java
    │   │       └── SearchQuery.java
    │   ├── common/
    │   │   ├── Result.java                 ← 统一返回结构
    │   │   └── GlobalExceptionHandler.java ← 统一异常处理
    │   └── config/
    │       └── WebConfig.java              ← 拦截器、CORS 等配置
    └── resources/
        ├── application.yml
        └── templates/
            └── welcome.html

这就是一个 Spring MVC 项目的标准骨架。真实项目只是这里加业务代码、加更多 Controller、加 Service / Repository 层。

第 11 步:常见错误

11.1 404:路径没匹配上

Whitelabel Error Page ... No mapping for GET /hello

检查三件事:

  • Controller 类有没有 @Controller / @RestController;
  • 方法有没有 @GetMapping("/hello")(注意路径大小写);
  • 包路径是否在 @SpringBootApplication 同包或子包(默认只扫描启动类所在包)。

11.2 415:不支持的 Media Type

HttpMessageNotReadableException ... Content type 'application/x-www-form-urlencoded' not supported

说明 Controller 用 @RequestBody 接 JSON,但请求是表单。两种修法:

// 方法 1:换 @RequestParam
public String create(@RequestParam String name) { ... }

// 方法 2:显式声明接受表单
@PostMapping(value = "/users", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String create(@RequestParam String name) { ... }

11.3 静态资源访问 404

Spring Boot 默认从以下位置找静态资源:classpath:/static/、classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/。

mkdir -p src/main/resources/static
echo "<h1>hello</h1>" > src/main/resources/static/test.html
# 访问 http://localhost:8080/test.html

11.4 中文乱码

请求/响应乱码大多是字符编码问题。Spring Boot 2.x 之后默认 UTF-8 一般不会出问题,但如果出现:

# application.yml
server:
  servlet:
    encoding:
      charset: UTF-8
      force: true

第 12 步:用 Spring Boot 简化一切(你已经在了)

你可能注意到,本文全程用 Spring Boot 起项目,没有配一行 web.xml。这是因为:

裸 Spring MVC 时代Spring Boot 时代
手写 web.xml 注册 DispatcherServlet自动注册
部署到外部 Tomcat内嵌 Tomcat,开箱即用
手配 @ComponentScan 路径默认扫启动类同包及子包
手配 ViewResolver、MessageConverter自动配置(autoconfigure)

对你来说,这两者的区别仅仅是”要不要写 50 行 XML”。注解、Controller 写法和返回 JSON 的逻辑完全一样。

完整代码清单(可复制)

// DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
// UserController.java
package com.example.demo.web;

import com.example.demo.common.Result;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public Result<Map<String, Object>> getUser(@PathVariable Long id) {
        Map<String, Object> user = new HashMap<>();
        user.put("id", id);
        user.put("name", "阳熙");
        return Result.ok(user);
    }

    @PostMapping
    public Result<UserDTO> createUser(@Valid @RequestBody UserDTO user) {
        return Result.ok(user);
    }
}
// UserDTO.java
package com.example.demo.web;

import jakarta.validation.constraints.*;
public class UserDTO {
    @NotBlank
    private String name;
    @Min(0) @Max(150)
    private Integer age;
    @Email
    private String email;
    // getter / setter ...
}
// Result.java
package com.example.demo.common;
public class Result<T> {
    private int code;
    private String message;
    private T data;
    public static <T> Result<T> ok(T data) {
        Result<T> r = new Result<>();
        r.code = 0; r.message = "ok"; r.data = data; return r;
    }
    public static <T> Result<T> fail(int code, String message) {
        Result<T> r = new Result<>();
        r.code = code; r.message = message; return r;
    }
    // getter / setter ...
}
// GlobalExceptionHandler.java
package com.example.demo.common;

import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage())
            .collect(Collectors.joining("; "));
        return Result.fail(400, msg);
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public Result<Void> handleMissingParam(MissingServletRequestParameterException e) {
        return Result.fail(400, "缺少参数: " + e.getParameterName());
    }

    @ExceptionHandler(Exception.class)
    public Result<Void> handleUnknown(Exception e) {
        e.printStackTrace();
        return Result.fail(500, "系统繁忙");
    }
}
# src/main/resources/application.yml
server:
  port: 8080
mvn spring-boot:run
# 访问 http://localhost:8080/api/users/42
# POST 测试: curl -X POST http://localhost:8080/api/users \
#   -H "Content-Type: application/json" -d '{"name":"阳熙","age":28}'

下一步学什么

你已掌握建议继续
Hello World接入数据库(点餐后端教程)
参数绑定复杂参数(List、自定义类型、嵌套对象)
数据校验自定义校验注解
统一异常文件上传与下载、异步接口
拦截器Spring Security 安全框架
Spring Boot 起项目Spring Cloud 微服务

学完这些后,可以去看 Spring MVC 核心机制深度解析——那篇会用更深的视角解释”为什么框架这样设计”,对你从”会用”到”懂原理”很重要。

把上面的代码敲一遍、跑一遍、改一改,你就过了 Spring MVC 第一关。比看十篇博客强。

阳熙

Java / 保险业务系统开发 · 无锡

← 返回文章列表

评论

还没有评论,来留第一条吧。

首次评论会做人机校验