Spring Boot Web 开发实战:RESTful API 设计、统一异常处理、参数校验与拦截器

一、前言:为什么选择 Spring Boot 做 Web 开发?

如果你做过 Node.js 后端,可能用过 Express 或 Koa。Spring Boot 是 Java 生态中最流行的 Web 开发框架,它的核心优势:

特性 Express.js Spring Boot
路由定义 app.get('/users', handler) @GetMapping("/users")
参数获取 req.paramsreq.queryreq.body @PathVariable@RequestParam@RequestBody
响应封装 res.json(data) 直接返回对象,自动转 JSON
参数校验 joiexpress-validator 内置 javax.validation,注解驱动
异常处理 中间件捕获 @ControllerAdvice 全局统一处理
依赖注入 手动 require 自动 @Autowired

Spring Boot 的核心理念是约定大于配置,大部分功能开箱即用,开发效率极高。

本文目标: 读完能独立搭建一个带完整异常处理、参数校验、日志拦截的 Web 项目。


二、环境准备

需要安装:

  • JDK 17+
  • Maven 3.8+
  • IDEA 2023+(Community 版免费)

创建 Spring Boot 项目:

  1. 打开 start.spring.io/

  2. 选择:

    • Project: Maven
    • Language: Java
    • Spring Boot: 3.2.0
    • Dependencies: Spring Web
  3. 点击 Generate,下载解压后用 IDEA 打开

或者直接在 IDEA 中:File → New → Project → Spring Initializr

项目结构:

plain 复制代码
src/main/java/com/example/demo/
├── DemoApplication.java          ← 启动类
├── controller/                  ← 控制器层(本文重点)
├── service/                     ← 业务逻辑层
├── dto/                         ← 数据传输对象
├── exception/                   ← 自定义异常
├── handler/                     ← 全局异常处理器
├── interceptor/                 ← 拦截器
├── filter/                      ← 过滤器
└── config/                      ← 配置类

三、第一个 RESTful API

3.1 什么是 RESTful API?

REST(Representational State Transfer)是一种软件架构风格:

  • URL 定位资源/api/users 表示用户资源
  • HTTP 方法描述操作:GET 查、POST 增、PUT 改、DELETE 删

HTTP 方法对应 CRUD:

HTTP 方法 操作 URL 示例 说明
GET 查询列表 /api/users 查询所有用户
GET 查询单个 /api/users/1 查询 ID 为 1 的用户
POST 新增 /api/users 创建用户
PUT 全量更新 /api/users/1 更新 ID 为 1 的用户全部字段
PATCH 局部更新 /api/users/1 更新 ID 为 1 的用户部分字段
DELETE 删除 /api/users/1 删除 ID 为 1 的用户

URL 设计规范:

规范 正确 错误 原因
名词复数 /api/users /api/getUser URL 是资源,不是动作
小写字母 /api/user-orders /api/userOrders 规范统一
连字符分隔 /api/user-orders /api/user_orders REST 约定
不用动词 /api/users/1 /api/deleteUser/1 HTTP 方法已经表达动作

3.2 创建实体类

java 复制代码
package com.example.demo.dto;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * 用户数据传输对象(DTO)
 * 
 * DTO 用于 Controller 和 Service 之间传递数据
 * 不包含数据库相关注解,也不包含敏感字段(如密码)
 */
@Data
public class UserDTO {
    /** 用户ID */
    private Long id;
    /** 用户名 */
    private String username;
    /** 邮箱 */
    private String email;
    /** 年龄 */
    private Integer age;
    /** 创建时间 */
    private LocalDateTime createTime;
}

3.3 创建 Controller

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

