本备忘清单旨在快速理解 Spring MVC 框架的核心概念,提供了在 Spring Boot 环境下构建 RESTful API 的最常用注解、配置和最佳实践,供您参考。
spring-boot-starter-web 依赖,自动配置了 Spring MVC 的核心组件(如 DispatcherServlet、HandlerMapping 等),使开发者能专注于业务代码。pom.xml 关键依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
@RestController 和 @GetMapping 的组合构成了最基础的 API 接口。
// @RestController 是 @Controller 和 @ResponseBody 的组合注解。
// 1. @Controller:将类声明为 Spring IoC 容器中的一个控制器 Bean。
// 2. @ResponseBody:告知 Spring MVC,此类所有方法返回的都是数据,
// 需要直接写入 HTTP 响应体,通常是 JSON 格式。
@RestController
public class HelloController {
// @GetMapping("/hello") 将 HTTP GET 请求映射到此方法。
// 它是 @RequestMapping(method = RequestMethod.GET, path = "/hello") 的简写。
@GetMapping("/hello")
public String sayHello() {
return "Hello, Spring MVC!";
}
}
所有请求都由 DispatcherServlet (前端控制器) 统一调度,流程简化如下:
DispatcherServlet。DispatcherServlet 询问 HandlerMapping (处理器映射器),找到能处理当前请求的 Controller 方法。HandlerAdapter (处理器适配器) 调用目标 Controller 方法。HttpMessageConverter (消息转换器) 将返回的 Java 对象序列化为 JSON 等格式,写入 HTTP 响应体。在类级别和方法级别上组合使用 @RequestMapping,为一组接口定义公共的父路径。
@RestController
@RequestMapping("/users") // 定义父路径
public class UserController {
// 完整访问路径: /users/all
@GetMapping("/all")
public String getAllUsers() {
return "返回所有用户列表";
}
// 完整访问路径: /users/{id}
@GetMapping("/{id}")
public String getUserById(@PathVariable Long id) {
return "查询用户: " + id;
}
}
用于从 URL 路径中提取动态值。
/**
* {id} 是一个路径占位符。
* @PathVariable("id") 会将 URL 中占位符 {id} 的实际值,
* 绑定到方法的 Long id 参数上。
* 如果方法参数名与占位符名称相同,可以简写为 @PathVariable Long id。
*/
@GetMapping("/{id}")
public String getUserById(@PathVariable("id") Long id) {
return "正在查询 ID 为: " + id + " 的用户";
}
@RequestMapping 支持使用 Ant 风格的通配符进行模糊路径匹配。
?: 匹配任意单个字符。*: 匹配任意数量的字符 (不含 /)。**: 匹配任意数量的字符 (可含 /)。// 匹配 /ant/testA, /ant/testB
@GetMapping("/ant/test?")
public String testAnt1() { return "Ant-style: ?"; }
// 匹配 /ant/testABC, 但不匹配 /ant/test/abc
@GetMapping("/ant/test*")
public String testAnt2() { return "Ant-style: *"; }
// 匹配 /ant/any/path
@GetMapping("/ant/**")
public String testAnt3() { return "Ant-style: **"; }
重要: 在 Spring Boot 3.x 中,出于安全考虑,不推荐在路径中间使用
**。
根据请求中是否包含特定参数或请求头来路由请求。
// 只有当请求 URL 中包含参数 "version" 时才匹配
// 例如: /precise?version=1
@GetMapping(value = "/precise", params = "version")
public String testParams() {
return "Match with param 'version'";
}
// 只有当请求头中包含 "X-API-VERSION" 时才匹配
@GetMapping(value = "/precise", headers = "X-API-VERSION")
public String testHeaders() {
return "Match with header 'X-API-VERSION'";
}
用于获取 URL 查询参数 (? 之后的部分)。
@GetMapping("/search")
public String searchUsers(
@RequestParam("keyword") String keyword,
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page) {
return "关键词: " + keyword + ", 页码: " + page;
}
value: 参数名。required: 是否必需,默认为 true。defaultValue: 默认值。用于将请求体中的 JSON 或 XML 数据反序列化为 Java 对象 (POJO)。
// 1. 定义一个 DTO (Data Transfer Object)
@Data
public class UserSaveDTO {
private String username;
private String password;
}
// 2. 在 Controller 方法中使用 @RequestBody
@PostMapping("/create")
public String createUser(@RequestBody UserSaveDTO user) {
return "成功创建用户: " + user.toString();
}
一个方法中,
@RequestBody注解最多只能使用一次。
当方法参数是一个没有 @RequestBody 注解的 POJO 时,Spring MVC 会自动将同名的 URL 查询参数或表单参数绑定到 POJO 的属性上。
// 请求 URL: /filter?username=lisi&email=lisi@example.com
@GetMapping("/filter")
public String filterUsers(UserQuery query) {
// Spring MVC 会自动将请求参数设置到 query 对象中
return "根据条件筛选用户: " + query.toString();
}
// UserQuery.java
@Data
public class UserQuery {
private String username;
private String email;
}
| 注解 | 作用 | 示例 |
|---|---|---|
@PathVariable | 从 URL 路径 (/users/{id}) 中获取 | @PathVariable Long id |
@RequestHeader | 从 请求头 (Headers) 中获取 | @RequestHeader("User-Agent") String ua |
@CookieValue | 从 Cookie 中获取 | @CookieValue("session-id") String sid |
为了实现各层解耦和保障数据安全,推荐使用不同的对象模型。
| 类型 | 全称 | 约定包名 | 核心职责 |
|---|---|---|---|
| PO | Persistent Object | entity | 持久化对象。与数据库表结构一一对应。 |
| DTO | Data Transfer Object | dto | 数据传输对象。用于接收前端传递的请求数据。 |
| VO | View Object | vo | 视图对象。用于返回给前端的展示数据,可隐藏敏感字段。 |
| QO | Query Object | dto/query | 查询对象。一种特殊的DTO,用于封装复杂的查询条件。 |
在 Service 层进行 PO、DTO、VO 之间的转换是核心业务之一。
场景: 将 User (PO) 转换为 UserVO (VO)。
// PO
@Data
public class User {
private Long id;
private String username;
private Integer status; // 1-正常, 2-禁用
}
// VO
@Data
public class UserVO {
private Long id;
@Alias("username") // Hutool 注解,解决 username -> name 的名称不一致问题
private String name;
private String statusText;
}
// Service 层转换逻辑
private UserVO convertToVO(User user) {
UserVO userVO = new UserVO();
// 1. 使用 BeanUtil 拷贝同名/有@Alias注解的属性
BeanUtil.copyProperties(user, userVO);
// 2. 手动处理需要逻辑转换的属性
if (user.getStatus() != null) {
userVO.setStatusText(user.getStatus() == 1 ? "正常" : "已禁用");
}
return userVO;
}
标准的后端项目通常采用三层架构,职责清晰。
. 📂 src/main/java/com/example
├── 📂 controller <- Controller层: Web入口, 参数校验, 调用Service
├── 📂 service <- Service层: 业务核心, 事务管理, 组合Mapper
│ └── 📂 impl
└── 📂 mapper <- Mapper层: 数据持久层, 与数据库交互
pom.xml 中必须添加 validation 启动器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
使用 jakarta.validation.constraints.* 下的注解为 DTO 字段添加校验规则。
@Data
public class UserSaveDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
}
| 注解 | 作用 |
|---|---|
@NotNull | 验证对象不为 null |
@NotBlank | 验证字符串不为 null 且不为空白 |
@NotEmpty | 验证集合、数组、字符串不为 null 且长度 > 0 |
@Size | 验证大小或长度在指定范围内 |
@Min / @Max | 验证数字在最小值/最大值之间 |
@Email | 验证字符串为合法的邮箱格式 |
@Pattern | 使用正则表达式验证字符串 |
在 Controller 方法中,使用 @Validated 注解激活对 DTO 的校验。
@RestController
@Validated // 可在类上添加,以支持方法级别的参数校验
public class UserController {
@PostMapping("/users")
public void saveUser(@Validated @RequestBody UserSaveDTO dto) {
// ... 如果校验失败,Spring MVC会抛出 MethodArgumentNotValidException
}
}
校验失败的异常,应由全局异常处理器统一捕获并返回友好的错误信息。
使用 @Validated 的分组功能,可以用一个 DTO 应对不同场景(如新增、修改)的校验规则。
public interface ValidationGroups {
interface Save {}
interface Update {}
}
groups 属性:
public class UserEditDTO {
@NotNull(groups = ValidationGroups.Update.class)
private Long id;
@NotBlank(groups = ValidationGroups.Save.class)
private String username;
}
@PostMapping
public void saveUser(@Validated(ValidationGroups.Save.class) @RequestBody UserEditDTO dto) {}
@PutMapping
public void updateUser(@Validated(ValidationGroups.Update.class) @RequestBody UserEditDTO dto) {}
使用 @RestControllerAdvice 和 @ExceptionHandler 统一处理全局异常,返回标准 Result 结构。
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理自定义业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Result<Void>> handleBusinessException(BusinessException ex) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(Result.error(ex.getResultCode()));
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<Void>> handleValidationException(MethodArgumentNotValidException ex) {
// 拼接错误信息
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(Result.error(message));
}
// 兜底处理所有未知异常
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleUnknownException(Exception ex) {
log.error("系统未知异常", ex);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error("系统异常,请联系管理员"));
}
}
通过实现 WebMvcConfigurer 接口进行全局 CORS 配置,是解决跨域问题的最佳实践。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 对所有路径生效
.allowedOrigins("http://localhost:5173") // 允许的前端源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true) // 允许发送 Cookie
.maxAge(3600);
}
}
application.yml):
spring:
servlet:
multipart:
enabled: true
max-file-size: 2MB
app:
upload:
dir: D:/uploads/ # 上传目录
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename();
// ... 生成唯一文件名 ...
String newFileName = generateUniqueName(originalFilename);
// ... 保存文件 ...
file.transferTo(new File(uploadDir, newFileName));
return Result.success(newFileName);
}
静态资源映射 (WebConfig.java): 将 URL 路径映射到物理文件目录,以支持浏览器直接预览。
@Value("${app.upload.dir}")
private String uploadDir;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadDir);
}
强制下载 Controller:
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> download(@PathVariable String filename) {
File file = new File(uploadDir, filename);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.body(resource);
}
拦截器允许在请求处理流程的关键节点执行通用逻辑,是实现权限校验的核心。
HandlerInterceptor 接口。
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 校验逻辑...
// 返回 true 放行,返回 false 拦截
return true;
}
}
WebConfig.java):
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/login", "/register"); // 排除特定路径
}
JWT (JSON Web Token) 是现代 API 中主流的无状态认证方案。
认证流程:
Authorization 请求头中携带 Token (格式: Bearer <token>)。AuthInterceptor 拦截请求,验证 Token 的合法性。登录接口 (签发 Token)
@PostMapping("/login")
public Result<String> login(@RequestBody LoginDTO dto) {
// 1. 验证用户名密码...
// 2. 验证成功,生成 Token
Map<String, Object> payload = new HashMap<>();
payload.put("userId", userId);
payload.put("username", username);
payload.put(JWTPayload.EXPIRES_AT, System.currentTimeMillis() + 1000 * 60 * 60 * 24); // 24小时过期
String token = JWTUtil.createToken(payload, jwtSecret.getBytes());
return Result.success(token);
}
拦截器 (校验 Token)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (StrUtil.isBlank(token) || !token.startsWith("Bearer ")) {
throw new BusinessException(ResultCode.UNAUTHORIZED);
}
token = token.substring(7);
// 使用 Hutool-JWT 验证 Token
if (!JWTUtil.verify(token, jwtSecret.getBytes())) {
throw new BusinessException(ResultCode.UNAUTHORIZED);
}
// 还可以进一步校验payload中的过期时间等
JWTValidator.of(token).validateDate();
return true;
}
让 Swagger UI 支持 Token 认证,方便测试。
@Configuration
@SecurityScheme(
name = "bearerAuth", // 方案名称
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT"
)
public class SpringDocConfig {}
@SecurityRequirement。
@RestController
@RequestMapping("/users")
@SecurityRequirement(name = "bearerAuth")
public class UserController {
// ...
}
HttpMessageConverter (HTTP 消息转换器) 是 Spring MVC 的核心组件,负责在 HTTP 请求/响应体与 Java 对象之间进行序列化和反序列化。
@RequestBody: 触发反序列化。HttpMessageConverter 调用 read() 方法,将请求体 (如 JSON) 转换为 Java 对象 (DTO)。@ResponseBody: 触发序列化。HttpMessageConverter 调用 write() 方法,将 Java 对象 (VO) 转换为响应体 (如 JSON)。| 方法 | 作用 |
|---|---|
canRead() / canWrite() | 能力检测。判断转换器是否能处理指定的 Java 类型和媒体类型 (MIME Type)。 |
read() / write() | 执行转换。进行实际的读写操作。 |
| 实现类 | 核心职责 |
|---|---|
StringHttpMessageConverter | 处理 text/plain 类型的纯字符串。 |
MappingJackson2HttpMessageConverter | 绝对主力。处理 application/json,依赖 Jackson 库。 |
ByteArrayHttpMessageConverter | 处理 application/octet-stream 等二进制数据。 |
FormHttpMessageConverter | 处理 application/x-www-form-urlencoded 表单数据。 |
内容协商是 Spring MVC 从众多转换器中智能选择一个的机制。
Content-Type。它告诉服务器请求体的数据格式。Accept。它告诉服务器客户端期望接收的数据格式。UserVO)。Accept 头 (如 application/json)。HttpMessageConverter。canWrite(UserVO.class, "application/json") 进行“招标”。MappingJackson2HttpMessageConverter 判断自己能处理 UserVO 和 application/json,返回 true。write() 方法,将 UserVO 序列化为 JSON 字符串并写入响应体。