一、前言:为什么选择 Spring Boot 做 Web 开发?
如果你做过 Node.js 后端,可能用过 Express 或 Koa。Spring Boot 是 Java 生态中最流行的 Web 开发框架,它的核心优势:
| 特性 | Express.js | Spring Boot |
|---|---|---|
| 路由定义 | app.get('/users', handler) |
@GetMapping("/users") |
| 参数获取 | req.params、req.query、req.body |
@PathVariable、@RequestParam、@RequestBody |
| 响应封装 | res.json(data) |
直接返回对象,自动转 JSON |
| 参数校验 | joi、express-validator |
内置 javax.validation,注解驱动 |
| 异常处理 | 中间件捕获 | @ControllerAdvice 全局统一处理 |
| 依赖注入 | 手动 require | 自动 @Autowired |
Spring Boot 的核心理念是约定大于配置,大部分功能开箱即用,开发效率极高。
本文目标: 读完能独立搭建一个带完整异常处理、参数校验、日志拦截的 Web 项目。
二、环境准备
需要安装:
- JDK 17+
- Maven 3.8+
- IDEA 2023+(Community 版免费)
创建 Spring Boot 项目:
-
选择:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.0
- Dependencies: Spring Web
-
点击 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:3000 → localhost:8080 |
是(端口不同) |
a.example.com → b.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 全局配置 |