17-Spring-Framework-MVC 详解

2 阅读11分钟

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 渲染视图,返回响应
详细流程
  1. 请求到达 DispatcherServlet

    • 捕获请求 URL
    • 封装 HttpServletRequest 和 HttpServletResponse
  2. 处理器映射

    • 根据请求 URL 找到对应的 Controller
    • 返回 HandlerExecutionChain(包含拦截器)
  3. 处理器适配

    • 调用 Controller 的方法
    • 执行前置拦截器
  4. 执行 Controller

    • 处理业务逻辑
    • 返回 ModelAndView
  5. 视图解析

    • 解析视图名称为具体视图
    • 执行后置拦截器
  6. 渲染视图

    • 填充模型数据
    • 生成最终响应

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 返回值处理

返回类型
  1. ModelAndView:包含模型数据和视图名称
  2. String:视图名称或 JSON 字符串
  3. Object:自动转为 JSON
  4. void:直接写入响应
  5. 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 设计原则

  1. URL 设计

    • 使用名词表示资源
    • 使用复数形式
    • 层级结构清晰
  2. HTTP 方法

    • GET:查询
    • POST:创建
    • PUT:完整更新
    • PATCH:部分更新
    • DELETE:删除
  3. 状态码使用

    • 200:成功
    • 201:创建成功
    • 204:删除成功
    • 400:参数错误
    • 401:未授权
    • 404:资源不存在
    • 500:服务器错误

最佳实践

  1. 统一响应格式
  2. 全局异常处理
  3. 参数校验
  4. 接口文档
  5. 版本管理

Spring MVC 是构建 Web 应用的强大框架,理解其工作流程和核心组件,遵循 RESTful 设计原则,能够开发出结构清晰、易于维护的 Web 应用。