Spring MVC 深度系统性解析:架构、原理、注解、校验、异常、文件与拦截器

Spring MVC核心机制解析

以下是为您精心撰写的 《Spring MVC 深度系统性解析:架构、原理、注解、校验、异常、文件与拦截器》 完整说明文档,涵盖您提出的所有核心内容,并附带一个真实可落地、企业级参考的综合性实战示例

本指南不仅解释“是什么”,更深入剖析“为什么”和“如何用”,帮助您从“会用注解”进阶到“理解 Spring MVC 设计哲学”。


📜 Spring MVC 深度系统性解析:架构、原理、注解、校验、异常、文件与拦截器

目标:全面掌握 Spring MVC 的核心机制、处理流程、注解体系与实战设计,构建企业级 Web 应用能力


✅ 一、Spring MVC 是什么?它有什么作用?

1.1 定义

Spring MVC(Model-View-Controller) 是 Spring 框架中用于构建 Web 应用程序 的模块,它是一个基于 Servlet API 的、轻量级、松耦合、高度可扩展 的 Web 框架。

它实现了经典的 MVC 设计模式,将 Web 请求的处理分为三个核心组件:

组件职责
Model(模型)业务数据和状态(如 User, Order 对象)
View(视图)数据展示层(如 JSP、Thymeleaf、JSON、XML)
Controller(控制器)接收请求、调用服务、返回模型和视图名称

1.2 核心作用

作用说明
统一请求处理将 HTTP 请求映射到 Java 方法,替代原始 Servlet 的 doGet/doPost
参数自动绑定自动将请求参数(Query、Body、Header)绑定到方法参数或对象
数据格式化与转换自动将字符串转为 DateInteger、枚举等类型
视图解析根据返回值自动选择视图(如跳转 JSP 或返回 JSON)
数据校验集成 JSR-303/JSR-380,实现声明式参数校验
全局异常处理统一捕获并格式化异常,避免暴露敏感信息
文件上传下载提供 MultipartFileResource 等便捷 API
拦截器机制在请求处理前后插入自定义逻辑(如登录校验、日志)
RESTful 支持原生支持 @RestController,构建 JSON API

一句话总结
Spring MVC 让你用写 Java 方法的方式,来处理 HTTP 请求,彻底告别原始 Servlet 的样板代码。


✅ 二、Spring MVC 的实现原理与核心架构

2.1 核心架构图(简化版)

客户端 (Browser)
        ↓
    [HTTP Request]
        ↓
┌───────────────────────┐
│   DispatcherServlet   │ ← Spring MVC 的“总入口”(前端控制器)
└──────────┬────────────┘
           ↓
┌───────────────────────┐
│ HandlerMapping        │ ← 根据 URL 找到对应的 Controller 方法
└──────────┬────────────┘
           ↓
┌───────────────────────┐
│     Controller        │ ← 执行业务逻辑,返回 Model & ViewName
└──────────┬────────────┘
           ↓
┌───────────────────────┐
│   ViewResolver        │ ← 根据 ViewName 找到实际视图(如 Thymeleaf 模板)
└──────────┬────────────┘
           ↓
┌───────────────────────┐
│       View            │ ← 渲染数据,生成 HTML/JSON/XML
└──────────┬────────────┘
           ↓
┌───────────────────────┐
│   HTTP Response       │ ← 返回给客户端
└───────────────────────┘

2.2 核心组件详解

组件作用关键类
DispatcherServlet前端控制器,所有请求的入口,负责协调其他组件org.springframework.web.servlet.DispatcherServlet
HandlerMapping映射请求 URL 到具体的 Controller 方法RequestMappingHandlerMapping(最常用)
HandlerAdapter调用 Controller 方法,适配不同类型的控制器RequestMappingHandlerAdapter
Controller业务逻辑处理器,处理请求并返回模型和视图@Controller, @RestController
ViewResolver将逻辑视图名解析为物理视图(模板文件)ThymeleafViewResolver, JsonView
View渲染模型数据,生成最终响应内容ThymeleafTemplate, MappingJackson2JsonView
HandlerInterceptor拦截器,在请求处理前后执行自定义逻辑实现 HandlerInterceptor 接口

关键设计
“前端控制器 + 多处理器”模式 —— 一个 DispatcherServlet 统一入口,多个 HandlerMappingHandlerAdapter 分别处理不同类型的请求,实现高度解耦。


