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

Spring、MyBatis、Spring Boot源码深度剖析:从核心原理到实战调试 在Java企业级开发中,理解框架底层原理是提升技术深度的关键。控制反转(IOC)和面向切面编程(AOP)等设计思想,通过Spring框架的IOC容器和动态代理机制实现,解决了对象依赖管理和横切关注点问题。其技术价值在于提升代码的模块化、可测试性和可维护性。MyBatis作为持久层框架,通过SQL映射和会话管理,简化了数据库操作。这些技术广泛应用于高并发、分布式系统等场景。本文聚焦Spring全家桶与MyBatis的源码核心,通过剖析**循环依赖**的解决机制与**动态代理**的实现原理,揭示框架内部工作机制 阅读详情

以下是为您精心撰写的 《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)绑定到方法参数或对象
✅ 数据格式化与转换自动将字符串转为 Date、Integer、枚举等类型
✅ 视图解析根据返回值自动选择视图(如跳转 JSP 或返回 JSON)
✅ 数据校验集成 JSR-303/JSR-380,实现声明式参数校验
✅ 全局异常处理统一捕获并格式化异常,避免暴露敏感信息
✅ 文件上传下载提供 MultipartFile 和 Resource 等便捷 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 统一入口,多个 HandlerMapping 和 HandlerAdapter 分别处理不同类型的请求,实现高度解耦。


✅ 三、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 之间执行(preHandle、postHandle、afterCompletion)。

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

注解类型作用示例说明
@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/1001 → id=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 架构师! 🚀

SpringMVC拦截器与过滤器核心区别、执行顺序与实战选型指南 在Java Web开发中,拦截器(Interceptor)和过滤器(Filter)是处理HTTP请求的两种核心机制,它们共同构成了请求处理链。过滤器基于Servlet规范,作用于所有进入容器的请求,如同全局门卫,负责字符编码、安全过滤等通用预处理。拦截器则是Spring MVC框架的一部分,工作在DispatcherServlet内部,能够访问Spring上下文和处理器对象,适用于权限校验、业务日志等与业务逻辑紧密相关的场景。理解二者的执行顺序至关重要:请求先经过过滤器链,再进入拦截器的preHandle、 阅读详情

相关推荐

WebSocket协议深度解析与Spring Boot实战:从原理到高可用实时通信系统构建

在构建实时Web应用时,传统的HTTP请求-响应模式因其单向、无状态的特性,难以满足聊天、推送、在线协作等场景对低延迟、双向通信的需求。为解决这一核心矛盾,WebSocket协议应运而生,它在TCP连接之上建立了全双工通信通道,允许服务器与客户端在握手后随时主动推送数据,从而大幅降低了延迟与开销。其技术价值在于彻底改变了Web的实时交互范式,成为在线游戏、金融行情、协同编辑等高性能应用的基础设施。理解其握手过程中的Sec-WebSocket-Key验证机制与数据帧格式,是掌握协议本质、进行高效调试与性能优化

weixin_30443075的博客 335

Spring WebMVC 5.1.5 RELEASE 深度解析与包结构详解

MVC 三层分离:Controller(业务逻辑)、View(页面渲染)、Model(数据传递)。:一切请求的总入口,负责分发和协调。:映射请求与处理器,适配调用方式。:前后处理,类似 AOP。:视图定位与页面渲染。:国际化与主题切换。资源管理:ResourceHttpRequestHandler 负责静态资源分发。速记口诀“调度分发入口明,映射适配链路清;拦截前后控逻辑,视图渲染见分明;国际主题随切换,资源管理有专精。

IT技术学习与工作笔记分享 1596

Java开发者如何构建技术护城河:从Spring、MySQL、Redis深度到AI工程化

在软件工程领域,后端开发的核心竞争力在于构建稳固、可扩展的系统能力。其原理根植于对基础架构组件的深刻理解与工程化实践。掌握Spring框架的IoC容器、AOP机制及自动配置原理,是构建企业级应用的基础;深入MySQL的InnoDB存储引擎、索引优化与事务隔离级别,能保障数据层的性能与一致性;而精通Redis的数据结构、高可用方案及缓存问题应对策略,则是支撑高并发场景的关键技术价值。这些能力共同服务于电商、金融、社交等需要处理复杂业务与高流量的应用场景。随着技术演进,AI工程化与云原生趋势正重塑后端开发边界,

weixin_30978239的博客 206

SpringBoot 3.x + JWT 登录校验实战:拦截器 vs 过滤器,3种方案性能对比

本文深入对比了SpringBoot 3.x项目中JWT登录校验的三种实现方案:过滤器、拦截器和Spring Security,通过性能测试数据展示各方案的优缺点。过滤器方案性能最优,适合高性能场景;拦截器适合需要Spring集成的应用;Spring Security提供完整安全体系但性能开销较大。文章还提供了技术选型决策树和优化建议,帮助开发者根据实际需求选择最佳方案。