import com.example.demo.dto.UserDTO;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 用户控制器
 * 
 * @RestController = @Controller + @ResponseBody
 * 表示这是一个控制器,并且所有方法的返回值自动转为 JSON
 */
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    // 模拟数据库,线程安全的自增ID
    private static final AtomicLong ID_GENERATOR = new AtomicLong(1);
    private static final List<UserDTO> USERS = new ArrayList<>();
    
    static {
        // 初始化测试数据
        UserDTO user1 = new UserDTO();
        user1.setId(ID_GENERATOR.getAndIncrement());
        user1.setUsername("张三");
        user1.setEmail("zhangsan@example.com");
        user1.setAge(25);
        user1.setCreateTime(LocalDateTime.now());
        
        UserDTO user2 = new UserDTO();
        user2.setId(ID_GENERATOR.getAndIncrement());
        user2.setUsername("李四");
        user2.setEmail("lisi@example.com");
        user2.setAge(30);
        user2.setCreateTime(LocalDateTime.now());
        
        USERS.add(user1);
        USERS.add(user2);
    }
    
    /**
     * 查询所有用户
     * GET /api/users
     */
    @GetMapping
    public List<UserDTO> findAll() {
        return new ArrayList<>(USERS);
    }
    
    /**
     * 根据 ID 查询用户
     * GET /api/users/1
     * 
     * @PathVariable 从 URL 路径中提取参数
     */
    @GetMapping("/{id}")
    public UserDTO findById(@PathVariable Long id) {
        return USERS.stream()
                .filter(user -> user.getId().equals(id))
                .findFirst()
                .orElse(null);
    }
    
    /**
     * 新增用户
     * POST /api/users
     * Body: {"username":"王五","email":"wangwu@example.com","age":28}
     * 
     * @RequestBody 将请求体的 JSON 自动转为 UserDTO 对象
     */
    @PostMapping
    public UserDTO create(@RequestBody UserDTO userDTO) {
        userDTO.setId(ID_GENERATOR.getAndIncrement());
        userDTO.setCreateTime(LocalDateTime.now());
        USERS.add(userDTO);
        return userDTO;
    }
    
    /**
     * 全量更新用户
     * PUT /api/users/1
     * Body: {"id":1,"username":"张三(已修改)","email":"new@example.com","age":26}
     */
    @PutMapping("/{id}")
    public UserDTO update(@PathVariable Long id, @RequestBody UserDTO userDTO) {
        UserDTO existing = findById(id);
        if (existing == null) {
            return null;
        }
        existing.setUsername(userDTO.getUsername());
        existing.setEmail(userDTO.getEmail());
        existing.setAge(userDTO.getAge());
        return existing;
    }
    
    /**
     * 删除用户
     * DELETE /api/users/1
     */
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        USERS.removeIf(user -> user.getId().equals(id));
    }
}

3.4 测试 API

启动项目后,用 curl 或 Postman 测试:

bash 复制代码
# 查询所有用户
curl http://localhost:8080/api/users

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

# 新增用户
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"username":"王五","email":"wangwu@example.com","age":28}'

# 更新用户
curl -X PUT http://localhost:8080/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"id":1,"username":"张三(已修改)","email":"new@example.com","age":26}'

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

四、统一响应封装

上面的接口直接返回对象,实际项目中需要统一响应格式,方便前端处理。

4.1 创建统一响应类

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

import lombok.Data;

import java.time.LocalDateTime;

/**
 * 统一响应结果
 * 
 * 前端期望的格式:
 * {
 *   "code": 200,
 *   "message": "成功",
 *   "data": { ... },
 *   "timestamp": "2026-06-14T10:30:00"
 * }
 */
@Data
public class Result<T> {
    /** 状态码:200成功,400参数错误,500系统错误 */
    private Integer code;
    /** 提示消息 */
    private String message;
    /** 业务数据 */
    private T data;
    /** 响应时间戳 */
    private String timestamp;

    private Result() {
        this.timestamp = LocalDateTime.now().toString();
    }

    /**
     * 成功响应
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("成功");
        result.setData(data);
        return result;
    }

    /**
     * 成功响应(无数据)
     */
    public static <T> Result<T> success() {
        return success(null);
    }

    /**
     * 错误响应
     */
    public static <T> Result<T> error(Integer code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
}

4.2 修改 Controller 返回统一格式

java 复制代码
@GetMapping
public Result<List<UserDTO>> findAll() {
    return Result.success(USERS);
}

@GetMapping("/{id}")
public Result<UserDTO> findById(@PathVariable Long id) {
    UserDTO user = USERS.stream()
            .filter(u -> u.getId().equals(id))
            .findFirst()
            .orElse(null);
    return Result.success(user);
}

五、统一异常处理

5.1 为什么需要统一异常处理?

没有处理时,异常直接暴露:

JSON 复制代码
{
    "timestamp": "2026-06-14T10:30:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api/users/1"
}

前端无法判断具体错误,用户体验差。

5.2 创建自定义业务异常

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

import lombok.Getter;

/**
 * 业务异常
 * 用于封装已知的业务错误,如参数不合法、用户不存在等
 */
@Getter
public class BusinessException extends RuntimeException {
    
    /** 错误码 */
    private final Integer code;
    
    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }
    
    public BusinessException(String message) {
        this(500, message);
    }
}