✅ 三、Spring MVC 请求处理流程(详细步骤)

Client DispatcherServlet HandlerMapping HandlerAdapter Controller ViewResolver View 1. 发送 HTTP 请求 2. 查询 HandlerMapping,找到匹配的 Controller 方法 3. 返回 HandlerExecutionChain(含 Controller 方法 + 拦截器) 4. 选择合适的 HandlerAdapter(如 RequestMappingHandlerAdapter) 5. 调用 Controller 方法(执行业务逻辑) 6. 返回 ModelAndView(包含数据和视图名) 7. 返回 ModelAndView 8. 用视图名查找 View(如 "user/list" → /WEB-INF/views/user/list.html) 9. 返回 View 对象 10. 调用 View.render(),将 Model 数据渲染成 HTML/JSON 11. 返回渲染后的响应内容 12. 返回最终 HTTP 响应 Client DispatcherServlet HandlerMapping HandlerAdapter Controller ViewResolver View

重点

  • 所有请求都经过 DispatcherServlet,这是 Spring MVC 的“心脏”。
  • @Controller 方法不是直接被调用,而是通过 HandlerAdapter 动态反射调用。
  • 拦截器在步骤 2 和 10 之间执行preHandlepostHandleafterCompletion)。

✅ 四、常用请求注解详解(请求映射与参数处理)

注解类型作用示例说明
@RequestMapping映射通用请求映射@RequestMapping("/users")可指定 method, params, headers
@GetMapping映射GET 请求映射@GetMapping("/users/{id}")等价于 @RequestMapping(method = GET)
@PostMapping映射POST 请求映射@PostMapping("/users")
@PutMapping映射PUT 请求映射@PutMapping("/users/{id}")
@DeleteMapping映射DELETE 请求映射@DeleteMapping("/users/{id}")
@PatchMapping映射PATCH 请求映射@PatchMapping("/users/{id}")部分更新
@RequestParam参数绑定查询参数或表单字段@RequestParam("name") String name必须存在,否则报错
@RequestParam(required = false)参数可选参数@RequestParam(value = "age", required = false) Integer age值为 null 时不会报错
@RequestParam(defaultValue = "0")参数默认值@RequestParam(defaultValue = "0") int page参数缺失时使用默认值
@PathVariable参数绑定 URL 路径变量@PathVariable("id") Long id/users/1001id=1001
@RequestBody参数绑定请求体(JSON/XML)@RequestBody User user需配合 Content-Type: application/json
@RequestHeader参数绑定请求头@RequestHeader("Authorization") String token
@CookieValue参数绑定 Cookie@CookieValue("JSESSIONID") String sessionId
@ModelAttribute参数绑定表单对象或预填充数据@ModelAttribute User user通常用于表单提交
@SessionAttribute参数获取 Session 中的属性@SessionAttribute("user") User user需先存入 HttpSession
@RequestPart参数绑定 multipart 文件或表单字段@RequestPart("file") MultipartFile file用于文件上传

最佳实践

  • REST API 推荐@RestController + @GetMapping/@PostMapping + @RequestBody + @PathVariable
  • 表单提交推荐@Controller + @PostMapping + @ModelAttribute
  • 避免字段注入:永远使用 @RequestParam@PathVariable 显式绑定,不要直接用 @Autowired 注入请求参数

✅ 五、请求数据校验:JSR-303/JSR-380 + 自定义校验

