如果你之前用 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——因为:
- Spring Boot 把 DispatcherServlet、嵌入式 Tomcat、Jackson 全自动装配好了;
- 手搭 Spring MVC(配 web.xml + Tomcat)现在已经不是主流做法;
- 你学到的注解和模式,在两种方式下完全一样——只是脚手架不同。
如果你坚持想看”裸 Spring MVC + 外部 Tomcat” 怎么搭,告诉我,我单独写一篇。
第 3 步:理解 Spring MVC 是什么
在写代码前,先搞清楚一件事:Spring MVC 到底做了什么?
你写一个 Java 类,标上 @Controller,里面写一个方法,标上 @RequestMapping("/hello")。之后所有的事——接收请求、解析参数、调用你的方法、把返回值转成 HTTP 响应——都交给 Spring 帮你做。
具体流程(见顶部图):
- 浏览器发请求
GET /hello给 Tomcat; - Tomcat 把请求交给 DispatcherServlet(Spring MVC 的唯一入口);
- DispatcherServlet 找到匹配
/hello的方法; - 把 HTTP 参数转成方法需要的 Java 类型;
- 调用你的方法;
- 你的方法返回一个东西(视图名 / 对象 / 字符串);
- 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 第一关。比看十篇博客强。
评论
还没有评论,来留第一条吧。