5.3 创建全局异常处理器

java 复制代码
package com.example.demo.handler;

import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * 全局异常处理器
 * 
 * @RestControllerAdvice = @ControllerAdvice + @ResponseBody
 * 自动捕获所有 Controller 抛出的异常,统一处理返回
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 处理业务异常(已知错误,如参数不合法)
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
        log.warn("业务异常 [{}] - {}", request.getRequestURI(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }
    
    /**
     * 处理其他所有异常(未知错误)
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e, HttpServletRequest request) {
        log.error("系统异常 [{}]", request.getRequestURI(), e);
        // 生产环境不要暴露具体错误信息,返回通用提示
        return Result.error(500, "系统繁忙,请稍后重试");
    }
}

5.4 在 Controller 中使用异常

java 复制代码
@GetMapping("/{id}")
public Result<UserDTO> findById(@PathVariable Long id) {
    if (id == null || id <= 0) {
        throw new BusinessException(400, "用户ID必须大于0");
    }
    
    UserDTO user = USERS.stream()
            .filter(u -> u.getId().equals(id))
            .findFirst()
            .orElseThrow(() -> new BusinessException(404, "用户不存在"));
    
    return Result.success(user);
}

测试异常:

bash 复制代码
# 参数不合法
curl http://localhost:8080/api/users/0
# 返回:{"code":400,"message":"用户ID必须大于0",...}

# 用户不存在
curl http://localhost:8080/api/users/999
# 返回:{"code":404,"message":"用户不存在",...}

六、参数校验

6.1 引入校验依赖

pom.xml 中添加:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

6.2 常用校验注解

注解 作用 示例
@NotNull 不能为 null @NotNull private Long id
@NotBlank 不能为 null 且至少一个非空白字符 @NotBlank private String name
@NotEmpty 不能为 null 且不为空 @NotEmpty private List<String> list
@Min 最小值 @Min(1) private Integer age
@Max 最大值 @Max(150) private Integer age
@Size 长度范围 @Size(min=2, max=20) private String name
@Email 邮箱格式 @Email private String email
@Pattern 正则匹配 @Pattern(regexp="^1[3-9]\\d{9}$")

6.3 创建带校验的 DTO

java 复制代码
package com.example.demo.dto;

import lombok.Data;

import javax.validation.constraints.*;

@Data
public class UserCreateDTO {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;
    
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
}

6.4 在 Controller 中启用校验

java 复制代码
@PostMapping
public Result<UserDTO> create(@RequestBody @Valid UserCreateDTO createDTO) {
    // @Valid 触发校验,失败时抛出 MethodArgumentNotValidException
    UserDTO userDTO = new UserDTO();
    userDTO.setUsername(createDTO.getUsername());
    userDTO.setEmail(createDTO.getEmail());
    userDTO.setAge(createDTO.getAge());
    
    userDTO.setId(ID_GENERATOR.getAndIncrement());
    userDTO.setCreateTime(LocalDateTime.now());
    USERS.add(userDTO);
    
    return Result.success(userDTO);
}

6.5 处理校验异常(补充到全局异常处理器)

java 复制代码
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidException(MethodArgumentNotValidException e) {
    // 提取所有字段错误信息
    String message = e.getBindingResult().getFieldErrors().stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .reduce((a, b) -> a + "; " + b)
            .orElse("参数校验失败");
    
    return Result.error(400, message);
}

测试校验:

bash 复制代码
# 用户名过短
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"username":"张","email":"invalid","age":-1}'
# 返回:{"code":400,"message":"username: 用户名长度必须在2-20之间; email: 邮箱格式不正确; age: 年龄不能小于0",...}

七、拦截器与过滤器

7.1 拦截器(Interceptor)

拦截器是 Spring MVC 层面的,可以获取 Controller 信息。

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

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 请求日志拦截器
 * 记录每个请求的耗时
 */
@Slf4j
@Component
public class RequestLogInterceptor implements HandlerInterceptor {
    
    private static final String START_TIME = "requestStartTime";
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        request.setAttribute(START_TIME, System.currentTimeMillis());
        log.info("请求开始 [{}] {}", request.getMethod(), request.getRequestURI());
        return true;  // true 继续执行,false 中断
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        Long startTime = (Long) request.getAttribute(START_TIME);
        long duration = System.currentTimeMillis() - startTime;
        log.info("请求结束 [{}] {} - 耗时 {}ms - 状态 {}", 
                request.getMethod(), 
                request.getRequestURI(),
                duration,
                response.getStatus());
    }
}