5.1 什么是 JSR-303 / JSR-380?

  • JSR-303:Java EE 6 标准,定义了 Bean 校验的 API(javax.validation
  • JSR-380:Java EE 8 标准,JSR-303 的升级版(jakarta.validation),Spring Boot 2.3+ 默认使用
  • Hibernate Validator:JSR-380 的官方参考实现

5.2 常用注解

注解说明示例
@NotNull非空(对象不为 null)@NotNull private String name;
@NotEmpty非空且长度 > 0(字符串、集合、数组)@NotEmpty private List<String> roles;
@NotBlank非空、非空白(字符串,去除空格后不为空)@NotBlank private String email;
@Size(min=2, max=10)长度校验@Size(min=6, max=20) private String password;
@Email邮箱格式校验@Email private String email;
@Min(18)数值 ≥ 最小值@Min(18) private Integer age;
@Max(100)数值 ≤ 最大值@Max(100) private Integer score;
@Pattern(regexp = "^[0-9]{11}$")正则校验@Pattern(regexp = "^[1-9]\\d{10}$") private String phone;
@Valid嵌套校验@Valid private User user;

5.3 使用方式

✅ 步骤1:在 Controller 方法上使用 @Valid
@PostMapping("/users")
public ResponseEntity<String> createUser(@Valid @RequestBody UserCreateRequest request, BindingResult bindingResult) {
    // ✅ 如果校验失败,bindingResult 会包含错误信息
    if (bindingResult.hasErrors()) {
        return ResponseEntity.badRequest().body("校验失败:" + bindingResult.getAllErrors());
    }

    userService.save(request);
    return ResponseEntity.ok("用户创建成功");
}
✅ 步骤2:在 DTO 类上标注校验注解
package com.example.dto;

import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class UserCreateRequest {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在 2~20 之间")
    private String username;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码至少 6 位")
    private String password;

    @Min(value = 18, message = "年龄不能小于 18 岁")
    @Max(value = 150, message = "年龄不能大于 150 岁")
    private Integer age;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
}

注意

  • @Valid 必须加在 方法参数 上,才能触发校验
  • BindingResult 必须紧跟在 @Valid 参数后,否则会抛出 MethodArgumentNotValidException

5.4 自定义校验注解(高级)

✅ 步骤1:定义注解
package com.example.validator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD}) // 只能用于字段
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Constraint(validatedBy = MobileValidator.class) // 指定校验器
public @interface ValidMobile {

    String message() default "手机号格式不正确";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
✅ 步骤2:实现校验器
package com.example.validator;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.regex.Pattern;

public class MobileValidator implements ConstraintValidator<ValidMobile, String> {

    private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$";
    private Pattern pattern;

    @Override
    public void initialize(ValidMobile constraintAnnotation) {
        // 初始化(可选)
        this.pattern = Pattern.compile(MOBILE_PATTERN);
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 如果为 null,不校验(由 @NotNull 处理)
        if (value == null || value.trim().isEmpty()) {
            return true;
        }
        return pattern.matcher(value).matches();
    }
}
✅ 步骤3:使用自定义注解
@ValidMobile(message = "请输入正确的中国大陆手机号")
private String phone;

优势:复用性强,团队统一规范,校验逻辑集中管理。


✅ 六、全局异常处理:@ExceptionHandler + @RestControllerAdvice

6.1 为什么需要全局异常处理?

  • 避免每个 Controller 都写 try-catch
  • 统一返回格式(如 {code: 200, msg: "ok", data: {}}
  • 防止敏感信息泄露(如数据库异常、堆栈信息)
  • 统一记录错误日志

6.2 核心注解

注解作用适用场景
@ExceptionHandler方法级异常处理器仅对当前 Controller 生效
@ControllerAdvice全局异常处理器(普通 Web)处理所有 Controller 的异常
@RestControllerAdvice全局 REST 异常处理器(推荐)处理所有 @RestController 的异常,自动加 @ResponseBody

6.3 实战:统一响应格式 + 全局异常处理

package com.example.exception;

import com.example.dto.CommonResult;
import jakarta.validation.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * ✅ 全局 REST 异常处理器(推荐)
 * 作用:统一返回格式、捕获所有异常、记录日志、避免堆栈暴露
 */
@RestControllerAdvice // ✅ 等价于 @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * ✅ 处理方法参数校验失败(@Valid 失败)
     * 触发条件:@RequestBody 或 @ModelAttribute 上有 @Valid,但校验失败
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<CommonResult<?>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.warn("参数校验失败:{}", e.getMessage());

        Map<String, String> errors = new HashMap<>();
        for (FieldError error : e.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }

        CommonResult<?> result = CommonResult.fail(400, "参数校验失败", errors);
        return ResponseEntity.badRequest().body(result);
    }

