概述
Spring MVC 是 Spring Framework 的一个模块,是一个基于 MVC 架构模式的 Web 框架。它提供了一个灵活且功能强大的 Web 应用程序开发解决方案。
核心特性
- MVC 架构:清晰的模型-视图-控制器分离
- 灵活的配置:支持 XML 和注解配置
- 强大的数据绑定:自动的请求参数绑定
- 视图技术无关:支持多种视图技术
- RESTful 支持:完整的 REST 风格API 支持
应用场景
| 场景 | 描述 | 示例 |
|---|---|---|
| Web 应用开发 | 传统的 Web 应用程序 | 企业管理系统、电商平台 |
| RESTful API | 构建 REST 风格的 API | 微服务接口、移动端 API |
| 前后端分离 | 作为后端 API 服务 | Vue/React + Spring MVC |
| 企业级应用 | 大型企业应用开发 | ERP、CRM 系统 |
💡 提示: Spring MVC 是 Spring Boot 的基础,掌握 Spring MVC 对理解 Spring Boot 至关重要。
环境准备
系统要求
| 工具 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8+ | Java 开发环境 |
| Maven | 3.6+ | 项目管理工具 |
| Tomcat | 9.0+ | Servlet 容器 |
| IDEA | 2020+ | 集成开发环境 |
依赖配置
<dependencies>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.21</version>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- JSP API -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<!-- JSTL -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
核心组件
MVC 架构图
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ View │ │ Controller │ │ Model │
│ (视图) │◀───│ (控制器) │───▶│ (模型) │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ Service │
│ (业务层) │
└─────────────┘
核心组件说明
| 组件 | 作用 | 示例 |
|---|---|---|
| DispatcherServlet | 前端控制器,统一处理请求 | 请求分发和响应处理 |
| HandlerMapping | 处理器映射器,找到对应的处理器 | URL 到 Controller 的映射 |
| HandlerAdapter | 处理器适配器,调用具体处理器 | 适配不同类型的 Controller |
| Controller | 控制器,处理业务逻辑 | 用户请求处理 |
| ViewResolver | 视图解析器,解析视图名称 | JSP、Thymeleaf 视图解析 |
| View | 视图,展示数据 | JSP 页面、JSON 响应 |
请求处理流程
1. 用户请求 → 2. DispatcherServlet → 3. HandlerMapping → 4. HandlerAdapter
↓ ↓ ↓
5. Controller → 6. ModelAndView → 7. ViewResolver → 8. View
↓ ↓ ↓
9. 响应用户 ← 10. 渲染视图 ← 11. 视图对象 ← 12. 视图解析
项目搭建
1. 创建 Maven 项目
项目结构:
spring-mvc-demo/
├── pom.xml
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── controller/
│ │ └── FirstController.java
│ ├── resources/
│ │ └── springmvc-config.xml
│ └── webapp/
│ ├── WEB-INF/
│ │ ├── web.xml
│ │ └── jsp/
│ │ └── first.jsp
│ └── index.jsp
└── target/
2. 配置前端控制器
web.xml 配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Spring MVC Demo</display-name>
<!-- 配置前端控制器 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 字符编码过滤器 -->
<filter>
<filter-name>characterEncodingFilter</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>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
🔧 配置说明:
DispatcherServlet: Spring MVC 的前端控制器contextConfigLocation: 指定 Spring MVC 配置文件位置load-on-startup: 设置为 1 表示启动时加载url-pattern: 拦截所有请求(除了 JSP)
3. 创建控制器
FirstController.java:
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller
public class FirstController {
@RequestMapping("/first")
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
// 添加数据到模型
modelAndView.addObject("msg", "这是我的第一个 Spring MVC 程序");
modelAndView.addObject("currentTime", new java.util.Date());
// 设置视图名称
modelAndView.setViewName("first");
return modelAndView;
}
}
传统 Controller 接口方式:
package com.example.controller;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class FirstController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView modelAndView = new ModelAndView();
// 添加数据
modelAndView.addObject("msg", "这是我的第一个 Spring MVC 程序");
// 设置视图路径
modelAndView.setViewName("/WEB-INF/jsp/first.jsp");
return modelAndView;
}
}
💡 发展说明:
- 传统方式: 实现 Controller 接口,需要在 XML 中配置
- 注解方式: 使用 @Controller 注解,更加灵活和现代化
4. 创建视图文件
first.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>Spring MVC 示例</title>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
margin: 50px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.message {
color: #2c3e50;
font-size: 18px;
margin-bottom: 10px;
}
.time {
color: #7f8c8d;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>Spring MVC 成功运行</h1>
<div class="message">${msg}</div>
<div class="time">
当前时间:<fmt:formatDate value="${currentTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
</div>
</div>
</body>
</html>
配置详解
1. Spring MVC 配置文件
springmvc-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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">
<!-- 开启注解扫描 -->
<context:component-scan base-package="com.example.controller"/>
<!-- 开启 Spring MVC 注解支持 -->
<mvc:annotation-driven/>
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- 配置静态资源处理 -->
<mvc:resources mapping="/static/**" location="/static/"/>
<!-- 配置默认 servlet 处理器 -->
<mvc:default-servlet-handler/>
</beans>
传统方式配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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">
<!-- 配置控制器 -->
<bean name="/firstController" class="com.example.controller.FirstController"/>
<!-- 处理器映射器 -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<!-- 处理器适配器 -->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"/>
</beans>
2. 组件配置详解
| 组件 | 作用 | 配置方式 |
|---|---|---|
| HandlerMapping | URL 到 Controller 的映射 | BeanNameUrlHandlerMapping, RequestMappingHandlerMapping |
| HandlerAdapter | 适配不同类型的 Controller | SimpleControllerHandlerAdapter, RequestMappingHandlerAdapter |
| ViewResolver | 视图解析 | InternalResourceViewResolver, ThymeleafViewResolver |
| MultipartResolver | 文件上传处理 | CommonsMultipartResolver |
3. 注解配置方式
JavaConfig 配置:
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example.controller")
public class WebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/jsp/");
resolver.setSuffix(".jsp");
return resolver;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("/static/");
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
控制器开发
1. 注解式控制器
@Controller
@RequestMapping("/user")
public class UserController {
// 处理 GET 请求
@GetMapping("/list")
public ModelAndView getUserList() {
ModelAndView mv = new ModelAndView();
// 模拟数据
List<User> users = Arrays.asList(
new User(1, "张三", "zhangsan@example.com"),
new User(2, "李四", "lisi@example.com")
);
mv.addObject("users", users);
mv.setViewName("user/list");
return mv;
}
// 处理 POST 请求
@PostMapping("/add")
public String addUser(@ModelAttribute User user, Model model) {
// 保存用户逻辑
System.out.println("添加用户:" + user.getName());
model.addAttribute("message", "用户添加成功");
return "redirect:/user/list";
}
// 处理路径参数
@GetMapping("/detail/{id}")
public ModelAndView getUserDetail(@PathVariable Integer id) {
ModelAndView mv = new ModelAndView();
// 模拟查询用户
User user = new User(id, "用户" + id, "user" + id + "@example.com");
mv.addObject("user", user);
mv.setViewName("user/detail");
return mv;
}
// 处理请求参数
@GetMapping("/search")
public ModelAndView searchUsers(@RequestParam(value = "keyword", defaultValue = "") String keyword) {
ModelAndView mv = new ModelAndView();
// 模拟搜索逻辑
List<User> users = keyword.isEmpty() ?
Collections.emptyList() :
Arrays.asList(new User(1, keyword, keyword + "@example.com"));
mv.addObject("users", users);
mv.addObject("keyword", keyword);
mv.setViewName("user/search");
return mv;
}
}
2. RESTful 风格控制器
@RestController
@RequestMapping("/api/users")
public class UserRestController {
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = Arrays.asList(
new User(1, "张三", "zhangsan@example.com"),
new User(2, "李四", "lisi@example.com")
);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Integer id) {
User user = new User(id, "用户" + id, "user" + id + "@example.com");
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<String> createUser(@RequestBody User user) {
// 保存用户逻辑
System.out.println("创建用户:" + user.getName());
return ResponseEntity.status(HttpStatus.CREATED)
.body("用户创建成功");
}
@PutMapping("/{id}")
public ResponseEntity<String> updateUser(@PathVariable Integer id, @RequestBody User user) {
// 更新用户逻辑
System.out.println("更新用户 " + id + ": " + user.getName());
return ResponseEntity.ok("用户更新成功");
}
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteUser(@PathVariable Integer id) {
// 删除用户逻辑
System.out.println("删除用户: " + id);
return ResponseEntity.ok("用户删除成功");
}
}
视图解析
1. JSP 视图解析器
<!-- JSP 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
</bean>
2. Thymeleaf 视图解析器
<!-- Thymeleaf 模板引擎 -->
<bean id="templateResolver" class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/"/>
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
<bean id="templateEngine" class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver"/>
</bean>
<bean class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
3. JSON 视图解析器
@Controller
public class JsonController {
@RequestMapping(value = "/api/user", produces = "application/json")
@ResponseBody
public User getUser() {
return new User(1, "张三", "zhangsan@example.com");
}
// 或者使用 @RestController
@GetMapping("/api/users")
public List<User> getUsers() {
return Arrays.asList(
new User(1, "张三", "zhangsan@example.com"),
new User(2, "李四", "lisi@example.com")
);
}
}
请求处理
1. 请求参数处理
@Controller
public class ParamController {
// 单个参数
@GetMapping("/param")
public String handleParam(@RequestParam String name, Model model) {
model.addAttribute("name", name);
return "param";
}
// 可选参数
@GetMapping("/optional")
public String handleOptional(@RequestParam(value = "age", defaultValue = "18") Integer age, Model model) {
model.addAttribute("age", age);
return "optional";
}
// 多个参数
@GetMapping("/multiple")
public String handleMultiple(
@RequestParam String name,
@RequestParam Integer age,
@RequestParam(required = false) String email,
Model model) {
model.addAttribute("name", name);
model.addAttribute("age", age);
model.addAttribute("email", email);
return "multiple";
}
// 数组参数
@GetMapping("/array")
public String handleArray(@RequestParam String[] hobbies, Model model) {
model.addAttribute("hobbies", Arrays.asList(hobbies));
return "array";
}
}
2. 路径参数处理
@Controller
@RequestMapping("/path")
public class PathController {
// 单个路径参数
@GetMapping("/user/{id}")
public String getUser(@PathVariable Long id, Model model) {
model.addAttribute("userId", id);
return "user";
}
// 多个路径参数
@GetMapping("/user/{id}/post/{postId}")
public String getUserPost(
@PathVariable Long id,
@PathVariable Long postId,
Model model) {
model.addAttribute("userId", id);
model.addAttribute("postId", postId);
return "userPost";
}
// 正则表达式限制
@GetMapping("/article/{id:\\d+}")
public String getArticle(@PathVariable Integer id, Model model) {
model.addAttribute("articleId", id);
return "article";
}
}
3. 请求头处理
@Controller
public class HeaderController {
// 获取单个请求头
@GetMapping("/header")
public String handleHeader(@RequestHeader("User-Agent") String userAgent, Model model) {
model.addAttribute("userAgent", userAgent);
return "header";
}
// 获取所有请求头
@GetMapping("/headers")
public String handleHeaders(@RequestHeader Map<String, String> headers, Model model) {
model.addAttribute("headers", headers);
return "headers";
}
// Cookie 处理
@GetMapping("/cookie")
public String handleCookie(@CookieValue(value = "sessionId", defaultValue = "none") String sessionId, Model model) {
model.addAttribute("sessionId", sessionId);
return "cookie";
}
}
数据绑定
1. 表单数据绑定
// 用户实体类
public class User {
private Long id;
private String name;
private String email;
private Integer age;
private Date birthday;
// 构造函数、getter 和 setter
}
@Controller
@RequestMapping("/form")
public class FormController {
// 显示表单
@GetMapping("/user")
public String showForm(Model model) {
model.addAttribute("user", new User());
return "form/user";
}
// 处理表单提交
@PostMapping("/user")
public String submitForm(@ModelAttribute User user, BindingResult result, Model model) {
// 验证数据
if (user.getName() == null || user.getName().trim().isEmpty()) {
result.rejectValue("name", "error.user.name.empty", "用户名不能为空");
}
if (result.hasErrors()) {
return "form/user";
}
// 处理业务逻辑
System.out.println("保存用户:" + user.getName());
model.addAttribute("message", "用户保存成功");
return "redirect:/form/user";
}
}
2. JSON 数据绑定
@RestController
@RequestMapping("/api")
public class ApiController {
// 接收 JSON 数据
@PostMapping("/user")
public ResponseEntity<String> createUser(@RequestBody User user) {
// 数据验证
if (user.getName() == null || user.getName().trim().isEmpty()) {
return ResponseEntity.badRequest().body("用户名不能为空");
}
// 业务处理
System.out.println("创建用户:" + user.getName());
return ResponseEntity.ok("用户创建成功");
}
// 返回 JSON 数据
@GetMapping("/user/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = new User();
user.setId(id);
user.setName("用户" + id);
user.setEmail("user" + id + "@example.com");
return ResponseEntity.ok(user);
}
}
异常处理
1. 全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 处理验证异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
Map<String, String> errors = new HashMap<>();
result.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ErrorResponse errorResponse = new ErrorResponse("参数验证失败", errors);
return ResponseEntity.badRequest().body(errorResponse);
}
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
logger.warn("业务异常: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(ex.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
}
// 处理系统异常
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
logger.error("系统异常", ex);
ErrorResponse errorResponse = new ErrorResponse("系统内部错误");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
// 处理 404 错误
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(NoHandlerFoundException ex) {
logger.warn("请求路径不存在: {}", ex.getRequestURL());
ErrorResponse errorResponse = new ErrorResponse("请求的资源不存在");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
// 错误响应类
public class ErrorResponse {
private String message;
private Map<String, String> errors;
private long timestamp;
public ErrorResponse(String message) {
this.message = message;
this.timestamp = System.currentTimeMillis();
}
public ErrorResponse(String message, Map<String, String> errors) {
this.message = message;
this.errors = errors;
this.timestamp = System.currentTimeMillis();
}
// getter 和 setter
}
RESTful API
1. RESTful 风格设计
@RestController
@RequestMapping("/api/v1/users")
public class UserRestController {
@Autowired
private UserService userService;
// GET /api/v1/users - 获取所有用户
@GetMapping
public ResponseEntity<PageResult<User>> getUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {
PageResult<User> result = userService.findUsers(page, size, keyword);
return ResponseEntity.ok(result);
}
// GET /api/v1/users/{id} - 获取单个用户
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
// POST /api/v1/users - 创建用户
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
// PUT /api/v1/users/{id} - 更新用户
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
User user = userService.updateUser(id, request);
return ResponseEntity.ok(user);
}
// DELETE /api/v1/users/{id} - 删除用户
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
// GET /api/v1/users/{id}/posts - 获取用户的所有文章
@GetMapping("/{id}/posts")
public ResponseEntity<List<Post>> getUserPosts(@PathVariable Long id) {
List<Post> posts = postService.findByUserId(id);
return ResponseEntity.ok(posts);
}
}
2. HTTP 状态码使用
@RestController
public class StatusCodeController {
// 200 OK - 成功返回数据
@GetMapping("/success")
public ResponseEntity<String> success() {
return ResponseEntity.ok("操作成功");
}
// 201 Created - 资源创建成功
@PostMapping("/resource")
public ResponseEntity<String> create() {
return ResponseEntity.status(HttpStatus.CREATED).body("资源创建成功");
}
// 204 No Content - 成功但无内容返回
@DeleteMapping("/resource/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
return ResponseEntity.noContent().build();
}
// 400 Bad Request - 请求参数错误
@PostMapping("/validate")
public ResponseEntity<String> validate(@RequestParam String param) {
if (param == null || param.trim().isEmpty()) {
return ResponseEntity.badRequest().body("参数不能为空");
}
return ResponseEntity.ok("验证成功");
}
// 404 Not Found - 资源不存在
@GetMapping("/resource/{id}")
public ResponseEntity<String> getResource(@PathVariable Long id) {
if (id > 1000) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok("资源数据");
}
// 500 Internal Server Error - 服务器内部错误
@GetMapping("/error")
public ResponseEntity<String> error() {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("服务器内部错误");
}
}
最佳实践
1. 控制器设计原则
- 单一职责: 每个控制器只处理一个业务领域
- 薄控制器: 控制器只负责请求处理,业务逻辑交给服务层
- 统一返回: 使用统一的响应格式
- 参数验证: 在控制器层进行参数验证
2. RESTful API 设计规范
// 好的做法
@RestController
@RequestMapping("/api/v1/users")
public class UserRestController {
@GetMapping // GET /api/v1/users
@PostMapping // POST /api/v1/users
@GetMapping("/{id}") // GET /api/v1/users/123
@PutMapping("/{id}") // PUT /api/v1/users/123
@DeleteMapping("/{id}") // DELETE /api/v1/users/123
}
// 不好的做法
@RestController
public class BadController {
@GetMapping("/getUser") // 不符合 RESTful 风格
@PostMapping("/createUser") // URL 中包含动词
@GetMapping("/deleteUser") // 使用 GET 删除数据
}
3. 统一响应格式
// 统一响应类
public class ApiResponse<T> {
private int code;
private String message;
private T data;
private long timestamp;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "成功", data);
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}
@RestController
public class ApiController {
@GetMapping("/api/users")
public ApiResponse<List<User>> getUsers() {
List<User> users = userService.findAll();
return ApiResponse.success(users);
}
@PostMapping("/api/users")
public ApiResponse<User> createUser(@RequestBody User user) {
User createdUser = userService.create(user);
return ApiResponse.success(createdUser);
}
}
拦截器机制
1. 拦截器概述
Spring MVC 拦截器(Interceptor)是对 Controller 请求进行拦截,在请求处理前后进行特定操作的组件。类似于 Servlet 的 Filter,但更加灵活和强大。
拦截器与过滤器的区别
| 特性 | 拦截器(Interceptor) | 过滤器(Filter) |
|---|---|---|
| 依赖 | Spring MVC 框架 | Servlet 规范 |
| 拦截范围 | 仅拦截 Controller 请求 | 拦截所有请求 |
| 配置方式 | Spring 配置 | web.xml 配置 |
| 执行时机 | DispatcherServlet 内部 | Servlet 容器层 |
| 访问对象 | 可以访问 Spring 容器 | 不能访问 Spring 容器 |
2. 自定义拦截器
实现 HandlerInterceptor 接口
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class LoginInterceptor implements HandlerInterceptor {
/**
* 预处理方法,在 Controller 方法执行之前调用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("LoginInterceptor: preHandle 执行");
// 获取请求路径
String requestURI = request.getRequestURI();
System.out.println("请求路径: " + requestURI);
// 白名单:登录、注册等页面不需要拦截
String[] allowUrls = {"/login", "/register", "/static/", "/error"};
for (String url : allowUrls) {
if (requestURI.contains(url)) {
return true;
}
}
// 检查用户是否已登录
Object user = request.getSession().getAttribute("user");
if (user != null) {
System.out.println("用户已登录: " + user);
return true; // 继续执行
} else {
System.out.println("用户未登录,重定向到登录页面");
response.sendRedirect(request.getContextPath() + "/login");
return false; // 中断执行
}
}
/**
* 后处理方法,在 Controller 方法执行之后,视图渲染之前调用
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("LoginInterceptor: postHandle 执行");
if (modelAndView != null) {
// 可以修改模型数据或视图名称
modelAndView.addObject("currentTime", new java.util.Date());
System.out.println("视图名称: " + modelAndView.getViewName());
}
}
/**
* 完成处理方法,在视图渲染完成后调用
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("LoginInterceptor: afterCompletion 执行");
if (ex != null) {
System.err.println("请求处理过程中发生异常: " + ex.getMessage());
}
// 清理资源,记录日志等
long startTime = System.currentTimeMillis();
Object startTimeAttr = request.getAttribute("startTime");
if (startTimeAttr != null) {
long endTime = System.currentTimeMillis();
long executeTime = endTime - (Long) startTimeAttr;
System.out.println("请求执行时间: " + executeTime + "ms");
}
}
}
3. 拦截器配置
XML 配置方式
<!-- springmvc-config.xml -->
<mvc:interceptors>
<!-- 全局拦截器 -->
<bean class="com.example.interceptor.LoggingInterceptor"/>
<!-- 指定路径拦截器 -->
<mvc:interceptor>
<mvc:mapping path="/admin/**"/>
<mvc:exclude-mapping path="/admin/login"/>
<bean class="com.example.interceptor.AdminInterceptor"/>
</mvc:interceptor>
<!-- 登录拦截器 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/login"/>
<mvc:exclude-mapping path="/register"/>
<mvc:exclude-mapping path="/static/**"/>
<bean class="com.example.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
Java 配置方式
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加登录拦截器
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/register", "/static/**", "/error");
// 添加管理员拦截器
registry.addInterceptor(new AdminInterceptor())
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/login");
// 添加日志拦截器
registry.addInterceptor(new LoggingInterceptor())
.addPathPatterns("/**");
}
}
4. 高级拦截器应用
权限控制拦截器
@Component
public class AuthorizationInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 检查是否是控制器方法
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 检查方法或类上是否有权限注解
RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
if (requireRole == null) {
requireRole = handlerMethod.getBeanType().getAnnotation(RequireRole.class);
}
if (requireRole == null) {
return true; // 没有权限要求,直接通过
}
// 获取当前用户
Object userObj = request.getSession().getAttribute("user");
if (userObj == null) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "用户未登录");
return false;
}
User user = (User) userObj;
String[] requiredRoles = requireRole.value();
// 检查用户权限
boolean hasPermission = userService.hasAnyRole(user.getId(), requiredRoles);
if (!hasPermission) {
response.sendError(HttpStatus.FORBIDDEN.value(), "权限不足");
return false;
}
return true;
}
}
// 权限注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value();
}
// 使用示例
@Controller
@RequestMapping("/admin")
@RequireRole({"ADMIN"})
public class AdminController {
@GetMapping("/users")
@RequireRole({"ADMIN", "USER_MANAGER"})
public String userList() {
return "admin/users";
}
}
高级异常处理
1. 全局异常处理器
@ControllerAdvice 注解
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorResponse handleValidationException(MethodArgumentNotValidException ex) {
logger.warn("参数验证失败", ex);
BindingResult result = ex.getBindingResult();
Map<String, String> errors = new HashMap<>();
result.getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return new ErrorResponse("参数验证失败", errors);
}
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorResponse handleBusinessException(BusinessException ex) {
logger.warn("业务异常: {}", ex.getMessage());
return new ErrorResponse(ex.getMessage());
}
/**
* 处理系统异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponse handleGenericException(Exception ex) {
logger.error("系统异常", ex);
return new ErrorResponse("系统内部错误,请稍后重试");
}
/**
* 处理 404 错误
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
@ResponseBody
public ErrorResponse handleNotFoundException(NoHandlerFoundException ex) {
logger.warn("请求路径不存在: {}", ex.getRequestURL());
return new ErrorResponse("请求的资源不存在");
}
}
// 错误响应类
public class ErrorResponse {
private String message;
private Map<String, String> errors;
private long timestamp;
public ErrorResponse(String message) {
this.message = message;
this.timestamp = System.currentTimeMillis();
}
public ErrorResponse(String message, Map<String, String> errors) {
this.message = message;
this.errors = errors;
this.timestamp = System.currentTimeMillis();
}
// getters and setters
}
// 自定义业务异常
public class BusinessException extends RuntimeException {
private int errorCode;
public BusinessException(String message) {
super(message);
}
public BusinessException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
}
2. 统一响应格式
// 统一响应类
public class ApiResponse<T> {
private int code;
private String message;
private T data;
private long timestamp;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage("成功");
response.setData(data);
response.setTimestamp(System.currentTimeMillis());
return response;
}
public static <T> ApiResponse<T> error(int code, String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(code);
response.setMessage(message);
response.setTimestamp(System.currentTimeMillis());
return response;
}
// getters and setters
}
@RestController
public class ApiController {
@GetMapping("/api/users")
public ApiResponse<List<User>> getUsers() {
List<User> users = userService.findAll();
return ApiResponse.success(users);
}
@PostMapping("/api/users")
public ApiResponse<User> createUser(@Valid @RequestBody User user) {
User createdUser = userService.create(user);
return ApiResponse.success(createdUser);
}
}
数据验证与转换
1. 数据验证
使用 Bean Validation
// 用户实体类
public class User {
@NotNull(message = "用户ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@Email(message = "邮箱格式不正确")
@NotBlank(message = "邮箱不能为空")
private String email;
@Range(min = 18, max = 100, message = "年龄必须在18-100之间")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Past(message = "生日必须是过去的日期")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
// constructors, getters and setters
}
// 控制器中使用验证
@Controller
public class UserController {
@PostMapping("/users")
public String createUser(@Valid @ModelAttribute User user, BindingResult result, Model model) {
if (result.hasErrors()) {
// 处理验证错误
model.addAttribute("user", user);
return "user/form";
}
// 保存用户
userService.save(user);
return "redirect:/users";
}
@PostMapping("/api/users")
@ResponseBody
public ApiResponse<User> createUserApi(@Valid @RequestBody User user) {
User savedUser = userService.save(user);
return ApiResponse.success(savedUser);
}
}
2. 自定义验证器
// 自定义验证注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdCardValidator.class)
public @interface IdCard {
String message() default "身份证号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 验证器实现
public class IdCardValidator implements ConstraintValidator<IdCard, String> {
@Override
public void initialize(IdCard constraintAnnotation) {
// 初始化
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.trim().isEmpty()) {
return true; // 由 @NotBlank 等注解处理空值
}
// 身份证号验证逻辑
return isValidIdCard(value);
}
private boolean isValidIdCard(String idCard) {
// 简化的身份证验证逻辑
return idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");
}
}
// 使用自定义验证
public class Person {
@IdCard
private String idCard;
// getters and setters
}
3. 数据转换
自定义类型转换器
// 字符串转日期转换器
@Component
public class StringToDateConverter implements Converter<String, Date> {
private static final String[] DATE_PATTERNS = {
"yyyy-MM-dd",
"yyyy/MM/dd",
"yyyy-MM-dd HH:mm:ss"
};
@Override
public Date convert(String source) {
if (source == null || source.trim().isEmpty()) {
return null;
}
for (String pattern : DATE_PATTERNS) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
return sdf.parse(source);
} catch (ParseException e) {
// 继续尝试下一个格式
}
}
throw new IllegalArgumentException("无法解析日期格式: " + source);
}
}
// 配置转换器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToDateConverter());
}
}
文件上传下载
1. 文件上传
配置文件上传
<!-- 配置文件上传解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设置上传文件总大小上限,单位字节 -->
<property name="maxUploadSize" value="10485760"/> <!-- 10MB -->
<!-- 设置单个文件大小上限 -->
<property name="maxUploadSizePerFile" value="5242880"/> <!-- 5MB -->
<!-- 设置内存临界值 -->
<property name="maxInMemorySize" value="4096"/>
<!-- 设置默认编码 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
文件上传控制器
@Controller
@RequestMapping("/file")
public class FileController {
private static final String UPLOAD_DIR = "/uploads/";
@GetMapping("/upload")
public String uploadForm() {
return "file/upload";
}
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file, Model model) {
if (file.isEmpty()) {
model.addAttribute("error", "请选择要上传的文件");
return "file/upload";
}
try {
// 检查文件类型
String contentType = file.getContentType();
if (!isAllowedFileType(contentType)) {
model.addAttribute("error", "不支持的文件类型");
return "file/upload";
}
// 生成文件名
String originalFilename = file.getOriginalFilename();
String fileName = generateFileName(originalFilename);
// 保存文件
File uploadDir = new File(UPLOAD_DIR);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
File dest = new File(uploadDir, fileName);
file.transferTo(dest);
model.addAttribute("message", "文件上传成功");
model.addAttribute("fileName", fileName);
} catch (IOException e) {
logger.error("文件上传失败", e);
model.addAttribute("error", "文件上传失败");
}
return "file/upload";
}
@PostMapping("/upload/multiple")
public String uploadMultipleFiles(@RequestParam("files") MultipartFile[] files, Model model) {
List<String> uploadedFiles = new ArrayList<>();
List<String> errors = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
try {
String fileName = generateFileName(file.getOriginalFilename());
File dest = new File(UPLOAD_DIR, fileName);
file.transferTo(dest);
uploadedFiles.add(fileName);
} catch (IOException e) {
errors.add("文件 " + file.getOriginalFilename() + " 上传失败");
}
}
}
model.addAttribute("uploadedFiles", uploadedFiles);
model.addAttribute("errors", errors);
return "file/upload";
}
private boolean isAllowedFileType(String contentType) {
String[] allowedTypes = {"image/jpeg", "image/png", "image/gif", "application/pdf", "text/plain"};
return Arrays.asList(allowedTypes).contains(contentType);
}
private String generateFileName(String originalFilename) {
String extension = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex > 0) {
extension = originalFilename.substring(dotIndex);
}
return System.currentTimeMillis() + "_" + UUID.randomUUID().toString() + extension;
}
}
2. 文件下载
@Controller
@RequestMapping("/file")
public class FileDownloadController {
private static final String UPLOAD_DIR = "/uploads/";
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
try {
File file = new File(UPLOAD_DIR, fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.header(HttpHeaders.CONTENT_TYPE, "application/octet-stream")
.body(resource);
} catch (Exception e) {
logger.error("文件下载失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/view/{fileName}")
public ResponseEntity<Resource> viewFile(@PathVariable String fileName) {
try {
File file = new File(UPLOAD_DIR, fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
String contentType = Files.probeContentType(file.toPath());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, contentType)
.body(resource);
} catch (Exception e) {
logger.error("文件查看失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
常见问题
问题1:404 错误
现象: 访问控制器时出现 404 错误 原因:
- URL 映射配置错误
- 控制器没有被扫描到
- DispatcherServlet 配置错误
解决方案:
<!-- 检查 web.xml 配置 -->
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern> <!-- 正确配置 -->
</servlet-mapping>
<!-- 检查组件扫描 -->
<context:component-scan base-package="com.example.controller"/>
问题2:中文乱码
现象: 返回的中文内容出现乱码 解决方案:
<!-- 字符编码过滤器 -->
<filter>
<filter-name>characterEncodingFilter</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>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
问题3:静态资源无法访问
现象: CSS、JS、图片等静态资源 404 解决方案:
<!-- 配置静态资源处理 -->
<mvc:resources mapping="/static/**" location="/static/"/>
<mvc:default-servlet-handler/>
问题4:@RequestBody 不生效
现象: 无法接收 JSON 数据 解决方案:
<!-- 开启 MVC 注解支持 -->
<mvc:annotation-driven/>
<!-- 或者手动配置 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
</list>
</property>
</bean>
问题5:跨域问题
现象: 前端调用 API 时出现跨域错误 解决方案:
// 全局跨域配置
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
// 或者使用注解
@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class ApiController {
// ...
}
⚠️ 注意: 在生产环境中,请使用 HTTPS,并配置合适的安全策略。
相关文章
前后章节导航
- 上一章:Web程序设计笔记14——第十章:Spring和MyBatis的整合
- 下一章:Web程序设计笔记16——第十二章:Spring MVC的核心类和注解
- 相关指南:Spring MVC 拦截器完整指南
Spring 基础系列
- Spring 基本应用
- Spring AOP 面向切面编程
- Spring 事务管理
SpringBoot 现代化开发
数据库技术
中间件技术
运维部署
开发工具
- IDEA 使用技巧
- Maven 项目管理
总结
Spring MVC 是 Java Web 开发的经典框架,本指南涵盖了:
🎯 核心内容
- 环境搭建: Maven + Spring MVC + Tomcat 开发环境
- 核心组件: DispatcherServlet、HandlerMapping、Controller、ViewResolver
- 配置方式: XML 配置和注解配置
- 请求处理: 参数绑定、数据验证、异常处理
🛠️ 实现方式
- 传统方式: 实现 Controller 接口,XML 配置
- 注解方式: @Controller、@RequestMapping 等注解
- RESTful API: @RestController、HTTP 方法映射
📚 实践应用
- Web 应用开发: 传统的 MVC 架构 Web 应用
- API 服务: RESTful 风格的 API 接口开发
- 前后端分离: 作为后端 API 服务提供者