注册拦截器:

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

import com.example.demo.interceptor.RequestLogInterceptor;
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 WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private RequestLogInterceptor requestLogInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestLogInterceptor)
                .addPathPatterns("/**")           // 拦截所有路径
                .excludePathPatterns("/api/login"); // 排除登录接口
    }
}

7.2 过滤器(Filter)

过滤器是 Servlet 层面的,在请求进入 Spring MVC 之前处理。

java 复制代码
package com.example.demo.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 请求编码过滤器
 */
@Slf4j
@Component
@Order(1)  // 执行顺序,数字越小越先执行
public class EncodingFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        log.info("Filter 处理: {}", httpRequest.getRequestURI());
        
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        
        chain.doFilter(request, response);  // 继续执行
    }
}

7.3 拦截器 vs 过滤器对比

特性 拦截器 过滤器
所属框架 Spring MVC Servlet
执行时机 Controller 前后 请求进入容器时
获取 Controller 信息 可以 不可以
修改请求/响应 可以 可以
处理异常 afterCompletion 可获取 无法直接获取
使用场景 权限校验、日志记录、性能监控 编码设置、跨域处理

八、跨域处理(CORS)

8.1 什么是跨域?

浏览器安全策略:协议、域名、端口任一不同即为跨域。

场景 是否跨域
localhost:3000localhost:8080 是(端口不同)
a.example.comb.example.com 是(子域名不同)
http://https:// 是(协议不同)

8.2 全局跨域配置

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

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        
        // 允许的来源(生产环境指定具体域名,如 http://example.com)
        config.addAllowedOriginPattern("*");  // 开发环境允许所有
        
        // 允许的请求头
        config.addAllowedHeader("*");
        
        // 允许的方法(GET、POST、PUT、DELETE 等)
        config.addAllowedMethod("*");
        
        // 允许携带 Cookie
        config.setAllowCredentials(true);
        
        // 预检请求缓存时间(秒)
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return new CorsFilter(source);
    }
}

九、完整项目代码汇总

pom.xml 完整依赖:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             https://maven.io/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>1.0.0</version>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- Web 开发 -->
        <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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        
        <!-- 简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

十、面试题自检

问题 答案
@RestController vs @Controller 前者自动返回 JSON,后者需要 @ResponseBody
@RequestBody 作用? 将请求体 JSON 绑定到 Java 对象
@Valid@Validated 区别? @Valid 用于 Bean 校验,@Validated 支持分组校验
拦截器和过滤器区别? 拦截器是 Spring MVC 层,过滤器是 Servlet 层
统一异常处理怎么做? @RestControllerAdvice + @ExceptionHandler
跨域怎么解决? CorsFilter 全局配置或 @CrossOrigin 注解

十一、总结

知识点 核心要点
RESTful 设计 HTTP 方法对应 CRUD,URL 用名词复数
参数绑定 @PathVariable@RequestParam@RequestBody
统一响应 Result<T> 封装 code、message、data
统一异常 @RestControllerAdvice 全局捕获
参数校验 javax.validation + @Valid
拦截器 Spring MVC 层,记录日志/权限校验
过滤器 Servlet 层,处理编码/跨域
跨域处理 CorsFilter 全局配置
相关推荐
yurenpai(27届找实习中)1 小时前
Feed 流推送与附近商户:从推模式到 GeoHash,一条 Timeline 的完整旅程
java·数据库·oracle·feed
Reart1 小时前
Go语言——slice切片技术原理
后端
生锈的键盘1 小时前
Bazel 深度实战:传统 WORKSPACE 依赖管理全解、痛点与企业二进制劫持方案
后端
小bo波1 小时前
Java反射机制——运行时"透视"类的秘密
java·jvm·反射·源码分析·动态代理·进阶·spring底层·框架原理
IT 行者1 小时前
GitHub Spec Kit 实战(三):写一份能管住所有 spec 的 /speckit.constitution
java·github·ai编程·claude
java1234_小锋1 小时前
Spring Boot 的核心注解 @SpringBootApplication 由哪三个注解组成?
java·spring boot·后端
::呵呵哒::1 小时前
在macOS/Linux上优雅管理多个JDK版本:环境变量与别名配置指南
java·linux·macos
Master_Azur1 小时前
Web后端基础-Spring分层解耦
spring boot·后端·spring
IT 行者1 小时前
GitHub Spec Kit 实战(二):写一份不偏的 /speckit.specify
java·github·ai编程·claude