    /**
     * ✅ 处理 JSR-380 单字段校验失败(如 @ValidMobile)
     * 触发条件:@Valid 作用于单个对象,但校验失败
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<CommonResult<?>> handleConstraintViolationException(ConstraintViolationException e) {
        log.warn("参数校验失败(ConstraintViolation):{}", e.getMessage());

        Map<String, String> errors = new HashMap<>();
        e.getConstraintViolations().forEach(violation ->
            errors.put(violation.getPropertyPath().toString(), violation.getMessage())
        );

        CommonResult<?> result = CommonResult.fail(400, "参数校验失败", errors);
        return ResponseEntity.badRequest().body(result);
    }

    /**
     * ✅ 处理绑定异常(如类型转换失败:字符串传给 Integer)
     */
    @ExceptionHandler(BindException.class)
    public ResponseEntity<CommonResult<?>> handleBindException(BindException e) {
        log.warn("参数绑定失败:{}", e.getMessage());

        Map<String, String> errors = new HashMap<>();
        for (FieldError error : e.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }

        CommonResult<?> result = CommonResult.fail(400, "参数绑定失败", errors);
        return ResponseEntity.badRequest().body(result);
    }

    /**
     * ✅ 处理自定义业务异常(如用户不存在、余额不足)
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<CommonResult<?>> handleBusinessException(BusinessException e) {
        log.warn("业务异常:{}", e.getMessage());

        CommonResult<?> result = CommonResult.fail(e.getCode(), e.getMessage(), null);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
    }

    /**
     * ✅ 处理其他所有未捕获的异常(如空指针、数据库连接失败)
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<CommonResult<?>> handleGenericException(Exception e) {
        log.error("系统内部错误", e);

        CommonResult<?> result = CommonResult.fail(500, "系统内部错误,请稍后再试", null);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
    }
}

6.4 统一响应封装类

package com.example.dto;

import lombok.Data;

/**
 * 统一响应格式(前后端约定)
 * {
 *   "code": 200,
 *   "msg": "成功",
 *   "data": { ... }
 * }
 */
@Data
public class CommonResult<T> {
    private Integer code;
    private String msg;
    private T data;

    public static <T> CommonResult<T> success(T data) {
        return new CommonResult<>(200, "成功", data);
    }

    public static <T> CommonResult<T> fail(Integer code, String msg, T data) {
        CommonResult<T> result = new CommonResult<>();
        result.code = code;
        result.msg = msg;
        result.data = data;
        return result;
    }

    public static <T> CommonResult<T> fail(String msg, T data) {
        return fail(500, msg, data);
    }

    public static <T> CommonResult<T> fail(String msg) {
        return fail(500, msg, null);
    }

    // 构造器
    public CommonResult() {}

    public CommonResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

6.5 自定义业务异常

package com.example.exception;

public class BusinessException extends RuntimeException {
    private final Integer code;

    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }
}

使用场景

if (user == null) {
    throw new BusinessException(1001, "用户不存在");
}

最终返回格式

{
  "code": 1001,
  "msg": "用户不存在",
  "data": null
}

✅ 七、文件上传与下载

7.1 文件上传:MultipartFile

✅ 前端表单(HTML)
<form method="post" enctype="multipart/form-data" action="/upload">
    <input type="file" name="file" required>
    <input type="submit" value="上传">
</form>
✅ 后端 Controller
@PostMapping("/upload")
public ResponseEntity<CommonResult<String>> uploadFile(@RequestParam("file") MultipartFile file) {
    // ✅ 校验文件
    if (file.isEmpty()) {
        return ResponseEntity.badRequest().body(CommonResult.fail("文件不能为空"));
    }

    // ✅ 校验文件类型(仅允许图片)
    String contentType = file.getContentType();
    if (!contentType.startsWith("image/")) {
        return ResponseEntity.badRequest().body(CommonResult.fail("仅支持图片文件"));
    }

    // ✅ 校验文件大小(最大 5MB)
    if (file.getSize() > 5 * 1024 * 1024) {
        return ResponseEntity.badRequest().body(CommonResult.fail("文件大小不能超过 5MB"));
    }

    // ✅ 生成唯一文件名(防止重名)
    String originalFilename = file.getOriginalFilename();
    String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
    String fileName = System.currentTimeMillis() + extension;

    // ✅ 保存到服务器(生产环境建议使用 OSS、MinIO)
    try {
        file.transferTo(new File("/opt/uploads/" + fileName));
    } catch (Exception e) {
        log.error("文件上传失败", e);
        return ResponseEntity.status(500).body(CommonResult.fail("文件上传失败"));
    }

    // ✅ 返回访问路径
    String url = "http://localhost:8080/uploads/" + fileName;
    return ResponseEntity.ok(CommonResult.success(url));
}

7.2 文件下载:Resource + InputStreamResource

@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
    File file = new File("/opt/uploads/" + fileName);
    if (!file.exists()) {
        return ResponseEntity.notFound().build();
    }

