单元1 · Spring MVC 概述与 HelloWorld
MVC 思想、DispatcherServlet 流程、依赖配置、第一个控制器
- MVC:Model 模型 + View 视图 + Controller 控制器,分离关注点。
- DispatcherServlet:前端控制器,统一接收请求并分发。
- 请求流程:DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → ViewResolver → View。
- 配置方式:XML(springmvc.xml)或 Java 配置(@EnableWebMvc + WebMvcConfigurer)。
实训1.1 Spring MVC 请求处理流程
说明 Spring MVC 的核心组件与一次请求的完整处理流程。
DispatcherServlet 前端控制器分发请求:HandlerMapping 找处理器 → HandlerAdapter 调用 Controller → 返回 ModelAndView → ViewResolver 解析视图 → 渲染响应。
Spring MVC 一次请求的处理流程:
1. 浏览器发起 HTTP 请求
2. DispatcherServlet(前端控制器)接收请求
3. HandlerMapping 根据 URL 找到对应的 Controller 方法
4. HandlerAdapter 调用 Controller 方法执行业务
5. Controller 返回 ModelAndView(模型数据 + 视图名)
6. ViewResolver 解析视图名 → 定位 JSP/Thymeleaf 模板
7. View 渲染模型数据生成 HTML
8. 响应返回浏览器
核心组件:
- DispatcherServlet:总控分发
- HandlerMapping:URL → 处理器映射
- HandlerAdapter:处理器适配执行
- ViewResolver:视图解析器
- View:视图渲染
实训1.2 基于 XML 配置第一个 Spring MVC 应用
给出 Maven 依赖与 springmvc.xml 配置,编写第一个 @Controller 返回页面。
引入 spring-webmvc 依赖;web.xml 配置 DispatcherServlet;springmvc.xml 开启注解驱动、组件扫描与视图解析器。
<!-- pom.xml -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.39</version>
</dependency>
<!-- springmvc.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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"/>
<mvc:annotation-driven/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
<!-- HelloController -->
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello(Model model) {
model.addAttribute("msg", "Hello Spring MVC");
return "hello";
}
}
实训1.3 基于注解配置代替 XML
使用 @Configuration + WebMvcConfigurer 代替 XML,实现 Java 配置版 Spring MVC。
@EnableWebMvc 开启 MVC 配置;实现 WebMvcConfigurer 自定义视图解析器与拦截器;避免 XML,类型安全。
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example.controller")
public class WebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
单元2 · 请求映射与参数绑定
@RequestMapping 族、@RequestParam、@PathVariable、@RequestBody、参数校验
- 映射注解:@RequestMapping 及 @GetMapping/@PostMapping/@PutMapping/@DeleteMapping 窄化。
- @RequestParam:绑定查询参数,支持 required/defaultValue。
- @PathVariable:绑定 URL 路径占位符 {id}。
- 参数校验:@Valid + @NotBlank/@Min/@Email 等校验注解。
实训2.1 请求映射注解详解
演示 @GetMapping/@PostMapping/@PutMapping/@DeleteMapping 与 @RequestMapping 的组合使用。
窄化注解等价于 @RequestMapping(method=...),同时支持 params/headers 条件映射;路径可用 {id} 占位符。
package com.example.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/books")
public class BookController {
@GetMapping
public String list() {
return "查询全部图书";
}
@GetMapping("/{id}")
public String detail(@PathVariable Long id) {
return "查询图书:" + id;
}
@PostMapping
public String create() {
return "新增图书";
}
@PutMapping("/{id}")
public String update(@PathVariable Long id) {
return "修改图书:" + id;
}
@DeleteMapping("/{id}")
public String delete(@PathVariable Long id) {
return "删除图书:" + id;
}
}
实训2.2 @RequestParam 参数绑定
使用 @RequestParam 绑定查询参数、设置必填与默认值,处理数组参数。
@RequestParam(value, required, defaultValue) 绑定 ?key=value;多个同名参数可用数组或 List 接收。
package com.example.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
public class ParamController {
@GetMapping("/search")
public String search(
@RequestParam(value = "keyword", required = false,
defaultValue = "spring") String keyword,
@RequestParam(value = "tags", required = false)
List<String> tags) {
return "关键词:" + keyword + ",标签:" + tags;
}
}
// 请求示例
// GET /search?keyword=mvc&tags=a&tags=b
// 返回:关键词:mvc,标签:[a, b]
实训2.3 @RequestBody JSON 与参数校验
接收 JSON 请求体并配合 @Valid 校验字段,校验失败返回统一错误信息。
@RequestBody 反序列化 JSON;实体字段加 @NotBlank/@NotNull 等校验注解,方法参数加 @Valid 触发校验。
package com.example.entity;
import jakarta.validation.constraints.*;
public class User {
@NotBlank(message = "姓名不能为空")
private String name;
@Min(value = 1, message = "年龄最小为 1")
@Max(value = 150, message = "年龄最大为 150")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
package com.example.controller;
import com.example.entity.User;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@RestController
public class UserController {
@PostMapping("/users")
public User create(@Valid @RequestBody User user) {
return user;
}
}
单元3 · 控制器返回值与数据传递
String 视图名、Model/ModelAndView、@ResponseBody、REST 风格、@RestController
- 返回值:String 视图名 / ModelAndView / @ResponseBody 对象。
- Model:视图数据容器,addAttribute 添加属性。
- @RestController:@Controller + @ResponseBody 组合,返回 JSON。
- 转发与重定向:forward 内部转发地址不变;redirect 浏览器跳转地址变化。
实训3.1 Model 与 ModelAndView 数据传递
控制器向视图传递数据:分别演示 Model 与 ModelAndView 两种方式。
Model 作为方法参数添加属性;ModelAndView 同时指定视图名与模型;JSP 中通过 ${msg} 表达式取值。
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class DataController {
// 方式一:Model 参数
@GetMapping("/data/model")
public String withModel(Model model) {
model.addAttribute("msg", "通过 Model 传递");
return "show";
}
// 方式二:ModelAndView
@GetMapping("/data/mav")
public ModelAndView withModelAndView() {
ModelAndView mv = new ModelAndView("show");
mv.addObject("msg", "通过 ModelAndView 传递");
return mv;
}
}
<%-- show.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<body><h1>${msg}</h1></body>
</html>
实训3.2 @ResponseBody 返回 JSON
使用 @ResponseBody 返回对象自动序列化为 JSON,说明底层 Jackson 配置。
@ResponseBody 让返回值直接写入响应体;引入 jackson-databind 后对象自动序列化;@RestController = @Controller + @ResponseBody。
package com.example.entity;
public class Result {
private int code;
private String message;
private Object data;
public Result(int code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
public static Result ok(Object data) {
return new Result(200, "success", data);
}
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Object getData() { return data; }
public void setData(Object data) { this.data = data; }
}
package com.example.controller;
import com.example.entity.Result;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
public class JsonController {
@GetMapping("/api/list")
public Result list() {
List<String> data = Arrays.asList("Java", "Spring", "MVC");
return Result.ok(data);
}
}
实训3.3 重定向与转发
演示控制器的转发(forward)与重定向(redirect)区别与用法。
return "forward:/path" 服务器内部转发,地址栏不变;return "redirect:/path" 浏览器 302 跳转,地址栏变化。
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ForwardRedirectController {
// 服务器内部转发:地址栏不变,一次请求
@GetMapping("/go/forward")
public String forward() {
return "forward:/hello";
}
// 浏览器重定向:地址栏变化,两次请求
@GetMapping("/go/redirect")
public String redirect() {
return "redirect:/hello";
}
// 带参数重定向(Flash 属性)
@GetMapping("/go/redirect-attr")
public String redirectWithAttr(
RedirectAttributes attributes) {
attributes.addFlashAttribute("tip", "操作成功");
return "redirect:/hello";
}
}
单元4 · 文件上传与下载
MultipartFile、上传配置、文件保存、文件下载、异常处理
- MultipartFile:接收 multipart/form-data 上传文件。
- 大小限制:spring.servlet.multipart.max-file-size 配置。
- 下载:Content-Disposition: attachment 触发浏览器下载。
- 防覆盖:UUID 或时间戳重命名上传文件。
实训4.1 单文件上传
使用 MultipartFile 接收前端上传的文件,保存到本地目录并返回文件名。
表单 enctype=multipart/form-data;方法参数 MultipartFile file 接收;transferTo 保存文件;配置 multipart 大小限制。
package com.example.controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.UUID;
@RestController
public class UploadController {
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file)
throws Exception {
// 原始文件名
String original = file.getOriginalFilename();
// 生成唯一文件名防覆盖
String ext = original.substring(original.lastIndexOf('.'));
String filename = UUID.randomUUID().toString() + ext;
File dir = new File("/tmp/upload");
if (!dir.exists()) dir.mkdirs();
file.transferTo(new File(dir, filename));
return "上传成功:" + filename;
}
}
# application.properties
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB
实训4.2 多文件上传
一次接收多个文件并循环保存,返回全部保存的文件名。
方法参数用 MultipartFile[] files 或 List
package com.example.controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@RestController
public class UploadController {
@PostMapping("/upload/many")
public List<String> uploadMany(
@RequestParam("files") MultipartFile[] files)
throws Exception {
List<String> names = new ArrayList<>();
for (MultipartFile file : files) {
if (file.isEmpty()) continue;
String name = System.currentTimeMillis()
+ "_" + file.getOriginalFilename();
file.transferTo(new File("/tmp/upload", name));
names.add(name);
}
return names;
}
}
<!-- 前端表单 -->
<form method="post" action="/upload/many"
enctype="multipart/form-data">
<input type="file" name="files" multiple/>
<button>提交</button>
</form>
实训4.3 文件下载
实现文件下载接口:设置 Content-Disposition 响应头,输出文件流。
用 ResponseEntity
package com.example.controller;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.File;
@RestController
public class DownloadController {
@GetMapping("/download/{name}")
public ResponseEntity<FileSystemResource> download(
@PathVariable String name) {
File file = new File("/tmp/upload/" + name);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + name);
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new FileSystemResource(file));
}
}
单元5 · 拦截器与过滤器
HandlerInterceptor、preHandle/postHandle/afterCompletion、注册配置、登录拦截实战
- HandlerInterceptor:preHandle/postHandle/afterCompletion 三阶段。
- 注册:WebMvcConfigurer.addInterceptors + 路径匹配。
- 放行:excludePathPatterns 配置登录页与静态资源白名单。
- Filter vs Interceptor:Filter 更早更通用;Interceptor 可访问 MVC 上下文。
实训5.1 拦截器三方法执行时机
编写拦截器打印 preHandle/postHandle/afterCompletion 的执行顺序。
preHandle 在处理器执行前调用(返回 false 终止);postHandle 在处理器后、视图渲染前;afterCompletion 在请求完成后(无论异常与否)。
package com.example.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("[1] preHandle 请求前");
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("[2] postHandle 视图渲染前");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
System.out.println("[3] afterCompletion 请求完成");
}
}
实训5.2 登录拦截器实战
实现登录状态校验拦截器,未登录跳转登录页,并注册放行白名单。
preHandle 中检查 session 是否存在登录用户;通过 WebMvcConfigurer.addInterceptors 注册,excludePathPatterns 放行登录页与静态资源。
package com.example.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");
if (user == null) {
response.sendRedirect("/login");
return false;
}
return true;
}
}
package com.example.config;
import com.example.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/login",
"/register",
"/css/**",
"/js/**",
"/images/**");
}
}
实训5.3 拦截器与过滤器对比
对比 Filter 与 HandlerInterceptor 的差异,并说明各自适用场景。
Filter 属于 Servlet 规范、在 DispatcherServlet 之前执行,适合编码/跨域;Interceptor 属于 Spring MVC、可访问 Handler 与 ModelAndView,适合业务级校验。
Filter 与 HandlerInterceptor 对比:
| 维度 | Filter | HandlerInterceptor |
|------|--------|--------------------|
| 规范 | Servlet 规范 | Spring MVC 框架 |
| 执行时机 | DispatcherServlet 之前 | 处理器执行前后 |
| 访问能力 | 只能操作 request/response | 可访问 Handler、ModelAndView |
| 依赖容器 | 无需 Spring 容器 | 依赖 Spring 容器 |
| 典型场景 | 编码、跨域、压缩 | 登录校验、权限、日志 |
执行链顺序:
Request → Filter → DispatcherServlet → Interceptor.preHandle
→ Controller → Interceptor.postHandle → 视图渲染
→ Interceptor.afterCompletion → Response
单元6 · RESTful API 设计
REST 原则、资源命名、状态码语义、REST 接口实践、异常统一处理
- REST:资源名词 + HTTP 方法表达 CRUD。
- 状态码:200/201/204/400/404/500 语义化。
- @RestControllerAdvice:全局异常处理统一返回结构。
- 版本管理:路径版本 /v1/ 或请求头 X-API-Version。
实训6.1 RESTful 接口设计规范
以用户资源为例,设计符合 REST 风格的增删改查接口并说明状态码语义。
资源用名词复数(/users),HTTP 方法表达动作:GET 查询、POST 新增、PUT 更新、DELETE 删除;状态码 200/201/204/404 语义明确。
RESTful 用户资源接口设计:
| 方法 | 路径 | 语义 | 成功状态码 |
|------|------|------|-----------|
| GET | /users | 查询用户列表 | 200 OK |
| GET | /users/{id} | 查询单个用户 | 200 OK |
| POST | /users | 新增用户 | 201 Created |
| PUT | /users/{id} | 全量更新 | 200 OK |
| PATCH| /users/{id} | 部分更新 | 200 OK |
| DELETE| /users/{id} | 删除用户 | 204 No Content |
设计要点:
1. 资源命名用名词复数,不用动词
2. 通过 HTTP 方法表达 CRUD
3. 使用嵌套表达关系:/users/{id}/orders
4. 分页:/users?page=1&size=10
5. 过滤:/users?status=active
实训6.2 统一异常处理 @RestControllerAdvice
使用 @RestControllerAdvice 全局处理业务异常与系统异常,返回统一 JSON 结构。
@ExceptionHandler 定义各异常处理逻辑;@ControllerAdvice 拦截所有控制器异常;自定义 BusinessException 携带错误码。
package com.example.common;
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() { return code; }
}
package com.example.common;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleBusiness(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleException(Exception e) {
return Result.error(500, "系统异常:" + e.getMessage());
}
}
实训6.3 REST 接口版本管理
演示通过 URL 路径与请求头两种方式进行接口版本控制。
路径版本 /api/v1/users 直观简单;Header 版本 X-API-Version 更规范;v1/v2 并存保证兼容升级。
package com.example.controller;
import org.springframework.web.bind.annotation.*;
// 方式一:路径版本(简单直观)
@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {
@GetMapping("/{id}")
public String getUserV1(@PathVariable Long id) {
return "v1 用户:" + id;
}
}
// 方式二:请求头版本
@RestController
@RequestMapping("/api/users")
public class UserV2Controller {
@GetMapping(value = "/{id}", headers = "X-API-Version=2")
public String getUserV2(@PathVariable Long id) {
return "v2 用户:" + id;
}
}
// 调用示例:
// GET /api/v1/users/1 → v1
// GET /api/users/1 + X-API-Version: 2 → v2
单元7 · 表单处理与数据绑定
表单提交、对象绑定、日期格式、JSON 数据绑定、@ModelAttribute
- 对象绑定:表单字段与对象属性同名自动绑定。
- @ModelAttribute:将表单对象放入 Model 供视图使用。
- @InitBinder:注册属性编辑器,统一预处理参数。
- 日期格式:@DateTimeFormat(pattern="yyyy-MM-dd")。
实训7.1 表单对象绑定
接收表单提交的用户数据,自动绑定到 User 对象并回显。
表单字段名与对象属性名一致时自动绑定;@ModelAttribute 将对象放入 Model;日期字段用 @DateTimeFormat 指定格式。
package com.example.entity;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
public class User {
private String name;
private Integer age;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Date getBirthday() { return birthday; }
public void setBirthday(Date birthday) { this.birthday = birthday; }
}
package com.example.controller;
import com.example.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@Controller
public class FormController {
@GetMapping("/form")
public String form() {
return "form";
}
@PostMapping("/form/submit")
public String submit(@ModelAttribute User user, Model model) {
model.addAttribute("user", user);
return "result";
}
}
<!-- form.jsp -->
<form method="post" action="/form/submit">
姓名:<input name="name"/><br/>
年龄:<input name="age"/><br/>
生日:<input name="birthday" placeholder="2024-01-01"/><br/>
<button>提交</button>
</form>
实训7.2 @InitBinder 自定义绑定
使用 @InitBinder 注册自定义编辑器(如 HTML 转义、日期格式全局处理)。
@InitBinder 标注方法对当前控制器请求生效;WebDataBinder 注册 CustomDateEditor 等属性编辑器。
package com.example.controller;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import java.beans.PropertyEditorSupport;
@Controller
public class BaseController {
@InitBinder
public void initBinder(WebDataBinder binder) {
// 去除字符串前后空格
binder.registerCustomEditor(String.class,
new StringTrimmerEditor(true));
// 自定义敏感词过滤编辑器
binder.registerCustomEditor(String.class, "comment",
new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
setValue(text == null ? "" :
text.replace("敏感词", "***"));
}
});
}
}
实训7.3 JSON 数据绑定与视图
接收 JSON 数组请求体并绑定到对象集合,返回 JSON 视图。
@RequestBody List
package com.example.controller;
import com.example.entity.User;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
// User 增加序列化格式
class UserVo {
public String name;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
public Date createTime;
}
@RestController
public class JsonBindController {
// 接收 JSON 数组
@PostMapping("/api/batch")
public int batch(@RequestBody List<User> users) {
return users.size();
}
// 返回 JSON 日期格式化
@GetMapping("/api/now")
public UserVo now() {
UserVo vo = new UserVo();
vo.name = "demo";
vo.createTime = new Date();
return vo;
}
}
// POST /api/batch
// [{"name":"a","age":20},{"name":"b","age":21}] → 2
单元8 · 视图技术 Thymeleaf
Thymeleaf 集成、th:text/th:each、条件与 URL 表达式、布局
- Thymeleaf:服务端模板引擎,html 中 th:* 属性渲染。
- 表达式:${...} 取值、@{...} URL、#{...} 国际化。
- 循环条件:th:each 遍历、th:if/th:unless 条件。
- 布局:th:fragment 定义片段,th:insert/th:replace 复用。
实训8.1 Thymeleaf 集成与基础语法
在 Spring Boot + Spring MVC 中集成 Thymeleaf,编写第一个模板页面。
引入 spring-boot-starter-thymeleaf;模板放 resources/templates/;th:text 输出文本,${...} 访问模型属性。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- src/main/resources/templates/hello.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Hello</title></head>
<body>
<h1 th:text="${msg}">默认文本</h1>
<p>当前时间:<span th:text="${#dates.format(now, 'yyyy-MM-dd')}"></span></p>
</body>
</html>
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Date;
@Controller
public class PageController {
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("msg", "Hello Thymeleaf");
model.addAttribute("now", new Date());
return "hello";
}
}
实训8.2 th:each 循环与条件渲染
使用 th:each 遍历用户列表,用 th:if/th:unless 条件渲染。
th:each="u, st : ${users}" 遍历并携带状态对象;th:if 为真渲染,th:unless 取反;th:text 输出转义文本。
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Arrays;
import java.util.List;
@Controller
public class UserPageController {
@GetMapping("/users/page")
public String page(Model model) {
List<String> users = Arrays.asList("张三", "李四", "王五");
model.addAttribute("users", users);
return "user-list";
}
}
<!-- templates/user-list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h1>用户列表</h1>
<ul>
<li th:each="u, st : ${users}"
th:class="${st.odd} ? 'odd' : 'even'"
th:text="|${st.count}. ${u}|"></li>
</ul>
<p th:if="${#lists.isEmpty(users)}">暂无用户</p>
<p th:unless="${#lists.isEmpty(users)}">共 3 位用户</p>
</body>
</html>
实训8.3 Thymeleaf 布局与 URL 表达式
使用 th:fragment/th:insert 实现公共布局,用 @{...} 生成 URL。
公共片段用 th:fragment 定义,th:insert/th:replace 复用;@{/path} 自动加上下文路径;@{/user/{id}(id=${u.id})} 带参数。
<!-- templates/fragments/header.html -->
<header th:fragment="nav(title)">
<nav>
<span th:text="${title}">默认标题</span>
<a th:href="@{/}">首页</a>
<a th:href="@{/users/page}">用户</a>
<a th:href="@{/about}">关于</a>
</nav>
</header>
<!-- templates/user-list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户列表</title>
</head>
<body>
<div th:insert="~{fragments/header :: nav('用户管理')}"></div>
<h1>用户列表</h1>
<ul>
<li th:each="u : ${users}">
<a th:href="@{/users/{id}(id=${u.id})}"
th:text="${u.name}">姓名</a>
</li>
</ul>
</body>
</html>
单元9 · 异步处理与跨域
@Async、Callable/DeferredResult、SSE 推送、@CrossOrigin、CORS 配置
- @Async:异步执行耗时任务,需 @EnableAsync 开启。
- @CrossOrigin:控制器级跨域配置,支持 origins/methods。
- SseEmitter:服务端推送,text/event-stream 长连接。
- 全局 CORS:WebMvcConfigurer.addCorsMappings 统一配置。
实训9.1 @Async 异步任务
开启 @EnableAsync,使用 @Async 异步执行耗时任务并立即返回响应。
@EnableAsync 开启异步;@Async 标注方法在独立线程池执行;调用方立即返回;注意自调用不生效。
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
}
package com.example.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class MailService {
@Async
public void sendMail(String to, String content) throws InterruptedException {
// 模拟耗时 3 秒的邮件发送
Thread.sleep(3000);
System.out.println("已发送邮件给:" + to);
}
}
package com.example.controller;
import com.example.service.MailService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AsyncController {
private final MailService mailService;
public AsyncController(MailService mailService) {
this.mailService = mailService;
}
@GetMapping("/send-mail")
public String send() {
mailService.sendMail("a@b.com", "Hello");
return "邮件已进入发送队列(立即返回)";
}
}
实训9.2 @CrossOrigin 跨域解决方案
在控制器上使用 @CrossOrigin 允许指定来源跨域访问。
@CrossOrigin(origins, methods) 标注类或方法;全局配置用 WebMvcConfigurer.addCorsMappings;allowCredentials 与通配符不可同时使用。
package com.example.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@CrossOrigin(origins = "http://localhost:5173",
maxAge = 3600)
public class CorsController {
@GetMapping("/api/info")
public String info() {
return "跨域数据";
}
// 方法级配置优先级更高
@PostMapping("/api/save")
@CrossOrigin(origins = {"http://localhost:5173",
"https://example.com"},
methods = {RequestMethod.POST})
public String save() {
return "已保存";
}
}
实训9.3 SSE 服务端推送
使用 SseEmitter 实现服务端主动推送消息到浏览器。
SseEmitter 保持长连接逐条推送;text/event-stream 协议;适合实时通知、股票行情等单向推送场景。
package com.example.controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@RestController
public class SseController {
@GetMapping(value = "/stream", produces = "text/event-stream")
public SseEmitter stream() {
SseEmitter emitter = new SseEmitter(30_000L);
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
pool.scheduleAtFixedRate(() -> {
try {
emitter.send(SseEmitter.event()
.name("tick")
.data("当前时间:" + System.currentTimeMillis()));
} catch (IOException e) {
emitter.completeWithError(e);
pool.shutdown();
}
}, 0, 1, TimeUnit.SECONDS);
return emitter;
}
}
单元10 · Spring MVC 与 Spring Boot 整合
自动配置、spring-boot-starter-web、MVC 属性配置、自定义 MVC 配置
- 自动配置:spring-boot-starter-web 自动装配 MVC 组件。
- 自定义增强:实现 WebMvcConfigurer 而非 @EnableWebMvc。
- 静态资源:addResourceHandlers 映射本地目录虚拟路径。
- MessageConverter:内容协商,支持 JSON/XML 多格式。
实训10.1 Spring Boot 中的 MVC 自动配置
说明 spring-boot-starter-web 自动配置了哪些 MVC 组件,以及如何覆盖。
自动配置 DispatcherServlet、Jackson、静态资源映射、错误页等;自定义组件通过实现 WebMvcConfigurer 增强而非替换全部配置。
spring-boot-starter-web 自动配置内容:
- DispatcherServlet:前端控制器
- HandlerMapping / HandlerAdapter
- Jackson JSON 序列化
- 静态资源映射(classpath:/static/)
- 错误页处理(/error)
- 内嵌 Tomcat
- ContentNegotiation(内容协商)
# 常用 MVC 属性
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
spring.mvc.static-path-pattern=/static/**
# 自定义增强:实现 WebMvcConfigurer 而不是
# 使用 @EnableWebMvc(会关闭自动配置)
实训10.2 静态资源映射与自定义配置
配置自定义静态资源目录与虚拟路径映射。
addResourceHandlers 将 /files/** 映射到本地磁盘目录;WebMvcConfigurer 中配置;默认静态目录 classpath:/static/。
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class ResourceConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 本地磁盘图片目录映射
registry.addResourceHandler("/files/**")
.addResourceLocations("file:/data/upload/");
// 自定义 classpath 静态目录
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/assets/");
}
}
// 访问:http://localhost:8080/files/a.jpg
// 实际读取:/data/upload/a.jpg
实训10.3 MessageConverter 内容协商
自定义 HttpMessageConverter 支持 XML 响应,配置内容协商策略。
实现 HttpMessageConverter 或继承 AbstractHttpMessageConverter;configureMessageConverters 注册;支持 Accept: application/xml 协商。
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class ConverterConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(
List<HttpMessageConverter<?>> converters) {
// 添加 Jackson XML 转换器
converters.add(new MappingJackson2XmlHttpMessageConverter());
}
}
// 引入依赖
// <dependency>
// <groupId>com.fasterxml.jackson.dataformat</groupId>
// <artifactId>jackson-dataformat-xml</artifactId>
// </dependency>
// 请求头 Accept: application/xml → 返回 XML
// 请求头 Accept: application/json → 返回 JSON
单元11 · 测试与性能优化
MockMvc 测试、@WebMvcTest、视图解析优化、Gzip 压缩、缓存控制
- MockMvc:模拟 HTTP 请求测试控制器,无需启动服务器。
- @WebMvcTest:只加载 Web 层上下文,测试更轻量。
- Gzip:server.compression.enabled 开启响应压缩。
- Tomcat 调优:threads.max/accept-count 控制吞吐。
实训11.1 MockMvc 控制器测试
使用 MockMvc 测试 GET 与 POST 接口,断言状态码与返回内容。
@WebMvcTest 只加载 Web 层;MockMvc.perform 模拟请求;andExpect 链式断言状态、内容、JSON 路径。
package com.example.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(HelloController.class)
public class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void helloReturnsOk() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, Spring MVC!"));
}
@Test
void listReturnsJson() throws Exception {
mockMvc.perform(get("/api/list"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
}
实训11.2 Gzip 压缩与缓存策略
配置响应 Gzip 压缩与静态资源缓存头,提升页面加载性能。
spring.compression.enabled 开启 Gzip;Cache-Control 头配置静态资源强缓存;动态接口用 no-cache。
# application.properties
# Gzip 压缩
server.compression.enabled=true
server.compression.mime-types=application/json,text/html,text/css,application/javascript
server.compression.min-response-size=1024
# 静态资源缓存
spring.web.resources.cache.cachecontrol.max-age=30d
spring.web.resources.cache.cachecontrol.cache-public=true
# 自定义响应头(拦截器中设置)
package com.example.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class CacheInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
response.setHeader("X-Content-Type-Options", "nosniff");
return true;
}
}
实训11.3 连接池与容器调优
配置内嵌 Tomcat 线程池参数,优化高并发下的吞吐。
server.tomcat.threads.max 调整最大工作线程;accept-count 等待队列;keep-alive 超时;配合压测工具验证。
# Tomcat 线程池调优
server:
port: 8080
tomcat:
threads:
max: 200 # 最大工作线程数(默认 200)
min-spare: 20 # 最小空闲线程数
max-connections: 10000 # 最大连接数
accept-count: 100 # 等待队列长度
connection-timeout: 20000
# 压测验证
ab -n 10000 -c 100 http://localhost:8080/hello
# 调优原则:
# - 线程数不宜过大,避免上下文切换开销
# - 结合 QPS/RT 指标迭代调整
# - 数据库连接池上限要匹配(默认 HikariCP 10)
单元12 · 综合项目:图书管理系统
分层结构、图书 CRUD、搜索分页、拦截器鉴权、统一异常
- 分层:Controller → Service → Repository,职责清晰。
- 分页:LIMIT 分页 + COUNT 总数,PageResult 封装。
- 事务:@Transactional 保证跨表操作原子性。
- 并发控制:条件 UPDATE(stock > 0)原子扣减防超卖。
实训12.1 图书实体与仓储层
设计 Book 实体与基于 JdbcTemplate/MyBatis 的仓储层,完成基础查询。
实体包含 id/title/author/price/stock 字段;仓储层封装数据访问;Controller 不直接触碰数据。
package com.example.entity;
public class Book {
private Long id;
private String title;
private String author;
private Double price;
private Integer stock;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
}
package com.example.repository;
import com.example.entity.Book;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class BookRepository {
private final JdbcTemplate jdbcTemplate;
public BookRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<Book> findAll() {
return jdbcTemplate.query(
"SELECT * FROM book",
(rs, i) -> {
Book b = new Book();
b.setId(rs.getLong("id"));
b.setTitle(rs.getString("title"));
b.setAuthor(rs.getString("author"));
b.setPrice(rs.getDouble("price"));
b.setStock(rs.getInt("stock"));
return b;
});
}
}
实训12.2 图书搜索与分页接口
实现按标题模糊搜索 + 分页的 REST 接口,返回总数与当前页数据。
查询参数 page/size/keyword;SQL 用 LIMIT 分页并统计总数;返回 PageResult 封装 page/size/total/list。
package com.example.controller;
import com.example.entity.Book;
import com.example.repository.BookRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookRepository repository;
public BookController(BookRepository repository) {
this.repository = repository;
}
@GetMapping
public PageResult<Book> page(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {
List<Book> list = repository.search(keyword,
(page - 1) * size, size);
long total = repository.count(keyword);
return new PageResult<>(page, size, total, list);
}
}
class PageResult<T> {
public int page;
public int size;
public long total;
public List<T> list;
public PageResult(int page, int size, long total, List<T> list) {
this.page = page;
this.size = size;
this.total = total;
this.list = list;
}
}
实训12.3 借阅流程与并发控制
实现图书借阅接口:检查库存、扣减库存、生成借阅记录,处理并发超卖。
UPDATE 语句带 stock > 0 条件原子扣减(乐观锁);@Transactional 保证借阅记录与库存一致;并发测试验证不超卖。
package com.example.service;
import com.example.common.BusinessException;
import com.example.repository.BookRepository;
import com.example.repository.BorrowRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BorrowService {
private final BookRepository bookRepository;
private final BorrowRepository borrowRepository;
public BorrowService(BookRepository bookRepository,
BorrowRepository borrowRepository) {
this.bookRepository = bookRepository;
this.borrowRepository = borrowRepository;
}
@Transactional(rollbackFor = Exception.class)
public void borrow(Long bookId, Long userId) {
// 原子扣减:库存大于 0 才扣减成功,防止超卖
int rows = bookRepository.deductStock(bookId);
if (rows == 0) {
throw new BusinessException(400, "库存不足");
}
// 生成借阅记录
borrowRepository.insert(bookId, userId);
}
}
// SQL
// UPDATE book SET stock = stock - 1
// WHERE id = #{id} AND stock > 0