weixin_30407613的博客 450

Spring6源码阅读方法论与实践指南

Spring框架作为Java生态中最重要的IoC容器和AOP实现,其核心原理依赖控制反转与依赖注入机制。通过分层解析Bean生命周期管理、动态代理生成等关键技术,开发者可以深入理解企业级应用架构设计。本文以Spring6最新版本为例,结合IDEA调试工具与模块化阅读策略,详细演示如何从接口设计、实现原理到扩展机制逐步掌握框架内核。特别针对循环依赖处理、事务管理等高频面试考点,提供可视化分析方法和典型调试场景设计,帮助开发者高效构建Spring源码知识体系。

weixin_34050519的博客 538

Java SQL注入漏洞深度解析:从原理到实战审计与修复

SQL注入是一种将用户输入数据误当作代码执行的Web安全漏洞,其核心原理在于程序未对用户输入与SQL指令进行有效分离,导致恶意输入能够改变原始查询的语义。在Java开发中,尽管普遍采用了参数化查询等防护手段,但在动态SQL拼接、框架特性误用等场景下,风险依然存在。理解其技术原理,对于构建安全的应用架构至关重要。通过系统性的代码审计,可以主动发现并修复此类漏洞,提升应用自身的安全水位。本文聚焦于Java生态,深入剖析SQL注入的产生机制,并结合MyBatis、JPA等主流框架,详解在参数化查询、动态排序、IN

weixin_34150830的博客 431

SpringBoot Web开发原理:深入理解Spring MVC定制化配置

这个类使用条件注解来控制配置生效时机,比如@ConditionalOnWebApplication确保只在Web环境中生效,@ConditionalOnClass检查关键类是否存在。Spring MVC作为SpringBoot的核心Web框架,其自动配置机制和定制化能力是开发者必须掌握的重要内容。可以快速验证这些配置效果,平台提供了一键部署能力,无需繁琐的环境配置就能看到实际运行效果。自定义配置类:通过@Bean替换特定组件,如消息转换器,适合对单个组件有特殊需求的场景。

StarfallOwl89的博客 297

Java数据脱敏实战:基于注解与Jackson序列化的优雅实现方案

数据脱敏是数据安全领域的一项基础技术,其核心原理是在确保数据可用性的前提下,对敏感信息进行变形或遮蔽处理,以保护个人隐私和商业机密。这项技术的核心价值在于平衡数据使用与安全合规,广泛应用于日志输出、接口返回、数据共享与测试环境构建等场景。在Java开发实践中,通过策略模式封装脱敏算法,并结合自定义注解与Jackson序列化器,可以构建出低侵入、高可维护的脱敏框架。本文聚焦于如何利用Spring AOP与自定义JsonSerializer,实现声明式的字段级脱敏,有效解决硬编码与代码重复问题,为处理用户手机号

weixin_30315435的博客 791

Spring Boot与微服务架构面试核心要点解析

微服务架构作为现代分布式系统的主流设计范式,通过服务解耦和独立部署实现系统弹性扩展。其核心原理基于领域驱动设计(DDD)划分业务边界,配合服务发现、配置中心等基础设施组件构建完整技术体系。Spring Boot作为微服务开发的黄金搭档,凭借自动配置机制和Starter组件极大提升了开发效率,其条件化Bean注册和配置绑定机制值得深入理解。在电商、金融等高并发场景中,这种技术组合能有效支撑从单体到分布式的平滑演进,解决分布式事务、服务熔断等典型挑战。通过剖析Spring Boot自动配置源码和微服务拆分原则,

weixin_30906185的博客 327

SpringBoot面试进阶:从原理到实战,掌握自动装配与性能调优

SpringBoot作为Java后端开发的主流框架,其核心在于通过自动装配机制简化配置,提升开发效率。自动装配基于条件注解和spring.factories文件,实现按需加载Bean,这是理解SpringBoot原理的关键。掌握这一机制不仅能优化日常开发,还能在解决复杂工程问题时发挥重要作用,例如在排查线上OOM问题或设计高并发延迟队列时,深入理解内存管理和中间件整合至关重要。在实际应用中,结合Redis缓存和消息队列,可以有效提升系统性能与可靠性。本文聚焦SpringBoot面试中的深度考点,帮助开发者从

weixin_30530339的博客 579

Spring MVC核心源码深度解析与架构实战

