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微服务架构简介,为进入分布式开发打下基础!


📚 参考资料

相关推荐
苏三说技术11 分钟前
推荐一个牛逼的RAG+KAG双引擎AI项目
后端
格子软件15 分钟前
2026年GEO优化系统源码级状态机与多模型调度拆解
java·前端·vue.js·人工智能·vue·geo
从此以后自律1 小时前
Spring 全家桶
java·后端·spring
HUMHSX1 小时前
Vue 项目启动全流程解析:从入口文件到全局指令注册与页面渲染
前端·javascript·vue.js
有颜有货1 小时前
PMC生产排产的4种算法,一次讲清
java·服务器·前端
小虎牙0071 小时前
Android kotlin图片库Coil源码详解
android·前端
随风一样自由1 小时前
【前端领域】前端开发核心应用场景与落地实践
前端·前端框架
an317422 小时前
弹窗数据流设计的两种高阶架构实践
前端·vue.js·架构
utmhikari2 小时前
【日常随笔】深入回答纯Vibe Coding写后端项目的几个问题
后端·ai编程·vibecoding
尚早立志2 小时前
Spring Boot 源码研读之ConfigurableEnvironment 环境准备
java·spring boot·后端