    try {
        // ✅ 创建资源对象
        Resource resource = new InputStreamResource(new FileInputStream(file));

        // ✅ 设置响应头
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .contentLength(file.length())
            .body(resource);

    } catch (Exception e) {
        log.error("文件下载失败", e);
        return ResponseEntity.status(500).build();
    }
}

生产建议

  • 文件上传路径使用 独立存储(如 MinIO、阿里云 OSS)
  • 下载使用 Nginx 静态代理,避免 Java 进程处理大文件
  • 文件名做 URL 编码 防止中文乱码

✅ 八、拦截器(Interceptor):登录校验、日志、性能监控

8.1 实现 HandlerInterceptor

package com.example.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.UUID;

/**
 * ✅ 登录校验拦截器 + 性能监控 + 请求日志
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);

    /**
     * ✅ 在 Controller 方法执行前调用
     * 返回 true:继续执行
     * 返回 false:中断请求,不执行 Controller
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        String requestId = UUID.randomUUID().toString();
        request.setAttribute("requestId", requestId); // 传递给后续组件

        // ✅ 1. 记录请求日志
        String uri = request.getRequestURI();
        String method = request.getMethod();
        String ip = getClientIpAddress(request);
        log.info("[REQUEST] {} {} | IP: {} | ID: {}", method, uri, ip, requestId);

        // ✅ 2. 登录校验:检查 Token(简化示例)
        String token = request.getHeader("Authorization");
        if (token == null || !token.equals("admin-token")) {
            response.setStatus(401);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write("{\"code\":401,\"msg\":\"未登录或Token无效\"}");
            return false; // ✅ 中断请求
        }

        // ✅ 3. 记录开始时间
        request.setAttribute("startTime", startTime);
        return true; // ✅ 放行
    }

    /**
     * ✅ 在 Controller 方法执行后、视图渲染前调用
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, org.springframework.web.servlet.ModelAndView modelAndView) throws Exception {
        // 可修改 Model 或 View
    }

    /**
     * ✅ 在请求处理完成后调用(无论是否异常)
     * 用于记录耗时、清理资源
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        Long startTime = (Long) request.getAttribute("startTime");
        String requestId = (String) request.getAttribute("requestId");

        if (startTime != null) {
            long duration = System.currentTimeMillis() - startTime;
            log.info("[RESPONSE] 耗时: {}ms | ID: {}", duration, requestId);
        }

        if (ex != null) {
            log.error("[ERROR] 请求处理异常 | ID: {}", requestId, ex);
        }
    }

    // 获取真实客户端 IP
    private String getClientIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            int index = ip.indexOf(",");
            if (index != -1) {
                return ip.substring(0, index);
            }
            return ip;
        }
        ip = request.getHeader("X-Real-IP");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            return ip;
        }
        return request.getRemoteAddr();
    }
}

8.2 注册拦截器

package com.example.config;

import com.example.interceptor.AuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 配置拦截器
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // ✅ 注册拦截器
        registry.addInterceptor(authInterceptor)
            .addPathPatterns("/api/**") // ✅ 拦截 /api/ 开头的所有请求
            .excludePathPatterns("/api/login", "/api/register"); // ✅ 排除登录、注册接口
    }
}

应用场景

  • 登录校验:拦截所有需要权限的接口
  • 请求日志:记录访问路径、IP、耗时
  • 性能监控:统计接口响应时间
  • 请求追踪:通过 requestId 链接日志、链路追踪

✅ 九、综合性实战示例:用户管理系统(完整整合)

🎯 功能清单

功能使用技术
用户注册@PostMapping + @RequestBody + @Valid
用户登录@PostMapping("/login")(无需校验)
查询用户列表@GetMapping("/users") + 分页参数
修改用户@PutMapping("/users/{id}") + @PathVariable + @Valid
删除用户@DeleteMapping("/users/{id}")
文件上传@RequestParam("avatar") MultipartFile
文件下载ResponseEntity<Resource>
登录校验HandlerInterceptor
全局异常@RestControllerAdvice
统一响应CommonResult<T>
日志记录@Slf4j + 拦截器

✅ DTO 类

// UserCreateRequest.java(同上)
// UserUpdateRequest.java
@Data
public class UserUpdateRequest {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20)
    private String username;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Min(value = 18, message = "年龄不能小于 18 岁")
    private Integer age;
}

✅ Controller

package com.example.controller;

import com.example.dto.CommonResult;
import com.example.dto.UserCreateRequest;
import com.example.dto.UserUpdateRequest;
import com.example.exception.BusinessException;
import com.example.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public ResponseEntity<CommonResult<String>> register(@Valid @RequestBody UserCreateRequest request) {
        userService.register(request);
        return ResponseEntity.ok(CommonResult.success("注册成功"));
    }

    @PostMapping("/login")
    public ResponseEntity<CommonResult<String>> login(@RequestParam String username, @RequestParam String password) {
        if (!"admin".equals(username) || !"123456".equals(password)) {
            throw new BusinessException(1001, "用户名或密码错误");
        }
        return ResponseEntity.ok(CommonResult.success("登录成功"));
    }

    @GetMapping
    public ResponseEntity<CommonResult<List<String>>> list() {
        List<String> users = userService.list();
        return ResponseEntity.ok(CommonResult.success(users));
    }

    @PutMapping("/{id}")
    public ResponseEntity<CommonResult<String>> update(@PathVariable Long id, @Valid @RequestBody UserUpdateRequest request) {
        userService.update(id, request);
        return ResponseEntity.ok(CommonResult.success("更新成功"));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<CommonResult<String>> delete(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.ok(CommonResult.success("删除成功"));
    }

    @PostMapping("/upload")
    public ResponseEntity<CommonResult<String>> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
        // 省略校验逻辑(见上文)
        String fileName = "avatar_" + System.currentTimeMillis() + ".jpg";
        try {
            file.transferTo(new File("/opt/uploads/" + fileName));
        } catch (IOException e) {
            throw new RuntimeException("上传失败", e);
        }
        return ResponseEntity.ok(CommonResult.success("http://localhost:8080/uploads/" + fileName));
    }

    @GetMapping("/download/{fileName}")
    public ResponseEntity<Resource> download(@PathVariable String fileName) {
        File file = new File("/opt/uploads/" + fileName);
        if (!file.exists()) {
            return ResponseEntity.notFound().build();
        }
        InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
    }
}

测试建议

  • 用 Postman 测试 /api/users/register(传 JSON)
  • 测试 /api/users/upload(Form-data 上传文件)
  • 测试 /api/users/download/xxx.jpg
  • 测试 /api/users/update/1(传非法邮箱,看是否返回校验错误)
  • 测试 /api/users/login(不传 token,看是否拦截)

✅ 十、总结:Spring MVC 的设计哲学

层级设计思想
请求映射@GetMapping 等注解让 HTTP 方法与 Java 方法一一对应
参数绑定不再手动 request.getParameter(),Spring 自动封装
数据校验声明式校验,业务逻辑与校验逻辑分离
异常处理全局统一,避免“到处 try-catch”
文件处理封装 MultipartFile,屏蔽底层 IO
拦截器AOP 思想在 Web 层的完美体现,解耦横切关注点
响应格式统一 CommonResult,前后端契约清晰

Spring MVC 的终极目标
让开发者只关心业务逻辑,其余交给框架。


✅ 十一、学习建议与下一步

建议说明
动手实践用上述完整示例搭建一个项目,跑通所有功能
阅读源码跟踪 DispatcherServlet.doDispatch() 方法,理解流程
学习 Thymeleaf掌握服务端渲染(虽然现在 API 为主)
学习 Spring Boot Web99% 项目都用它,自动配置、内嵌 Tomcat
学习 RESTful 设计语义化 URL、HTTP 状态码、HATEOAS
学习 Swagger/OpenAPI自动生成接口文档

✅ 附:推荐学习资源

资源说明
《Spring 实战(第6版)》第 8–10 章最权威的 Spring MVC 教程
Spring 官方文档https://docs.spring.io/spring-framework/reference/web/webmvc.html权威、准确、完整
GitHub 项目spring-petclinic官方示例,学习最佳实践
Postman / Apifox测试 API 的必备工具

你已掌握 Spring MVC 的全部核心能力
你现在不仅能写接口,还能设计安全、健壮、可维护、可审计的 Web 系统。
恭喜你,成为真正的 Java Web 架构师! 🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值