Spring MVC基于经典的MVC(Model-View-Controller)架构模式,通过职责分离实现Web层的高内聚低耦合。其中,Model承载业务数据,View负责渲染展示,Controller处理请求调度与流程控制。这种分层结构使得各组件可独立演化,提升代码可维护性。// 数据与视图解耦上述代码体现了Spring MVC对MVC模式的编程抽象:控制器返回对象,将模型数据与逻辑视图名分离,交由后续视图解析器动态决定渲染方式。是。

weixin_28681379的博客 315

海康威视iVMS-8700安全审计实战:从Spring MVC架构到业务逻辑漏洞挖掘

在Web应用安全领域,Spring MVC作为经典的Java EE框架,其安全机制与漏洞模式是安全工程师必须掌握的核心知识。Spring MVC通过控制器映射、拦截器与数据绑定机制处理用户请求,若配置不当或验证缺失,极易引发SQL注入、命令执行等高风险漏洞。理解其工作原理,有助于从架构层面识别认证绕过、未授权访问等安全隐患,这对于企业级应用的安全防护具有重要价值。在安防行业,海康威视iVMS-8700这类综合管理平台广泛部署于智慧城市、大型园区等关键场景,其安全性直接关系到整体安防体系的稳固。本次实战聚焦于

weixin_34062469的博客 474

从GAN生成失败案例出发:为什么你的AI画不准位置?CoordConv保姆级解决方案

本文深入探讨了GAN生成图像时位置偏移问题的根源,并提供了CoordConv的保姆级解决方案。通过分析传统CNN在空间坐标建模上的缺陷,详细介绍了CoordConv的原理、实现步骤及在StyleGAN2等模型中的实战应用,显著提升了医学图像生成等任务的定位精度。

pp12345的博客 204

Java面试全流程解析:从基础到微服务架构

Java作为企业级开发的主流语言,其技术栈涵盖从基础语法到分布式系统的完整体系。理解JVM内存模型、多线程并发等核心原理是构建可靠系统的基石,而Spring生态则提供了高效的开发框架。在微服务架构中,服务发现机制和分布式事务处理成为关键技术挑战,Eureka、Nacos等服务注册中心的选择需要权衡CAP理论。实际开发中,Hibernate的延迟加载优化和Docker容器化部署都是提升系统性能的重要手段。本文通过典型面试问题解析,展示Java开发者如何系统准备大厂技术面试,涵盖Spring MVC设计优势、微

chuange6363的博客 334

TOD和PPS精确时间同步技术

授时

u010783226的专栏 1万+

实战拆解:RAG vs Agent在金融风控中的性能对比

2024年上半年我帮某华东地区城商行做零售交易反欺诈系统升级时,技术团队曾发生过一次激烈争论:一派认为应该优先上RAG(检索增强生成)方案,成本低、落地快、合规性好;另一派坚持要上Agent(智能体)方案,推理能力强、能应对复杂新型欺诈。最终我们做了完整的AB测试,结论是两者没有绝对的优劣,只有场景适配的差异:RAG适合高并发、低时延的实时风控场景,Agent适合低并发、高准确率要求的事后核查/大额审批场景,两者结合的 hybrid 方案能覆盖95%以上的风控需求。本文我会结合真实项目的测试数据,从核心概念

AI天才研究院 251

Java实习面试全流程解析与备战策略

Java作为企业级开发的核心语言,其技术栈深度与广度直接影响开发效率与系统稳定性。从JVM内存模型到并发编程机制,理解底层原理是优化性能的关键基础。以HashMap为例,其扰动函数与红黑树转换机制体现了数据结构与算法在工程实践中的巧妙应用。Spring框架通过IoC容器和AOP实现了松耦合架构,其中动态代理技术的选择直接影响系统性能。在分布式场景下,Redis缓存与MySQL索引优化成为解决高并发问题的标配方案。本文基于大厂Java实习面试的高频考点,系统梳理了从JVM调优到Spring原理的技术攻关路径,

weixin_30298497的博客 299

骗分导论 CSP、NOIP、蓝桥杯、PAT、USACO等竞赛的算法骗分技巧 轻松省三

问题来了,那么多座桥,炸一座就破坏岛屿的联系,可能性微乎其微(除非特别设计数据)。它们的价值极大,不仅能初步帮你检验程序的对错,而且,如果你不会做这道题,你就可以直接输出样例!在OI考试中最后难的题数据最后的几个,他们往往不会创造新的数据,此时就可以直接输出最后几个数据了。需要运用《骗分导论》第3章中学到的知识,先写出朴素算法,然后造一些数据,可能就会发现规律。有些时候,问题的答案可能很有特点:对于大多数情况,答案是一样的。据说,在NOIP2013中,有人最后一题不会,愤然打了个随机数,结果得了。

星卯教育-信奥教练tony 6403

Spring Boot 3.X参数绑定失效:从编译配置到依赖变更的完整解决方案

