39 Spring Boot Web实战

目录

  • [🌐 39 Spring Boot Web实战------RESTful、参数校验、统一响应与Swagger文档](#🌐 39 Spring Boot Web实战——RESTful、参数校验、统一响应与Swagger文档)
    • [一、RESTful API设计规范](#一、RESTful API设计规范)
      • [1.1 什么是RESTful](#1.1 什么是RESTful)
      • [1.2 RESTful核心原则](#1.2 RESTful核心原则)
      • [1.3 HTTP方法与资源映射](#1.3 HTTP方法与资源映射)
      • [1.4 URL设计规范](#1.4 URL设计规范)
    • [二、Spring Boot构建RESTful接口](#二、Spring Boot构建RESTful接口)
      • [2.1 项目依赖](#2.1 项目依赖)
      • [2.2 实体类定义](#2.2 实体类定义)
      • [2.3 Controller完整实现](#2.3 Controller完整实现)
      • [2.4 常用注解速查](#2.4 常用注解速查)
    • 三、参数校验实战
      • [3.1 为什么需要参数校验](#3.1 为什么需要参数校验)
      • [3.2 创建请求DTO](#3.2 创建请求DTO)
      • [3.3 常用校验注解](#3.3 常用校验注解)
      • [3.4 嵌套对象校验](#3.4 嵌套对象校验)
      • [3.5 分组校验](#3.5 分组校验)
    • 四、统一响应封装
      • [4.1 为什么需要统一响应](#4.1 为什么需要统一响应)
      • [4.2 统一响应类](#4.2 统一响应类)
      • [4.3 分页结果封装](#4.3 分页结果封装)
      • [4.4 使用示例](#4.4 使用示例)
    • 五、全局异常处理
      • [5.1 自定义业务异常](#5.1 自定义业务异常)
      • [5.2 全局异常处理器](#5.2 全局异常处理器)
      • [5.3 异常处理流程](#5.3 异常处理流程)
    • 六、拦截器与过滤器
      • [6.1 过滤器 vs 拦截器](#6.1 过滤器 vs 拦截器)
      • [6.2 拦截器实现:请求日志](#6.2 拦截器实现:请求日志)
      • [6.3 拦截器实现:Token鉴权](#6.3 拦截器实现:Token鉴权)
      • [6.4 注册拦截器](#6.4 注册拦截器)
      • [6.5 拦截器执行顺序](#6.5 拦截器执行顺序)
    • 七、Swagger/Knife4j接口文档
      • [7.1 添加依赖](#7.1 添加依赖)
      • [7.2 配置文件](#7.2 配置文件)
      • [7.3 为Controller添加文档注解](#7.3 为Controller添加文档注解)
      • [7.4 Swagger常用注解](#7.4 Swagger常用注解)
      • [7.5 DTO文档化](#7.5 DTO文档化)
      • [7.6 访问文档](#7.6 访问文档)
    • 八、完整项目实战
      • [8.1 项目结构](#8.1 项目结构)
      • [8.2 Service层完整实现](#8.2 Service层完整实现)
      • [8.3 接口测试示例](#8.3 接口测试示例)
    • 九、常见面试题解析
    • 十、总结与下篇预告
      • 本文核心要点
      • [🎯 动手实践](#🎯 动手实践)
      • [📖 下篇预告](#📖 下篇预告)

🌐 39 Spring Boot Web实战------RESTful、参数校验、统一响应与Swagger文档

更新日期 :2026年6月 | Java入门到精通系列 · 第五阶段·企业级开发

© 版权声明:本文为原创技术文章,转载请联系作者并注明出处。



一、RESTful API设计规范

1.1 什么是RESTful

REST(Representational State Transfer)是一种架构风格 ,它将后端服务看作一组资源,通过HTTP方法对资源进行操作。

1.2 RESTful核心原则

原则 说明 示例
资源标识 每个资源有唯一URI /api/users/42
统一接口 使用HTTP方法表示操作 GET/POST/PUT/DELETE
无状态 每次请求包含所有信息 Token放在Header
分层系统 客户端无需关心服务端架构 网关、负载均衡透明
统一响应 规范化的返回格式 {code, message, data}

1.3 HTTP方法与资源映射

复制代码
┌────────────┬──────────────────────┬────────────────────────┐
│  HTTP方法  │       URL路径        │        含义            │
├────────────┼──────────────────────┼────────────────────────┤
│  GET       │ /api/users           │ 查询所有用户(列表)    │
│  GET       │ /api/users/{id}      │ 查询指定用户           │
│  POST      │ /api/users           │ 创建新用户             │
│  PUT       │ /api/users/{id}      │ 全量更新用户           │
│  PATCH     │ /api/users/{id}      │ 部分更新用户           │
│  DELETE    │ /api/users/{id}      │ 删除指定用户           │
└────────────┴──────────────────────┴────────────────────────┘

1.4 URL设计规范

bash 复制代码
# ✅ 正确示例
GET    /api/v1/users              # 获取用户列表
GET    /api/v1/users/123          # 获取ID为123的用户
POST   /api/v1/users              # 创建用户
GET    /api/v1/users/123/orders   # 获取用户123的订单

# ❌ 错误示例
GET    /api/getUsers              # 不要用动词
POST   /api/deleteUser/123        # 删除应该用DELETE方法
GET    /api/user_list             # 用复数名词,不用下划线

二、Spring Boot构建RESTful接口

2.1 项目依赖

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2.2 实体类定义

java 复制代码
package com.example.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String username;
    private String email;
    private Integer age;
    private String phone;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

2.3 Controller完整实现

java 复制代码
package com.example.controller;

import com.example.entity.User;
import com.example.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

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

    private final UserService userService;

    /**
     * 查询用户列表
     * GET /api/v1/users
     */
    @GetMapping
    public List<User> listUsers() {
        return userService.findAll();
    }

    /**
     * 根据ID查询用户
     * GET /api/v1/users/{id}
     */
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.findById(id);
    }

    /**
     * 创建用户
     * POST /api/v1/users
     */
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User createUser(@Valid @RequestBody UserCreateRequest request) {
        return userService.create(request);
    }

    /**
     * 更新用户
     * PUT /api/v1/users/{id}
     */
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id,
                           @Valid @RequestBody UserUpdateRequest request) {
        return userService.update(id, request);
    }

    /**
     * 删除用户
     * DELETE /api/v1/users/{id}
     */
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }

    /**
     * 条件查询
     * GET /api/v1/users/search?keyword=zhang&page=1&size=10
     */
    @GetMapping("/search")
    public List<User> searchUsers(
            @RequestParam(required = false) String keyword,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        return userService.search(keyword, page, size);
    }
}

2.4 常用注解速查

注解 作用 位置
@RestController 组合了@Controller+@ResponseBody 类上
@RequestMapping 映射请求路径 类/方法上
@GetMapping 映射GET请求 方法上
@PostMapping 映射POST请求 方法上
@PutMapping 映射PUT请求 方法上
@DeleteMapping 映射DELETE请求 方法上
@PathVariable 获取路径中的参数 参数上
@RequestParam 获取查询参数 参数上
@RequestBody 获取请求体JSON 参数上
@ResponseStatus 设置响应状态码 方法上

三、参数校验实战

3.1 为什么需要参数校验

永远不要信任客户端传来的数据! 参数校验是保障系统安全和数据完整性的第一道防线。

3.2 创建请求DTO

java 复制代码
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;

    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄不能小于1")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;

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

3.3 常用校验注解

注解 作用 示例
@NotNull 不能为null @NotNull Integer age
@NotBlank 不能为null/空/空白 @NotBlank String name
@NotEmpty 不能为null/空集合 @NotEmpty List<String> tags
@Size 长度/大小限制 @Size(min=2, max=20)
@Min / @Max 数值范围 @Min(0) @Max(150)
@Email 邮箱格式 @Email
@Pattern 正则匹配 @Pattern(regexp="^1\\d{10}$")
@Past / @Future 日期范围 @Past LocalDate birthday
@Positive 正数 @Positive Double price

3.4 嵌套对象校验

java 复制代码
@Data
public class OrderCreateRequest {

    @NotNull(message = "用户ID不能为空")
    private Long userId;

    @Valid                    // ← 加上@Valid才能触发嵌套校验
    @NotNull(message = "收货地址不能为空")
    private AddressDTO address;

    @NotEmpty(message = "订单商品不能为空")
    @Size(max = 50, message = "单次最多下单50件商品")
    private List<@Valid OrderItemDTO> items;
}

@Data
public class AddressDTO {
    @NotBlank(message = "省不能为空")
    private String province;

    @NotBlank(message = "市不能为空")
    private String city;

    @NotBlank(message = "详细地址不能为空")
    private String detail;
}

3.5 分组校验

java 复制代码
// 定义校验分组
public interface ValidationGroups {
    interface Create {}
    interface Update {}
}

@Data
public class UserRequest {
    @Null(groups = ValidationGroups.Create.class, message = "创建时ID必须为空")
    @NotNull(groups = ValidationGroups.Update.class, message = "更新时ID不能为空")
    private Long id;

    @NotBlank(message = "用户名不能为空")
    private String username;
}

// Controller中指定分组
@PostMapping
public User createUser(@Validated(ValidationGroups.Create.class)
                        @RequestBody UserRequest request) { ... }

@PutMapping("/{id}")
public User updateUser(@PathVariable Long id,
                       @Validated(ValidationGroups.Update.class)
                       @RequestBody UserRequest request) { ... }

四、统一响应封装

4.1 为什么需要统一响应

问题 不统一时 统一后
前端解析 每个接口返回结构不同,需分别处理 统一code/message/data结构
错误处理 有的返回字符串,有的返回对象 统一错误码体系
接口文档 格式混乱 规范、清晰

4.2 统一响应类

java 复制代码
package com.example.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {

    private int code;
    private String message;
    private T data;

    /** 成功 ------ 有数据 */
    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(200, "操作成功", data);
    }

    /** 成功 ------ 自定义消息 */
    public static <T> ApiResponse<T> ok(String message, T data) {
        return new ApiResponse<>(200, message, data);
    }

    /** 成功 ------ 无数据 */
    public static <T> ApiResponse<T> ok() {
        return new ApiResponse<>(200, "操作成功", null);
    }

    /** 失败 */
    public static <T> ApiResponse<T> fail(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }

    /** 失败 ------ 默认错误码 */
    public static <T> ApiResponse<T> fail(String message) {
        return new ApiResponse<>(500, message, null);
    }

    /** 分页响应封装 */
    public static <T> ApiResponse<PageResult<T>> okPage(
            java.util.List<T> list, long total, int page, int size) {
        return ok(new PageResult<>(list, total, page, size));
    }
}

4.3 分页结果封装

java 复制代码
@Data
@AllArgsConstructor
public class PageResult<T> {
    private java.util.List<T> records;
    private long total;
    private int page;
    private int size;
    private long totalPages;

    public PageResult(java.util.List<T> records, long total, int page, int size) {
        this.records = records;
        this.total = total;
        this.page = page;
        this.size = size;
        this.totalPages = (total + size - 1) / size;
    }
}

4.4 使用示例

java 复制代码
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/{id}")
    public ApiResponse<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ApiResponse.ok(user);
    }

    @PostMapping
    public ApiResponse<User> createUser(@Valid @RequestBody UserCreateRequest req) {
        User user = userService.create(req);
        return ApiResponse.ok("创建成功", user);
    }

    @GetMapping
    public ApiResponse<PageResult<User>> listUsers(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        PageResult<User> result = userService.findByPage(page, size);
        return ApiResponse.ok(result);
    }
}

五、全局异常处理

5.1 自定义业务异常

java 复制代码
package com.example.exception;

import lombok.Getter;

@Getter
public class BusinessException extends RuntimeException {

    private final int code;

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

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

    // ---- 常用静态工厂 ----
    public static BusinessException notFound(String resource) {
        return new BusinessException(404, resource + "不存在");
    }

    public static BusinessException duplicate(String field) {
        return new BusinessException(409, field + "已存在");
    }
}

5.2 全局异常处理器

java 复制代码
package com.example.exception;

import com.example.common.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.stream.Collectors;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
        return ApiResponse.fail(e.getCode(), e.getMessage());
    }

    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleValidationException(
            MethodArgumentNotValidException e) {
        String errors = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining("; "));
        log.warn("参数校验失败: {}", errors);
        return ApiResponse.fail(400, errors);
    }

    /**
     * 处理其他未捕获异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return ApiResponse.fail(500, "系统内部错误,请稍后再试");
    }
}

5.3 异常处理流程

复制代码
客户端请求
    │
    ▼
Controller方法
    │
    ├── 正常返回 ──► @RestControllerAdvice (不干预)
    │
    └── 抛出异常
         │
         ├── BusinessException ──► 返回 code + message
         │
         ├── MethodArgumentNotValidException ──► 返回参数错误详情
         │
         └── 其他Exception ──► 记录日志,返回 "系统内部错误"

六、拦截器与过滤器

6.1 过滤器 vs 拦截器

对比项 Filter(过滤器) Interceptor(拦截器)
规范 Servlet规范 Spring MVC框架
作用范围 所有请求(含静态资源) 只拦截Controller请求
可获取信息 只有request/response 可获取handler信息
执行顺序 先于拦截器 后于过滤器
典型用途 编码、CORS、请求体缓存 鉴权、日志、限流

6.2 拦截器实现:请求日志

java 复制代码
package com.example.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@Component
public class RequestLogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        request.setAttribute("startTime", System.currentTimeMillis());
        log.info("→ {} {} from {}",
                request.getMethod(),
                request.getRequestURI(),
                request.getRemoteAddr());
        return true; // 返回false会中断请求
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) {
        long startTime = (long) request.getAttribute("startTime");
        long duration = System.currentTimeMillis() - startTime;
        log.info("← {} {} [{}] {}ms",
                request.getMethod(),
                request.getRequestURI(),
                response.getStatus(),
                duration);
    }
}

6.3 拦截器实现:Token鉴权

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {

    private final JwtTokenUtil jwtTokenUtil;

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        // 放行OPTIONS预检请求
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            return true;
        }

        // 获取Token
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(401);
            return false;
        }

        token = token.substring(7);

        // 验证Token
        if (!jwtTokenUtil.validateToken(token)) {
            response.setStatus(401);
            return false;
        }

        // 将用户信息放入请求属性
        Long userId = jwtTokenUtil.getUserId(token);
        request.setAttribute("currentUserId", userId);
        return true;
    }
}

6.4 注册拦截器

java 复制代码
package com.example.config;

import com.example.interceptor.AuthInterceptor;
import com.example.interceptor.RequestLogInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final RequestLogInterceptor requestLogInterceptor;
    private final AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 请求日志拦截器 ------ 拦截所有请求
        registry.addInterceptor(requestLogInterceptor)
                .addPathPatterns("/api/**");

        // 鉴权拦截器 ------ 排除登录等公开接口
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns(
                        "/api/v1/auth/login",
                        "/api/v1/auth/register",
                        "/api/v1/public/**"
                );
    }
}

6.5 拦截器执行顺序

复制代码
请求 → Filter链 → DispatcherServlet
                      │
                      ▼
                preHandle(拦截器1) → preHandle(拦截器2)
                      │
                      ▼
                Controller方法执行
                      │
                      ▼
                postHandle(拦截器2) → postHandle(拦截器1)
                      │
                      ▼
                afterCompletion(拦截器2) → afterCompletion(拦截器1)
                      │
                      ▼
                Filter链 → 响应

七、Swagger/Knife4j接口文档

7.1 添加依赖

xml 复制代码
<!-- 方式一:使用SpringDoc + Knife4j(推荐,Spring Boot 3.x) -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.5.0</version>
</dependency>

7.2 配置文件

yaml 复制代码
# application.yml
springdoc:
  swagger-ui:
    path: /swagger-ui.html    # Swagger UI路径
    tags-sorter: alpha         # 按名称排序
    operations-sorter: alpha   # 按方法排序
  api-docs:
    path: /v3/api-docs        # OpenAPI JSON路径

knife4j:
  enable: true                 # 启用Knife4j增强
  setting:
    language: zh_cn            # 中文界面

7.3 为Controller添加文档注解

java 复制代码
@Tag(name = "用户管理", description = "用户的CRUD操作")
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @Operation(summary = "查询用户列表", description = "支持分页和关键词搜索")
    @GetMapping
    public ApiResponse<PageResult<User>> listUsers(
            @ParameterObject PageRequest pageRequest) {
        return ApiResponse.ok(userService.findByPage(pageRequest));
    }

    @Operation(summary = "根据ID查询用户")
    @GetMapping("/{id}")
    public ApiResponse<User> getUser(
            @Parameter(description = "用户ID", example = "1", required = true)
            @PathVariable Long id) {
        return ApiResponse.ok(userService.findById(id));
    }

    @Operation(summary = "创建用户")
    @PostMapping
    public ApiResponse<User> createUser(
            @Valid @RequestBody UserCreateRequest request) {
        return ApiResponse.ok("创建成功", userService.create(request));
    }

    @Operation(summary = "删除用户")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "删除成功"),
        @ApiResponse(responseCode = "404", description = "用户不存在")
    })
    @DeleteMapping("/{id}")
    public ApiResponse<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ApiResponse.ok();
    }
}

7.4 Swagger常用注解

注解 作用位置 说明
@Tag Controller类 模块/分组描述
@Operation 方法 接口描述
@Parameter 参数 请求参数描述
@ParameterObject 对象参数 自动解析对象字段
@Schema DTO/Entity 数据模型描述
@ApiResponse 方法 响应状态码描述

7.5 DTO文档化

java 复制代码
@Schema(description = "用户创建请求")
@Data
public class UserCreateRequest {

    @Schema(description = "用户名", example = "zhangsan", requiredMode = RequiredMode.REQUIRED)
    @NotBlank(message = "用户名不能为空")
    private String username;

    @Schema(description = "邮箱", example = "zhangsan@example.com")
    @NotBlank @Email
    private String email;

    @Schema(description = "年龄", example = "25", minimum = "1", maximum = "150")
    @NotNull @Min(1) @Max(150)
    private Integer age;
}

7.6 访问文档

启动应用后,访问以下地址:

地址 说明
http://localhost:8080/doc.html Knife4j增强文档(推荐)
http://localhost:8080/swagger-ui.html 原生Swagger UI
http://localhost:8080/v3/api-docs OpenAPI 3.0 JSON

八、完整项目实战

8.1 项目结构

复制代码
src/main/java/com/example/
├── common/
│   ├── ApiResponse.java          # 统一响应
│   └── PageResult.java           # 分页结果
├── config/
│   └── WebMvcConfig.java         # 拦截器注册
├── controller/
│   └── UserController.java       # 用户控制器
├── dto/
│   ├── UserCreateRequest.java    # 创建请求
│   ├── UserUpdateRequest.java    # 更新请求
│   └── UserSearchRequest.java    # 搜索请求
├── entity/
│   └── User.java                 # 用户实体
├── exception/
│   ├── BusinessException.java     # 业务异常
│   └── GlobalExceptionHandler.java # 全局异常
├── interceptor/
│   ├── AuthInterceptor.java      # 鉴权拦截器
│   └── RequestLogInterceptor.java # 日志拦截器
├── service/
│   ├── UserService.java          # 用户服务接口
│   └── UserServiceImpl.java      # 用户服务实现
└── Application.java              # 启动类

8.2 Service层完整实现

java 复制代码
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    // 模拟数据库(实际项目用MyBatis/JPA)
    private final Map<Long, User> userStore = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(0);

    @Override
    public List<User> findAll() {
        return new ArrayList<>(userStore.values());
    }

    @Override
    public User findById(Long id) {
        User user = userStore.get(id);
        if (user == null) {
            throw BusinessException.notFound("用户");
        }
        return user;
    }

    @Override
    @Transactional
    public User create(UserCreateRequest request) {
        // 检查用户名是否重复
        boolean exists = userStore.values().stream()
                .anyMatch(u -> u.getUsername().equals(request.getUsername()));
        if (exists) {
            throw BusinessException.duplicate("用户名");
        }

        long id = idGenerator.incrementAndGet();
        User user = User.builder()
                .id(id)
                .username(request.getUsername())
                .email(request.getEmail())
                .age(request.getAge())
                .phone(request.getPhone())
                .createTime(LocalDateTime.now())
                .build();

        userStore.put(id, user);
        log.info("用户创建成功: id={}, username={}", id, user.getUsername());
        return user;
    }

    @Override
    @Transactional
    public User update(Long id, UserUpdateRequest request) {
        User existing = findById(id);
        existing.setUsername(request.getUsername());
        existing.setEmail(request.getEmail());
        existing.setAge(request.getAge());
        existing.setUpdateTime(LocalDateTime.now());
        log.info("用户更新成功: id={}", id);
        return existing;
    }

    @Override
    @Transactional
    public void delete(Long id) {
        if (userStore.remove(id) == null) {
            throw BusinessException.notFound("用户");
        }
        log.info("用户删除成功: id={}", id);
    }

    @Override
    public PageResult<User> search(String keyword, int page, int size) {
        List<User> filtered = userStore.values().stream()
                .filter(u -> keyword == null ||
                        u.getUsername().contains(keyword) ||
                        u.getEmail().contains(keyword))
                .collect(Collectors.toList());

        long total = filtered.size();
        int from = (page - 1) * size;
        List<User> pageData = filtered.stream()
                .skip(from)
                .limit(size)
                .collect(Collectors.toList());

        return new PageResult<>(pageData, total, page, size);
    }
}

8.3 接口测试示例

bash 复制代码
# 创建用户
curl -X POST http://localhost:8080/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"username":"zhangsan","email":"zhangsan@example.com","age":25,"phone":"13800138000"}'

# 响应
# {"code":200,"message":"创建成功","data":{"id":1,"username":"zhangsan",...}}

# 查询用户
curl http://localhost:8080/api/v1/users/1

# 更新用户
curl -X PUT http://localhost:8080/api/v1/users/1 \
  -H "Content-Type: application/json" \
  -d '{"username":"zhangsan_new","email":"new@example.com","age":26}'

# 删除用户
curl -X DELETE http://localhost:8080/api/v1/users/1

# 分页搜索
curl "http://localhost:8080/api/v1/users/search?keyword=zhang&page=1&size=10"

九、常见面试题解析

Q1:@RestController和@Controller的区别?

@Controller返回视图名,配合ViewResolver渲染页面;@RestController相当于@Controller + @ResponseBody,直接将返回值序列化为JSON。

Q2:@Valid和@Validated的区别?

@Valid是Jakarta标准注解,支持嵌套校验;@Validated是Spring注解,额外支持分组校验 。嵌套校验用@Valid,分组校验用@Validated

Q3:拦截器和过滤器的执行顺序?

先执行Filter链,再进入Spring MVC的Interceptor链。具体的拦截器执行顺序由注册顺序决定。

Q4:@RestControllerAdvice的作用?

它是@ControllerAdvice + @ResponseBody的组合,用于定义全局异常处理、数据绑定和数据预处理。

Q5:如何实现接口版本控制?

常见方案:

  • URL路径版本:/api/v1/users/api/v2/users
  • 请求头版本:X-API-Version: 1
  • 参数版本:/api/users?version=1

十、总结与下篇预告

本文核心要点

知识点 关键内容
RESTful 资源导向、HTTP方法语义、URL设计规范
参数校验 Jakarta Validation、嵌套校验、分组校验
统一响应 ApiResponse封装、分页结果
全局异常 BusinessException、@RestControllerAdvice
拦截器 HandlerInterceptor、执行顺序、鉴权场景
Swagger SpringDoc + Knife4j、注解体系

🎯 动手实践

  1. 创建一个完整的CRUD项目,包含User和Order两张表
  2. 实现自定义校验注解(如@PhoneNumber
  3. 添加一个限流拦截器(基于Redis计数器)
  4. 为所有接口添加Swagger文档注解

📖 下篇预告

第40篇:Redis与微服务入门------将学习Redis数据类型、Spring Data Redis集成、缓存策略设计以及Spring Cloud微服务架构简介,为进入分布式开发打下基础!


📚 参考资料

相关推荐
西安邮电大学1 小时前
有关数组的经典算法题
java·后端·其他·算法·面试
山东点狮信息科技有限公司1 小时前
点狮HRM-HRM系统安全体系与数据保护方案
后端·安全·spring·spring cloud·微服务·系统安全·资产
纽格立科技1 小时前
DRM 发射端链路图(上)
前端·人工智能·车载系统·信息与通信·传媒
一 乐2 小时前
幼儿园管理系统|基于springboot + vue幼儿园管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·幼儿园管理系统
摇滚侠2 小时前
SpringMVC 入门到实战 SpringMVC 的执行流程 96
java·后端·spring·maven·intellij-idea
云水一下2 小时前
Vue.js从零到精通系列(七):高级特性实战——Teleport、异步组件、自定义指令与TypeScript深度结合
前端·vue.js·typescript
qq4356947012 小时前
Vue05
前端·vue.js
qq_422152572 小时前
PDF 解密工具怎么选?2026 年文档密码移除方案与注意事项
java·前端·pdf
YHHLAI2 小时前
前端工程化调用 AI 多模态生图模型:Qwen Image Demo 实战
前端·人工智能