Spring MVC 详解
一、知识概述
Spring MVC 是 Spring 框架的 Web 模块,基于 Model-View-Controller 模式实现,提供了构建 Web 应用程序的完整解决方案。它通过 DispatcherServlet 作为核心控制器,将请求分发给相应的处理器,并返回响应结果。
Spring MVC 核心组件:
- DispatcherServlet:前端控制器
- HandlerMapping:处理器映射
- HandlerAdapter:处理器适配器
- Controller:控制器
- ViewResolver:视图解析器
- View:视图
理解 Spring MVC 的工作流程,是开发 Web 应用的基础。
二、知识点详细讲解
2.1 MVC 架构模式
Model-View-Controller 模式
用户请求 → Controller → Model → View → 用户响应
↓ ↑
业务逻辑 数据
- Model(模型):数据封装和业务逻辑
- View(视图):页面展示
- Controller(控制器):请求处理和流程控制
2.2 Spring MVC 工作流程
1. 用户请求 → DispatcherServlet
2. DispatcherServlet → HandlerMapping(查找处理器)
3. DispatcherServlet → HandlerAdapter(执行处理器)
4. Controller 处理请求,返回 ModelAndView
5. DispatcherServlet → ViewResolver(解析视图)
6. View 渲染视图,返回响应
详细流程
-
请求到达 DispatcherServlet
- 捕获请求 URL
- 封装 HttpServletRequest 和 HttpServletResponse
-
处理器映射
- 根据请求 URL 找到对应的 Controller
- 返回 HandlerExecutionChain(包含拦截器)
-
处理器适配
- 调用 Controller 的方法
- 执行前置拦截器
-
执行 Controller
- 处理业务逻辑
- 返回 ModelAndView
-
视图解析
- 解析视图名称为具体视图
- 执行后置拦截器
-
渲染视图
- 填充模型数据
- 生成最终响应
2.3 核心注解
@Controller 和 @RestController
@Controller // 返回视图
@RestController // 返回 JSON(@Controller + @ResponseBody)
@RequestMapping
@RequestMapping(value = "/users", method = RequestMethod.GET)
@GetMapping("/users")
@PostMapping("/users")
@PutMapping("/users/{id}")
@DeleteMapping("/users/{id}")
@PatchMapping("/users/{id}")
参数绑定注解
| 注解 | 说明 |
|---|---|
| @RequestParam | 绑定请求参数 |
| @PathVariable | 绑定路径变量 |
| @RequestBody | 绑定请求体 |
| @RequestHeader | 绑定请求头 |
| @CookieValue | 绑定 Cookie |
| @ModelAttribute | 绑定模型属性 |
| @SessionAttribute | 绑定会话属性 |
2.4 返回值处理
返回类型
- ModelAndView:包含模型数据和视图名称
- String:视图名称或 JSON 字符串
- Object:自动转为 JSON
- void:直接写入响应
- ResponseEntity:包含状态码和响应体
2.5 拦截器
HandlerInterceptor 接口
public interface HandlerInterceptor {
boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler);
void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler, ModelAndView modelAndView);
void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex);
}
2.6 异常处理
@ExceptionHandler
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.status(500).body(e.getMessage());
}
@ControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
// 全局异常处理
}
}
三、代码示例
3.1 Controller 基础示例
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
// 实体类
public class User {
private Long id;
private String username;
private String email;
private Integer age;
public User() {}
public User(Long id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
// getter/setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public String toString() {
return "User{id=" + id + ", username='" + username + "', email='" + email + "'}";
}
}
// RESTful Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// GET 查询所有用户
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
// GET 根据 ID 查询用户
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.findById(id);
}
// GET 查询参数
@GetMapping("/search")
public List<User> searchUsers(
@RequestParam(required = false) String username,
@RequestParam(required = false) String email,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return userService.search(username, email, page, size);
}
// POST 创建用户
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.create(user);
return ResponseEntity.ok(created);
}
// PUT 更新用户
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return userService.update(user);
}
// DELETE 删除用户
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
// PATCH 部分更新
@PatchMapping("/{id}")
public User patchUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
return userService.partialUpdate(id, updates);
}
// 获取请求头
@GetMapping("/headers")
public Map<String, String> getHeaders(@RequestHeader Map<String, String> headers) {
return headers;
}
// 获取特定请求头
@GetMapping("/auth")
public String getAuth(@RequestHeader("Authorization") String auth) {
return "Authorization: " + auth;
}
// 获取 Cookie
@GetMapping("/cookie")
public String getCookie(@CookieValue(value = "sessionId", required = false) String sessionId) {
return "Session ID: " + sessionId;
}
}
// Service 层
@Service
public class UserService {
private final Map<Long, User> users = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public List<User> findAll() {
return new ArrayList<>(users.values());
}
public User findById(Long id) {
User user = users.get(id);
if (user == null) {
throw new UserNotFoundException("User not found with id: " + id);
}
return user;
}
public List<User> search(String username, String email, int page, int size) {
return users.values().stream()
.filter(u -> username == null || u.getUsername().contains(username))
.filter(u -> email == null || u.getEmail().contains(email))
.skip(page * size)
.limit(size)
.collect(Collectors.toList());
}
public User create(User user) {
user.setId(idGenerator.getAndIncrement());
users.put(user.getId(), user);
return user;
}
public User update(User user) {
if (!users.containsKey(user.getId())) {
throw new UserNotFoundException("User not found");
}
users.put(user.getId(), user);
return user;
}
public void delete(Long id) {
if (users.remove(id) == null) {
throw new UserNotFoundException("User not found");
}
}
public User partialUpdate(Long id, Map<String, Object> updates) {
User user = findById(id);
if (updates.containsKey("username")) {
user.setUsername((String) updates.get("username"));
}
if (updates.containsKey("email")) {
user.setEmail((String) updates.get("email"));
}
return user;
}
}
// 自定义异常
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
3.2 参数绑定示例
import org.springframework.web.bind.annotation.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.util.*;
@RestController
@RequestMapping("/api/params")
public class ParameterBindingController {
// 路径变量
@GetMapping("/path/{id}")
public String pathVariable(@PathVariable Long id) {
return "ID from path: " + id;
}
// 多个路径变量
@GetMapping("/path/{category}/{id}")
public String multiplePathVariables(
@PathVariable String category,
@PathVariable Long id) {
return "Category: " + category + ", ID: " + id;
}
// 请求参数
@GetMapping("/query")
public String queryParam(
@RequestParam String name,
@RequestParam(required = false) Integer age,
@RequestParam(defaultValue = "guest") String role) {
return String.format("Name: %s, Age: %s, Role: %s", name, age, role);
}
// 多值参数
@GetMapping("/multi")
public String multiValueParam(@RequestParam List<String> tags) {
return "Tags: " + tags;
}
// 请求体 - JSON
@PostMapping("/body")
public User requestBody(@RequestBody User user) {
return user;
}
// 表单数据
@PostMapping("/form")
public String formData(@ModelAttribute User user) {
return "Form data: " + user;
}
// 日期参数
@GetMapping("/date")
public String dateParam(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
return "Date range: " + startDate + " to " + endDate;
}
// 枚举参数
@GetMapping("/status")
public String enumParam(@RequestParam UserStatus status) {
return "Status: " + status;
}
// 自定义对象参数
@GetMapping("/search")
public String searchParams(@ModelAttribute SearchCriteria criteria) {
return "Search: " + criteria;
}
}
// 搜索条件
public class SearchCriteria {
private String keyword;
private Integer minAge;
private Integer maxAge;
private String sortBy;
private String sortOrder;
// getter/setter
@Override
public String toString() {
return String.format("Criteria{keyword='%s', age=[%d,%d], sort=%s %s}",
keyword, minAge, maxAge, sortBy, sortOrder);
}
}
// 用户状态枚举
public enum UserStatus {
ACTIVE, INACTIVE, PENDING, BANNED
}
3.3 返回值处理示例
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.*;
@RestController
@RequestMapping("/api/response")
public class ResponseController {
// 返回对象(自动转 JSON)
@GetMapping("/object")
public User returnObject() {
return new User(1L, "张三", "zhangsan@example.com");
}
// 返回集合
@GetMapping("/list")
public List<User> returnList() {
return Arrays.asList(
new User(1L, "张三", "zhangsan@example.com"),
new User(2L, "李四", "lisi@example.com")
);
}
// ResponseEntity - 完整控制
@GetMapping("/entity")
public ResponseEntity<User> responseEntity() {
User user = new User(1L, "张三", "zhangsan@example.com");
HttpHeaders headers = new HttpHeaders();
headers.add("X-Custom-Header", "custom-value");
return ResponseEntity.ok()
.headers(headers)
.body(user);
}
// 带状态的 ResponseEntity
@PostMapping("/create")
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = new User(1L, user.getUsername(), user.getEmail());
return ResponseEntity
.status(HttpStatus.CREATED)
.header("Location", "/api/users/1")
.body(created);
}
// 错误响应
@GetMapping("/error")
public ResponseEntity<Map<String, Object>> error() {
Map<String, Object> body = new HashMap<>();
body.put("code", 404);
body.put("message", "Resource not found");
body.put("timestamp", System.currentTimeMillis());
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(body);
}
// 异步处理
@GetMapping("/async")
public CompletableFuture<User> asyncResponse() {
return CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new User(1L, "异步用户", "async@example.com");
});
}
// 流式响应
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<User> streamResponse() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> new User(i, "User-" + i, "user" + i + "@example.com"))
.take(5);
}
// 下载文件
@GetMapping("/download")
public ResponseEntity<byte[]> downloadFile() {
byte[] content = "Hello, World!".getBytes();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"hello.txt\"")
.contentType(MediaType.TEXT_PLAIN)
.body(content);
}
// 重定向
@GetMapping("/redirect")
public String redirect() {
return "redirect:/api/users";
}
// 转发
@GetMapping("/forward")
public String forward() {
return "forward:/api/users";
}
}
// 控制器返回视图示例
@Controller
@RequestMapping("/page")
public class PageController {
@GetMapping("/home")
public ModelAndView home() {
ModelAndView mav = new ModelAndView("home");
mav.addObject("title", "首页");
mav.addObject("users", Arrays.asList(
new User(1L, "张三", "zhangsan@example.com"),
new User(2L, "李四", "lisi@example.com")
));
return mav;
}
@GetMapping("/profile")
public String profile(Model model) {
model.addAttribute("user", new User(1L, "张三", "zhangsan@example.com"));
return "profile";
}
}
3.4 拦截器示例
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.*;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicLong;
// 日志拦截器
@Component
public class LoggingInterceptor implements HandlerInterceptor {
private static final String START_TIME = "startTime";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute(START_TIME, startTime);
System.out.println("[LOG] 请求开始: " + request.getMethod() + " " + request.getRequestURI());
System.out.println("[LOG] 客户端IP: " + getClientIp(request));
return true; // 继续
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("[LOG] 请求处理完成");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
long startTime = (Long) request.getAttribute(START_TIME);
long duration = System.currentTimeMillis() - startTime;
System.out.println("[LOG] 请求结束: " + request.getRequestURI());
System.out.println("[LOG] 耗时: " + duration + " ms");
System.out.println("[LOG] 状态码: " + response.getStatus());
if (ex != null) {
System.out.println("[LOG] 异常: " + ex.getMessage());
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip;
}
}
// 认证拦截器
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未授权\"}");
return false;
}
// 设置用户信息到请求属性
request.setAttribute("userId", getUserIdFromToken(token));
return true;
}
private boolean validateToken(String token) {
// Token 验证逻辑
return token.startsWith("Bearer ");
}
private String getUserIdFromToken(String token) {
// 解析 Token 获取用户 ID
return "user-123";
}
}
// 限流拦截器
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
private static final int MAX_REQUESTS_PER_MINUTE = 60;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String clientId = getClientId(request);
RateLimiter limiter = limiters.computeIfAbsent(
clientId,
k -> new RateLimiter(MAX_REQUESTS_PER_MINUTE)
);
if (!limiter.tryAcquire()) {
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁\"}");
return false;
}
return true;
}
private String getClientId(HttpServletRequest request) {
return request.getHeader("X-Client-Id");
}
static class RateLimiter {
private final int maxRequests;
private final Map<Long, AtomicLong> counters = new ConcurrentHashMap<>();
public RateLimiter(int maxRequests) {
this.maxRequests = maxRequests;
}
public boolean tryAcquire() {
long currentMinute = System.currentTimeMillis() / 60000;
AtomicLong counter = counters.computeIfAbsent(currentMinute, k -> new AtomicLong(0));
return counter.incrementAndGet() <= maxRequests;
}
}
}
// 拦截器配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final LoggingInterceptor loggingInterceptor;
private final AuthenticationInterceptor authInterceptor;
private final RateLimitInterceptor rateLimitInterceptor;
public WebMvcConfig(LoggingInterceptor loggingInterceptor,
AuthenticationInterceptor authInterceptor,
RateLimitInterceptor rateLimitInterceptor) {
this.loggingInterceptor = loggingInterceptor;
this.authInterceptor = authInterceptor;
this.rateLimitInterceptor = rateLimitInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor)
.addPathPatterns("/**")
.order(1);
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**")
.order(2);
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/**", "/api/public/**")
.order(3);
}
}
3.5 异常处理示例
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.context.request.WebRequest;
import org.springframework.validation.BindException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import java.time.LocalDateTime;
import java.util.*;
// 全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
// 处理自定义异常
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"User Not Found",
ex.getMessage(),
request.getDescription(false),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex, WebRequest request) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation Failed",
"参数校验失败",
request.getDescription(false),
LocalDateTime.now(),
errors
);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
// 处理参数绑定异常
@ExceptionHandler(BindException.class)
public ResponseEntity<ErrorResponse> handleBindException(
BindException ex, WebRequest request) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Binding Failed",
"参数绑定失败",
request.getDescription(false),
LocalDateTime.now(),
errors
);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
// 处理参数类型转换异常
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleTypeMismatch(
MethodArgumentTypeMismatchException ex, WebRequest request) {
String message = String.format("参数 '%s' 应该是 '%s' 类型",
ex.getName(), ex.getRequiredType().getSimpleName());
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Type Mismatch",
message,
request.getDescription(false),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Business Error",
ex.getMessage(),
request.getDescription(false),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
// 处理所有其他异常
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(
Exception ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
"系统内部错误",
request.getDescription(false),
LocalDateTime.now()
);
// 记录日志
System.err.println("未处理的异常: " + ex.getMessage());
ex.printStackTrace();
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// 错误响应类
public class ErrorResponse {
private int status;
private String error;
private String message;
private String path;
private LocalDateTime timestamp;
private List<String> details;
public ErrorResponse(int status, String error, String message,
String path, LocalDateTime timestamp) {
this.status = status;
this.error = error;
this.message = message;
this.path = path;
this.timestamp = timestamp;
}
public ErrorResponse(int status, String error, String message,
String path, LocalDateTime timestamp, List<String> details) {
this(status, error, message, path, timestamp);
this.details = details;
}
// getter/setter
}
// 业务异常
public class BusinessException extends RuntimeException {
private String errorCode;
public BusinessException(String message) {
super(message);
}
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
3.6 参数校验示例
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
// 校验注解示例
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 20, message = "密码长度必须在8-20之间")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
message = "密码必须包含大小写字母和数字")
private String password;
@Min(value = 1, message = "年龄必须大于0")
@Max(value = 150, message = "年龄必须小于150")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
// getter/setter
}
// 分组校验
public class UserUpdateRequest {
public interface UpdateGroup {}
@NotNull(message = "ID不能为空", groups = UpdateGroup.class)
private Long id;
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间", groups = UpdateGroup.class)
private String username;
@Email(message = "邮箱格式不正确", groups = UpdateGroup.class)
private String email;
// getter/setter
}
// 自定义校验注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
public @interface ValidPassword {
String message() default "密码强度不够";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 自定义校验器
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (password == null) {
return true; // null 值由 @NotNull 处理
}
// 密码强度校验
boolean hasUpper = password.chars().anyMatch(Character::isUpperCase);
boolean hasLower = password.chars().anyMatch(Character::isLowerCase);
boolean hasDigit = password.chars().anyMatch(Character::isDigit);
boolean hasSpecial = password.chars().anyMatch(ch -> "!@#$%^&*".indexOf(ch) >= 0);
return hasUpper && hasLower && hasDigit && hasSpecial;
}
}
// Controller 使用校验
@RestController
@RequestMapping("/api/users")
@Validated // 启用方法参数校验
public class UserValidationController {
// 校验请求体
@PostMapping
public User createUser(@Valid @RequestBody UserCreateRequest request) {
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
return user;
}
// 分组校验
@PutMapping("/{id}")
public User updateUser(
@PathVariable Long id,
@Validated(UserUpdateRequest.UpdateGroup.class)
@RequestBody UserUpdateRequest request) {
// 更新逻辑
return new User(id, request.getUsername(), request.getEmail());
}
// 校验路径变量
@GetMapping("/{id}")
public User getUser(
@PathVariable
@Min(value = 1, message = "ID必须大于0") Long id) {
return new User(id, "user-" + id, "user@example.com");
}
// 校验请求参数
@GetMapping("/search")
public List<User> search(
@RequestParam
@Size(min = 2, max = 50, message = "关键词长度必须在2-50之间") String keyword) {
return Arrays.asList(new User(1L, keyword, keyword + "@example.com"));
}
}
四、实战应用场景
4.1 RESTful API 设计
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.data.domain.*;
import java.util.*;
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// 列表查询(分页、排序、过滤)
@GetMapping
public ResponseEntity<Page<Product>> list(
@RequestParam(required = false) String name,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "desc") String sortDirection) {
ProductQuery query = new ProductQuery(name, categoryId, minPrice, maxPrice);
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
Page<Product> products = productService.query(query, pageable);
return ResponseEntity.ok(products);
}
// 详情查询
@GetMapping("/{id}")
public ResponseEntity<Product> get(@PathVariable Long id) {
Product product = productService.findById(id);
return ResponseEntity.ok(product);
}
// 创建
@PostMapping
public ResponseEntity<Product> create(@Valid @RequestBody ProductCreateRequest request) {
Product product = productService.create(request);
return ResponseEntity
.created(URI.create("/api/v1/products/" + product.getId()))
.body(product);
}
// 更新
@PutMapping("/{id}")
public ResponseEntity<Product> update(
@PathVariable Long id,
@Valid @RequestBody ProductUpdateRequest request) {
Product product = productService.update(id, request);
return ResponseEntity.ok(product);
}
// 删除
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build();
}
// 批量操作
@PostMapping("/batch")
public ResponseEntity<BatchResult> batchCreate(
@Valid @RequestBody List<ProductCreateRequest> requests) {
BatchResult result = productService.batchCreate(requests);
return ResponseEntity.ok(result);
}
// 导出
@GetMapping("/export")
public ResponseEntity<byte[]> export(ProductQuery query) {
byte[] data = productService.export(query);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=products.xlsx")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(data);
}
}
4.2 文件上传下载
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.http.*;
import java.io.*;
import java.nio.file.*;
import java.util.*;
@RestController
@RequestMapping("/api/files")
public class FileController {
private final FileStorageService fileStorageService;
public FileController(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
// 单文件上传
@PostMapping("/upload")
public ResponseEntity<FileInfo> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam(required = false) String category) {
if (file.isEmpty()) {
throw new BusinessException("文件不能为空");
}
FileInfo fileInfo = fileStorageService.store(file, category);
return ResponseEntity.ok(fileInfo);
}
// 多文件上传
@PostMapping("/upload/multiple")
public ResponseEntity<List<FileInfo>> uploadFiles(
@RequestParam("files") MultipartFile[] files) {
List<FileInfo> results = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
results.add(fileStorageService.store(file, null));
}
}
return ResponseEntity.ok(results);
}
// 文件下载
@GetMapping("/download/{id}")
public ResponseEntity<Resource> downloadFile(@PathVariable String id) {
FileResource fileResource = fileStorageService.load(id);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(fileResource.getContentType()))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileResource.getFilename() + "\"")
.body(fileResource.getResource());
}
// 预览文件
@GetMapping("/preview/{id}")
public ResponseEntity<Resource> previewFile(@PathVariable String id) {
FileResource fileResource = fileStorageService.load(id);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(fileResource.getContentType()))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline")
.body(fileResource.getResource());
}
// 删除文件
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteFile(@PathVariable String id) {
fileStorageService.delete(id);
return ResponseEntity.noContent().build();
}
// 获取文件信息
@GetMapping("/{id}/info")
public ResponseEntity<FileInfo> getFileInfo(@PathVariable String id) {
FileInfo info = fileStorageService.getFileInfo(id);
return ResponseEntity.ok(info);
}
}
// 文件存储服务
@Service
public class FileStorageService {
private final Path rootLocation = Paths.get("uploads");
public FileInfo store(MultipartFile file, String category) {
try {
String fileId = UUID.randomUUID().toString();
String originalFilename = file.getOriginalFilename();
String extension = getFileExtension(originalFilename);
String storedFilename = fileId + extension;
Path targetLocation = rootLocation.resolve(storedFilename);
Files.copy(file.getInputStream(), targetLocation);
FileInfo info = new FileInfo();
info.setId(fileId);
info.setOriginalFilename(originalFilename);
info.setStoredFilename(storedFilename);
info.setContentType(file.getContentType());
info.setSize(file.getSize());
info.setCategory(category);
info.setUploadTime(LocalDateTime.now());
return info;
} catch (IOException e) {
throw new BusinessException("文件存储失败: " + e.getMessage());
}
}
public FileResource load(String id) {
// 加载文件
return null;
}
public void delete(String id) {
// 删除文件
}
private String getFileExtension(String filename) {
return filename.substring(filename.lastIndexOf("."));
}
}
五、总结与最佳实践
RESTful API 设计原则
-
URL 设计
- 使用名词表示资源
- 使用复数形式
- 层级结构清晰
-
HTTP 方法
- GET:查询
- POST:创建
- PUT:完整更新
- PATCH:部分更新
- DELETE:删除
-
状态码使用
- 200:成功
- 201:创建成功
- 204:删除成功
- 400:参数错误
- 401:未授权
- 404:资源不存在
- 500:服务器错误
最佳实践
- 统一响应格式
- 全局异常处理
- 参数校验
- 接口文档
- 版本管理
Spring MVC 是构建 Web 应用的强大框架,理解其工作流程和核心组件,遵循 RESTful 设计原则,能够开发出结构清晰、易于维护的 Web 应用。