在Web开发中,参数绑定是Spring MVC框架处理HTTP请求的核心机制,它负责将请求中的查询参数、表单数据或JSON内容映射到控制器方法的入参上。其原理基于Java反射和内省机制,结合一系列HandlerMethodArgumentResolver实现数据转换与注入。这一机制的技术价值在于简化了Web层开发,提升了代码的简洁性和可维护性。然而,在框架升级或配置不当的场景下,参数绑定可能失效,导致业务逻辑异常。本文聚焦于Spring Boot 3.X升级后常见的参数解析问题,深入剖析因编译参数缺失、依赖

weixin_30527323的博客 504

第十六届蓝桥杯大赛软件赛(编程类)知识点大纲

难度:[1-3]难度:[1-5]难度:[1-3]难度:[2-5]难度:[1-5]

Future_yzx的博客 4112

SpringBoot中使用FastJson解析Json数据

场景 1.SpringBoot默认配置的是Jackson。 2.项目搭建专栏: https://blog.youkuaiyun.com/BADAO_LIUMANG_QIZHI/column/info/35688 实现 引入fastJson的依赖 <!-- fastjson的依赖 --> <dependency> <groupId>com.alibab...

BADAO_LIUMANG_QIZHI的博客 4140

Java Web安全实战:XSS与CSRF攻击原理与Spring Boot防御指南

Web安全是构建可靠在线服务的基石,其核心在于对用户输入的处理与请求来源的验证。XSS(跨站脚本攻击)与CSRF(跨站请求伪造)是两种最常见且危害巨大的Web安全漏洞,理解其原理是有效防御的前提。XSS攻击通过注入恶意脚本,在用户浏览器中执行,窃取数据或劫持会话;CSRF则利用用户已登录的身份,伪造请求执行非授权操作。防御XSS需遵循“输入过滤、输出转义”原则,结合内容安全策略(CSP)等HTTP安全头;防御CSRF则依赖同步令牌等机制验证请求合法性。在Java Web开发中,尤其是基于Spring Boo

weixin_30562507的博客 348

【EMC专题】为什么芯片规格书ESD性能中看不到MM(机器放电模型)

器件级ESD包括三种类型,HBM人体放电模型、MM机械放电模型和CDM元件充电模型。但在芯片规格书中为什么只见到HBM模型与CDM模型,却没有看到MM模型的数据?

一点硬件 450

Spring Boot集成Shiro时SecurityManager未绑定问题的深度解析与解决方案

在Java应用安全领域,Apache Shiro作为轻量级安全框架,其核心SecurityManager负责协调认证、授权等关键操作。其工作原理基于ThreadContext机制,通过线程绑定的方式实现安全上下文传递。这一设计在Spring Boot集成中尤为重要,但常因Bean初始化顺序、异步线程或静态代码块等问题,导致UnavailableSecurityManagerException异常,表现为“找不到SecurityManager”。这不仅影响应用安全功能,在微服务架构和异步编程场景下尤为突出。理

weixin_30800807的博客 294

Unity 编辑器篇|(四)编辑器拓展GUI类 (全面总结 | 建议收藏)

GUI 类是 Unity GUI 的接口,并且具有手动定位功能。“即时模式”GUI 系统(也称为 IMGUI)是一个完全独立的功能系统,不同于 Unity 基于游戏对象的主 UI 系统。IMGUI 是一个代码驱动的 GUI 系统,主要用作程序员的工具。为了驱动该系统,需在实现脚本上调用 OnGUI 函数即时模式 GUI 系统常用于:创建游戏内调试显示和工具。为脚本组件创建自定义检视面板。创建新的编辑器窗口和工具以扩展 Unity 本身。

游戏开发小Y的博客 12万+

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Java Web任意文件下载漏洞深度剖析:从CVE-2024-27765看路径遍历攻击与防御

在Web应用安全领域,文件操作功能是常见的安全风险点,其中任意文件下载漏洞(Arbitrary File Download)尤为典型。其核心原理在于应用程序未对用户可控的文件路径参数进行严格校验,攻击者通过构造包含目录遍历序列(如../)的路径,即可绕过预期目录限制,读取服务器上的敏感文件。这类漏洞的技术价值在于揭示了输入验证与信任边界的重要性,一旦被利用,可导致配置文件、源码甚至系统关键文件泄露,为后续攻击铺平道路。在Java Web开发中,涉及new File()、FileInputStream等危险函

weixin_30572613的博客 549

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

上一篇: Spring AOP 参数绑定机制系统性分析与最佳实践
下一篇: Spring MVC 核心架构与请求生命周期深度解析
龙茶清欢
博客等级 码龄7年 3720粉丝 375原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

龙茶清欢

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

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

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

打赏作者

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

抵扣说明:

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

余额充值