Spring Boot 第一个 RESTful API:@RestController 与 @RequestMapping
Spring Boot 第一个 RESTful API:@RestController 与 @RequestMapping
一、引言
在当今的微服务架构时代,RESTful API 已成为前后端分离、服务间通信的事实标准。Spring Boot 作为 Java 生态中最流行的框架之一,通过其简洁的注解驱动开发模式,让开发者能够快速构建高性能、易维护的 RESTful API。其中,@RestController 和 @RequestMapping 是最基础也是最重要的两个注解,它们奠定了 Spring Boot Web 开发的基石。
许多初学者在学习 Spring Boot 时,往往急于求成地想要构建复杂的应用,却忽略了对基础注解的深入理解。实际上,只有真正掌握了 @RestController 和 @RequestMapping 的工作原理和使用技巧,才能在后续的开发中游刃有余地处理各种复杂的 API 设计场景。
本文将从一个简单的 “Hello World” API 开始,逐步深入到复杂的 RESTful API 设计,通过完整的代码示例和实践场景,全面解析这两个核心注解的使用方法和底层原理。
二、技术背景
2.1 RESTful API 基础概念
REST(Representational State Transfer,表述性状态转移)是一种软件架构风格,它定义了一组约束条件和原则,用于创建可扩展的 Web 服务。RESTful API 遵循以下核心原则:
- 资源导向:一切皆为资源,每个资源都有唯一的标识符(URI)
- 统一接口:使用标准的 HTTP 方法(GET、POST、PUT、DELETE 等)
- 无状态性:每个请求必须包含处理该请求所需的所有信息
- 可缓存性:响应应明确标识是否可缓存
- 分层系统:客户端无法直接知道是直接与服务器通信还是通过中间层
2.2 Spring MVC 与 Spring Boot 的关系
Spring Boot 并不是替代 Spring MVC,而是在 Spring MVC 基础上的进一步封装和简化:
- Spring MVC:提供了基于 MVC 模式的 Web 开发框架,需要大量的 XML 或 Java 配置
- Spring Boot:通过自动配置和起步依赖,消除了繁琐的配置,让 Spring MVC 开箱即用
2.3 @RestController 与 @Controller 的区别
在 Spring 中,我们有两个主要的控制器注解:
- @Controller:传统的 Spring MVC 控制器,主要用于返回视图(HTML 页面)
- @RestController:Spring 4.0 引入的组合注解,相当于
@Controller + @ResponseBody
// 传统 @Controller 需要 @ResponseBody 来返回 JSON
@Controller
public class TraditionalController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "Hello World";
}
}
// @RestController 自动包含 @ResponseBody
@RestController
public class ModernController {
@GetMapping("/hello")
public String hello() {
return "Hello World";
}
}
2.4 @RequestMapping 的演进
@RequestMapping 是 Spring MVC 中最基础的映射注解,但随着 REST 风格的普及,Spring 4.3 引入了更具体的快捷注解:
- @GetMapping:等价于
@RequestMapping(method = RequestMethod.GET) - @PostMapping:等价于
@RequestMapping(method = RequestMethod.POST) - @PutMapping:等价于
@RequestMapping(method = RequestMethod.PUT) - @DeleteMapping:等价于
@RequestMapping(method = RequestMethod.DELETE) - @PatchMapping:等价于
@RequestMapping(method = RequestMethod.PATCH)
三、应用使用场景
3.1 基础 CRUD 操作场景
场景描述:构建用户管理系统的基础 API,实现用户的增删改查操作。
技术需求:使用 @RestController 定义控制器,@RequestMapping 及其派生注解映射 HTTP 请求到具体方法。
3.2 前后端分离项目场景
场景描述:前端 Vue.js 或 React 应用需要通过 API 与后端交互,获取和提交数据。
技术需求:RESTful API 返回 JSON 格式数据,支持跨域访问,提供标准化的响应格式。
3.3 微服务间通信场景
场景描述:在微服务架构中,服务 A 需要调用服务 B 提供的用户信息查询接口。
技术需求:API 设计遵循 REST 原则,支持服务发现和健康检查,提供清晰的 API 文档。
3.4 移动应用后端场景
场景描述:为 iOS 或 Android 应用提供后端 API 服务,需要处理不同的数据格式和设备适配。
技术需求:轻量级的 API 响应,支持分页和过滤,处理文件上传下载。
3.5 第三方系统集成场景
场景描述:向合作伙伴或第三方系统开放 API,需要提供 OAuth2 认证和详细的 API 文档。
技术需求:安全的认证授权机制,API 版本管理,限流和监控。
四、不同场景下详细代码实现
4.1 项目基础结构与依赖配置
4.1.1 Maven 项目结构
rest-api-demo/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/com/example/restapidemo/
│ │ │ ├── RestApiDemoApplication.java
│ │ │ ├── controller/
│ │ │ │ ├── UserController.java
│ │ │ │ ├── ProductController.java
│ │ │ │ └── ApiController.java
│ │ │ ├── model/
│ │ │ │ ├── User.java
│ │ │ │ ├── Product.java
│ │ │ │ └── ApiResponse.java
│ │ │ ├── service/
│ │ │ │ ├── UserService.java
│ │ │ │ └── ProductService.java
│ │ │ ├── dto/
│ │ │ │ ├── UserDto.java
│ │ │ │ └── CreateUserRequest.java
│ │ │ ├── exception/
│ │ │ │ ├── ResourceNotFoundException.java
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ └── config/
│ │ │ ├── CorsConfig.java
│ │ │ └── WebConfig.java
│ │ ├── resources/
│ │ │ ├── application.yml
│ │ │ ├── application-dev.yml
│ │ │ └── messages.properties
│ │ └── static/
│ └── test/
└── target/
4.1.2 父 POM 配置
pom.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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>rest-api-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Spring Boot REST API Demo</name>
<description>深入理解 @RestController 与 @RequestMapping</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<lombok.version>1.18.30</lombok.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
</properties>
<dependencies>
<!-- Spring Boot Web - 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Validation - 请求参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok - 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- MapStruct - 对象映射 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 领域模型与数据传输对象
4.2.1 实体类
src/main/java/com/example/restapidemo/model/User.java
package com.example.restapidemo.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
/**
* 用户实体类
* 演示 JPA 实体与 REST API 的结合
*/
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度必须在 2-50 个字符之间")
@Column(unique = true, nullable = false)
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Column(unique = true, nullable = false)
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于 6 个字符")
@Column(nullable = false)
private String password;
@NotBlank(message = "姓名不能为空")
@Size(max = 100, message = "姓名长度不能超过 100 个字符")
private String fullName;
@NotNull(message = "年龄不能为空")
private Integer age;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/**
* 用户状态枚举
*/
public enum UserStatus {
ACTIVE("激活"),
INACTIVE("未激活"),
SUSPENDED("已禁用");
private final String description;
UserStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
}
src/main/java/com/example/restapidemo/model/Product.java
package com.example.restapidemo.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 产品实体类
* 演示复杂业务对象的建模
*/
@Entity
@Table(name = "products")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "产品名称不能为空")
@Size(max = 200, message = "产品名称长度不能超过 200 个字符")
@Column(nullable = false)
private String name;
@Size(max = 1000, message = "产品描述长度不能超过 1000 个字符")
private String description;
@NotNull(message = "价格不能为空")
@DecimalMin(value = "0.01", message = "价格必须大于 0")
@Digits(integer = 10, fraction = 2, message = "价格格式不正确")
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal price;
@NotNull(message = "库存数量不能为空")
@Min(value = 0, message = "库存数量不能为负数")
@Max(value = 999999, message = "库存数量超出限制")
@Column(nullable = false)
private Integer stock;
@NotBlank(message = "分类不能为空")
@Size(max = 50, message = "分类长度不能超过 50 个字符")
private String category;
@Pattern(regexp = "^[A-Za-z0-9-]+$", message = "SKU 只能包含字母、数字和连字符")
@Column(unique = true)
private String sku;
@Column(name = "is_active")
private Boolean isActive = true;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
4.2.2 数据传输对象(DTO)
src/main/java/com/example/restapidemo/dto/CreateUserRequest.java
package com.example.restapidemo.dto;
import lombok.Data;
import javax.validation.constraints.*;
/**
* 创建用户请求 DTO
* 演示请求参数的封装和验证
*/
@Data
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度必须在 2-50 个字符之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于 6 个字符")
private String password;
@NotBlank(message = "姓名不能为空")
@Size(max = 100, message = "姓名长度不能超过 100 个字符")
private String fullName;
@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄必须大于 0")
@Max(value = 150, message = "年龄不能超过 150")
private Integer age;
}
src/main/java/com/example/restapidemo/dto/UserDto.java
package com.example.restapidemo.dto;
import com.example.restapidemo.model.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 用户 DTO
* 演示响应数据的封装(不包含敏感信息如密码)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private Long id;
private String username;
private String email;
private String fullName;
private Integer age;
private User.UserStatus status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* 从 User 实体转换为 UserDto
*/
public static UserDto from(User user) {
return UserDto.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.fullName(user.getFullName())
.age(user.getAge())
.status(user.getStatus())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build();
}
}
4.2.3 统一响应格式
src/main/java/com/example/restapidemo/model/ApiResponse.java
package com.example.restapidemo.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 统一 API 响应格式
* 演示标准化的 API 响应结构
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private String timestamp;
private String path;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.success(true)
.message("操作成功")
.data(data)
.timestamp(LocalDateTime.now().toString())
.build();
}
public static <T> ApiResponse<T> success(T data, String message) {
return ApiResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.timestamp(LocalDateTime.now().toString())
.build();
}
public static <T> ApiResponse<T> error(String message) {
return ApiResponse.<T>builder()
.success(false)
.message(message)
.timestamp(LocalDateTime.now().toString())
.build();
}
}
4.3 服务层实现
4.3.1 用户服务
src/main/java/com/example/restapidemo/service/UserService.java
package com.example.restapidemo.service;
import com.example.restapidemo.dto.CreateUserRequest;
import com.example.restapidemo.dto.UserDto;
import com.example.restapidemo.model.User;
import com.example.restapidemo.model.User.UserStatus;
import java.util.List;
import java.util.Optional;
/**
* 用户服务接口
* 演示业务逻辑层的抽象
*/
public interface UserService {
/**
* 创建用户
*/
UserDto createUser(CreateUserRequest request);
/**
* 根据 ID 获取用户
*/
Optional<UserDto> getUserById(Long id);
/**
* 获取所有用户
*/
List<UserDto> getAllUsers();
/**
* 更新用户
*/
UserDto updateUser(Long id, CreateUserRequest request);
/**
* 删除用户
*/
void deleteUser(Long id);
/**
* 根据用户名查找用户
*/
Optional<UserDto> getUserByUsername(String username);
/**
* 更新用户状态
*/
UserDto updateUserStatus(Long id, UserStatus status);
}
src/main/java/com/example/restapidemo/service/impl/UserServiceImpl.java
package com.example.restapidemo.service.impl;
import com.example.restapidemo.dto.CreateUserRequest;
import com.example.restapidemo.dto.UserDto;
import com.example.restapidemo.model.User;
import com.example.restapidemo.model.User.UserStatus;
import com.example.restapidemo.service.UserService;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 用户服务实现(模拟数据库操作)
* 在实际项目中,这里会是 JPA Repository 或 MyBatis Mapper
*/
@Service
public class UserServiceImpl implements UserService {
// 使用 ConcurrentHashMap 模拟数据库存储
private final ConcurrentHashMap<Long, User> userDatabase = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public UserServiceImpl() {
// 初始化一些测试数据
initializeTestData();
}
private void initializeTestData() {
User user1 = User.builder()
.id(idGenerator.getAndIncrement())
.username("zhangsan")
.email("zhangsan@example.com")
.password("password123")
.fullName("张三")
.age(28)
.status(UserStatus.ACTIVE)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
User user2 = User.builder()
.id(idGenerator.getAndIncrement())
.username("lisi")
.email("lisi@example.com")
.password("password456")
.fullName("李四")
.age(32)
.status(UserStatus.ACTIVE)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
userDatabase.put(user1.getId(), user1);
userDatabase.put(user2.getId(), user2);
}
@Override
public UserDto createUser(CreateUserRequest request) {
// 检查用户名和邮箱是否已存在
if (userDatabase.values().stream()
.anyMatch(user -> user.getUsername().equals(request.getUsername()))) {
throw new IllegalArgumentException("用户名已存在: " + request.getUsername());
}
if (userDatabase.values().stream()
.anyMatch(user -> user.getEmail().equals(request.getEmail()))) {
throw new IllegalArgumentException("邮箱已存在: " + request.getEmail());
}
// 创建新用户
User newUser = User.builder()
.id(idGenerator.getAndIncrement())
.username(request.getUsername())
.email(request.getEmail())
.password(request.getPassword()) // 实际项目中应该加密存储
.fullName(request.getFullName())
.age(request.getAge())
.status(UserStatus.ACTIVE)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
userDatabase.put(newUser.getId(), newUser);
return UserDto.from(newUser);
}
@Override
public Optional<UserDto> getUserById(Long id) {
User user = userDatabase.get(id);
return Optional.ofNullable(user).map(UserDto::from);
}
@Override
public List<UserDto> getAllUsers() {
return userDatabase.values().stream()
.map(UserDto::from)
.toList();
}
@Override
public UserDto updateUser(Long id, CreateUserRequest request) {
User existingUser = userDatabase.get(id);
if (existingUser == null) {
throw new IllegalArgumentException("用户不存在: " + id);
}
// 检查用户名是否被其他用户使用
Optional<User> userWithSameUsername = userDatabase.values().stream()
.filter(user -> user.getUsername().equals(request.getUsername()) && !user.getId().equals(id))
.findFirst();
if (userWithSameUsername.isPresent()) {
throw new IllegalArgumentException("用户名已被其他用户使用: " + request.getUsername());
}
// 更新用户信息
User updatedUser = User.builder()
.id(existingUser.getId())
.username(request.getUsername())
.email(request.getEmail())
.password(request.getPassword())
.fullName(request.getFullName())
.age(request.getAge())
.status(existingUser.getStatus())
.createdAt(existingUser.getCreatedAt())
.updatedAt(LocalDateTime.now())
.build();
userDatabase.put(id, updatedUser);
return UserDto.from(updatedUser);
}
@Override
public void deleteUser(Long id) {
if (!userDatabase.containsKey(id)) {
throw new IllegalArgumentException("用户不存在: " + id);
}
userDatabase.remove(id);
}
@Override
public Optional<UserDto> getUserByUsername(String username) {
return userDatabase.values().stream()
.filter(user -> user.getUsername().equals(username))
.findFirst()
.map(UserDto::from);
}
@Override
public UserDto updateUserStatus(Long id, UserStatus status) {
User existingUser = userDatabase.get(id);
if (existingUser == null) {
throw new IllegalArgumentException("用户不存在: " + id);
}
User updatedUser = User.builder()
.id(existingUser.getId())
.username(existingUser.getUsername())
.email(existingUser.getEmail())
.password(existingUser.getPassword())
.fullName(existingUser.getFullName())
.age(existingUser.getAge())
.status(status)
.createdAt(existingUser.getCreatedAt())
.updatedAt(LocalDateTime.now())
.build();
userDatabase.put(id, updatedUser);
return UserDto.from(updatedUser);
}
}
4.4 控制器层实现
4.4.1 基础控制器(Hello World)
src/main/java/com/example/restapidemo/controller/ApiController.java
package com.example.restapidemo.controller;
import com.example.restapidemo.model.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* 基础 API 控制器
* 演示最简单的 @RestController 和 @RequestMapping 使用
*/
@RestController
@RequestMapping("/api")
@Tag(name = "基础 API", description = "演示最基本的 REST API 操作")
public class ApiController {
@Value("${app.version:1.0.0}")
private String appVersion;
@Value("${spring.application.name:REST API Demo}")
private String appName;
/**
* GET 请求 - 获取应用信息
* 演示最基础的 @GetMapping 注解
*/
@Operation(summary = "获取应用信息", description = "返回应用的基本信息")
@GetMapping("/info")
public ApiResponse<Map<String, String>> getAppInfo() {
Map<String, String> info = new HashMap<>();
info.put("name", appName);
info.put("version", appVersion);
info.put("author", "Spring Boot Team");
info.put("description", "深入理解 @RestController 与 @RequestMapping");
return ApiResponse.success(info, "获取应用信息成功");
}
/**
* GET 请求 - 带路径变量的问候
* 演示 @PathVariable 注解的使用
*/
@Operation(summary = "个性化问候", description = "根据用户名返回个性化问候语")
@GetMapping("/greeting/{name}")
public ApiResponse<String> greetByName(@PathVariable String name) {
String greeting = String.format("Hello, %s! Welcome to Spring Boot REST API!", name);
return ApiResponse.success(greeting);
}
/**
* GET 请求 - 带多个路径变量
* 演示多个 @PathVariable 的使用
*/
@Operation(summary = "数学运算", description = "执行简单的加法运算")
@GetMapping("/math/add/{a}/{b}")
public ApiResponse<Map<String, Object>> addNumbers(@PathVariable Integer a, @PathVariable Integer b) {
Map<String, Object> result = new HashMap<>();
result.put("operation", a + " + " + b);
result.put("result", a + b);
result.put("timestamp", System.currentTimeMillis());
return ApiResponse.success(result, "计算完成");
}
/**
* GET 请求 - 带请求参数的查询
* 演示 @RequestParam 注解的使用
*/
@Operation(summary = "搜索用户", description = "根据关键词搜索用户")
@GetMapping("/search")
public ApiResponse<Map<String, Object>> searchUsers(
@RequestParam(defaultValue = "") String keyword,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
Map<String, Object> result = new HashMap<>();
result.put("keyword", keyword);
result.put("page", page);
result.put("size", size);
result.put("total", 0);
result.put("results", new Object[]{});
String message = keyword.isEmpty() ? "显示所有用户" : "搜索关键词: " + keyword;
return ApiResponse.success(result, message);
}
/**
* POST 请求 - 接收 JSON 数据
* 演示 @RequestBody 注解的使用
*/
@Operation(summary = "创建消息", description = "创建一条新的消息")
@PostMapping("/messages")
public ApiResponse<Map<String, Object>> createMessage(@RequestBody Map<String, String> messageRequest) {
String title = messageRequest.get("title");
String content = messageRequest.get("content");
Map<String, Object> createdMessage = new HashMap<>();
createdMessage.put("id", System.currentTimeMillis());
createdMessage.put("title", title);
createdMessage.put("content", content);
createdMessage.put("createdAt", System.currentTimeMillis());
return ApiResponse.success(createdMessage, "消息创建成功");
}
/**
* PUT 请求 - 更新资源
* 演示 @PutMapping 注解的使用
*/
@Operation(summary = "更新消息", description = "更新指定的消息")
@PutMapping("/messages/{id}")
public ApiResponse<Map<String, Object>> updateMessage(
@PathVariable Long id,
@RequestBody Map<String, String> messageRequest) {
String title = messageRequest.get("title");
String content = messageRequest.get("content");
Map<String, Object> updatedMessage = new HashMap<>();
updatedMessage.put("id", id);
updatedMessage.put("title", title);
updatedMessage.put("content", content);
updatedMessage.put("updatedAt", System.currentTimeMillis());
return ApiResponse.success(updatedMessage, "消息更新成功");
}
/**
* DELETE 请求 - 删除资源
* 演示 @DeleteMapping 注解的使用
*/
@Operation(summary = "删除消息", description = "删除指定的消息")
@DeleteMapping("/messages/{id}")
public ApiResponse<Void> deleteMessage(@PathVariable Long id) {
// 实际项目中这里会有数据库删除操作
return ApiResponse.success(null, "消息删除成功: " + id);
}
/**
* PATCH 请求 - 部分更新
* 演示 @PatchMapping 注解的使用
*/
@Operation(summary = "部分更新用户", description = "部分更新用户信息")
@PatchMapping("/users/{id}/status")
public ApiResponse<Map<String, Object>> partialUpdateUserStatus(
@PathVariable Long id,
@RequestParam String status) {
Map<String, Object> result = new HashMap<>();
result.put("id", id);
result.put("status", status);
result.put("updatedAt", System.currentTimeMillis());
return ApiResponse.success(result, "用户状态更新成功");
}
/**
* 演示 @RequestMapping 的 method 属性
* 等价于 @GetMapping
*/
@Operation(summary = "健康检查", description = "检查 API 服务健康状态")
@RequestMapping(value = "/health", method = RequestMethod.GET)
public ApiResponse<String> healthCheck(HttpServletRequest request) {
Map<String, String> info = new HashMap<>();
info.put("status", "UP");
info.put("timestamp", System.currentTimeMillis() + "");
info.put("path", request.getRequestURI());
info.put("method", request.getMethod());
return ApiResponse.success(info, "服务正常运行");
}
}
4.4.2 用户控制器(完整 CRUD)
src/main/java/com/example/restapidemo/controller/UserController.java
package com.example.restapidemo.controller;
import com.example.restapidemo.dto.CreateUserRequest;
import com.example.restapidemo.dto.UserDto;
import com.example.restapidemo.model.ApiResponse;
import com.example.restapidemo.model.User;
import com.example.restapidemo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
/**
* 用户控制器
* 演示完整的 RESTful API 设计和实现
*/
@RestController
@RequestMapping("/api/users")
@Tag(name = "用户管理", description = "用户资源的 CRUD 操作")
@Validated
public class UserController {
@Autowired
private UserService userService;
/**
* GET /api/users - 获取所有用户
* 演示集合资源的获取
*/
@Operation(
summary = "获取所有用户",
description = "返回系统中所有用户的列表",
responses = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "成功获取用户列表",
content = @Content(
array = @ArraySchema(schema = @Schema(implementation = UserDto.class))
)
)
}
)
@GetMapping
public ResponseEntity<ApiResponse<List<UserDto>>> getAllUsers() {
List<UserDto> users = userService.getAllUsers();
return ResponseEntity.ok(ApiResponse.success(users, "获取用户列表成功"));
}
/**
* GET /api/users/{id} - 根据 ID 获取用户
* 演示单个资源的获取
*/
@Operation(
summary = "根据ID获取用户",
description = "根据用户ID返回用户详细信息",
responses = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "成功获取用户信息",
content = @Content(schema = @Schema(implementation = UserDto.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "404",
description = "用户不存在"
)
}
)
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<UserDto>> getUserById(
@Parameter(description = "用户ID", required = true, example = "1")
@PathVariable Long id) {
return userService.getUserById(id)
.map(userDto -> ResponseEntity.ok(ApiResponse.success(userDto, "获取用户信息成功")))
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("用户不存在: " + id)));
}
/**
* GET /api/users/username/{username} - 根据用户名获取用户
* 演示自定义路径参数的使用
*/
@Operation(summary = "根据用户名获取用户", description = "根据用户名返回用户详细信息")
@GetMapping("/username/{username}")
public ResponseEntity<ApiResponse<UserDto>> getUserByUsername(
@PathVariable String username) {
return userService.getUserByUsername(username)
.map(userDto -> ResponseEntity.ok(ApiResponse.success(userDto)))
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("用户不存在: " + username)));
}
/**
* POST /api/users - 创建新用户
* 演示资源的创建
*/
@Operation(
summary = "创建用户",
description = "创建一个新用户",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "用户创建请求",
required = true,
content = @Content(schema = @Schema(implementation = CreateUserRequest.class))
),
responses = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "201",
description = "用户创建成功",
content = @Content(schema = @Schema(implementation = UserDto.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "400",
description = "请求参数错误"
)
}
)
@PostMapping
public ResponseEntity<ApiResponse<UserDto>> createUser(
@Valid @RequestBody CreateUserRequest request) {
try {
UserDto createdUser = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(createdUser, "用户创建成功"));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(e.getMessage()));
}
}
/**
* PUT /api/users/{id} - 更新用户(完整更新)
* 演示资源的完整更新
*/
@Operation(summary = "更新用户", description = "更新指定用户的完整信息")
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<UserDto>> updateUser(
@PathVariable Long id,
@Valid @RequestBody CreateUserRequest request) {
try {
UserDto updatedUser = userService.updateUser(id, request);
return ResponseEntity.ok(ApiResponse.success(updatedUser, "用户更新成功"));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(e.getMessage()));
}
}
/**
* PATCH /api/users/{id}/status - 部分更新用户状态
* 演示资源的部分更新
*/
@Operation(summary = "更新用户状态", description = "部分更新用户的状态信息")
@PatchMapping("/{id}/status")
public ResponseEntity<ApiResponse<UserDto>> updateUserStatus(
@PathVariable Long id,
@RequestParam User.UserStatus status) {
try {
UserDto updatedUser = userService.updateUserStatus(id, status);
return ResponseEntity.ok(ApiResponse.success(updatedUser, "用户状态更新成功"));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(e.getMessage()));
}
}
/**
* DELETE /api/users/{id} - 删除用户
* 演示资源的删除
*/
@Operation(summary = "删除用户", description = "删除指定的用户")
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable Long id) {
try {
userService.deleteUser(id);
return ResponseEntity.ok(ApiResponse.success(null, "用户删除成功"));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(e.getMessage()));
}
}
/**
* GET /api/users/search - 搜索用户
* 演示复杂查询参数的使用
*/
@Operation(summary = "搜索用户", description = "根据条件搜索用户")
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<UserDto>>> searchUsers(
@RequestParam(required = false) String username,
@RequestParam(required = false) String email,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
// 这里简化处理,实际项目中会有复杂的查询逻辑
List<UserDto> allUsers = userService.getAllUsers();
// 模拟过滤逻辑
List<UserDto> filteredUsers = allUsers.stream()
.filter(user -> username == null || user.getUsername().contains(username))
.filter(user -> email == null || user.getEmail().contains(email))
.filter(user -> minAge == null || user.getAge() >= minAge)
.filter(user -> maxAge == null || user.getAge() <= maxAge)
.toList();
// 模拟分页
int start = page * size;
int end = Math.min(start + size, filteredUsers.size());
List<UserDto> pagedUsers = filteredUsers.subList(start, end);
return ResponseEntity.ok(ApiResponse.success(pagedUsers,
String.format("搜索完成,共找到 %d 个用户", filteredUsers.size())));
}
}
4.5 配置类与异常处理
4.5.1 跨域配置
src/main/java/com/example/restapidemo/config/CorsConfig.java
package com.example.restapidemo.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();
// 允许所有域名进行跨域调用
config.addAllowedOriginPattern("*");
// 允许跨越发送 cookie
config.setAllowCredentials(true);
// 放行全部原始头信息
config.addAllowedHeader("*");
// 允许所有请求方法跨域调用
config.addAllowedMethod("*");
// 预检请求的有效期(秒)
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
4.5.2 全局异常处理
src/main/java/com/example/restapidemo/exception/ResourceNotFoundException.java
package com.example.restapidemo.exception;
/**
* 资源未找到异常
*/
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s 不存在,%s: %s", resourceName, fieldName, fieldValue));
}
}
src/main/java/com/example/restapidemo/exception/GlobalExceptionHandler.java
package com.example.restapidemo.exception;
import com.example.restapidemo.model.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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 javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 全局异常处理器
* 演示统一的异常处理机制
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理资源未找到异常
*/
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleResourceNotFound(ResourceNotFoundException e) {
log.warn("资源未找到: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(e.getMessage()));
}
/**
* 处理参数验证异常(@Valid 注解)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
log.warn("参数验证失败: {}", errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.<Map<String, String>>builder()
.success(false)
.message("参数验证失败")
.data(errors)
.build());
}
/**
* 处理路径参数验证异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleConstraintViolation(
ConstraintViolationException ex) {
Map<String, String> errors = ex.getConstraintViolations().stream()
.collect(Collectors.toMap(
violation -> violation.getPropertyPath().toString(),
ConstraintViolation::getMessage
));
log.warn("路径参数验证失败: {}", errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.<Map<String, String>>builder()
.success(false)
.message("参数验证失败")
.data(errors)
.build());
}
/**
* 处理非法参数异常
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException e) {
log.warn("非法参数: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(e.getMessage()));
}
/**
* 处理通用异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("系统内部错误,请联系管理员"));
}
}
4.6 主启动类
src/main/java/com/example/restapidemo/RestApiDemoApplication.java
package com.example.restapidemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* Spring Boot REST API 演示应用主类
*/
@SpringBootApplication
@EnableConfigurationProperties
public class RestApiDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RestApiDemoApplication.class, args);
}
}
4.7 配置文件
src/main/resources/application.yml
# 应用基本信息
spring:
application:
name: rest-api-demo
# 配置属性(演示 @Value 注解的使用)
app:
version: 1.0.0
description: Spring Boot REST API 学习项目
# 服务器配置
server:
port: 8080
servlet:
context-path: /
tomcat:
uri-encoding: UTF-8
max-threads: 800
min-spare-threads: 30
# 日志配置
logging:
level:
com.example.restapidemo: DEBUG
org.springframework.web: INFO
org.hibernate: ERROR
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/rest-api-demo.log
# 自定义配置
custom:
api:
prefix: /api
version: v1
pagination:
default-page-size: 20
max-page-size: 100
src/main/resources/application-dev.yml
# 开发环境配置
spring:
profiles: dev
app:
version: 1.0.0-dev
logging:
level:
com.example.restapidemo: DEBUG
org.springframework.web: DEBUG
# 开发环境启用更详细的错误信息
server:
error:
include-stacktrace: always
include-message: always
五、原理解释
5.1 @RestController 注解原理
@RestController 是一个组合注解,其源码如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
String value() default "";
}
核心机制:
- @Controller:标记该类为 Spring MVC 控制器,使其能够被组件扫描自动检测并注册为 Spring Bean
- @ResponseBody:指示该控制器的所有方法的返回值都应该直接写入 HTTP 响应体,而不是通过视图解析器解析为视图
5.2 @RequestMapping 注解原理
@RequestMapping 是最基础的映射注解,它可以标注在类级别和方法级别:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
映射匹配过程:
- 路径匹配:将请求的 URL 与注解中指定的路径进行匹配
- 方法匹配:检查 HTTP 请求方法(GET、POST 等)是否与
method()属性匹配 - 参数匹配:检查请求参数是否满足
params()条件 - 头部匹配:检查请求头是否满足
headers()条件 - 内容协商:根据
consumes()和produces()确定请求/响应的内容类型
5.3 Spring MVC 请求处理流程
┌──────────────┐ HTTP Request ┌──────────────────┐
│ 浏览器 │ ──────────────────▶ │ DispatcherServlet │
└──────────────┘ └──────────────────┘
│
▼
┌──────────────────┐ 查找 Handler ┌──────────────────┐
│ HandlerMapping │ ◀───────────────── │ 根据 URL 匹配 │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
┌──────────────────┐ 调用 Handler ┌──────────────────┐
│ HandlerAdapter │ ──────────────────▶ │ @RestController │
│ (如: │ │ 方法执行 │
│ RequestMappingHandlerAdapter) └──────────────────┘
│ │
▼ ▼
┌──────────────────┐ 处理返回值 ┌──────────────────┐
│HttpMessageConverter│ ◀───────────────── │ @ResponseBody │
│ (JSON 转换器) │ │ 序列化对象 │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
┌──────────────┐ HTTP Response ┌──────────────────┐
│ 浏览器 │ ◀───────────────── │ JSON 响应体 │
└──────────────┘ └──────────────────┘
5.4 方法参数解析原理
Spring MVC 通过一系列 HandlerMethodArgumentResolver 实现来处理不同类型的参数:
- @PathVariable:
PathVariableMethodArgumentResolver - @RequestParam:
RequestParamMethodArgumentResolver - @RequestBody:
RequestResponseBodyMethodProcessor - @RequestHeader:
RequestHeaderMethodArgumentResolver
六、核心特性
6.1 请求映射的核心特性
- HTTP 方法限定:通过
method属性或专用注解限定请求方法 - 路径模板:支持
{variable}语法提取路径参数 - 正则表达式:路径变量支持正则表达式约束
- 通配符匹配:支持
*和**通配符 - 内容协商:通过
produces和consumes指定媒体类型
6.2 参数绑定的核心特性
- 路径变量绑定:
@PathVariable - 请求参数绑定:
@RequestParam - 请求体绑定:
@RequestBody - 请求头绑定:
@RequestHeader - Cookie 绑定:
@CookieValue - 表单数据绑定:
@ModelAttribute
6.3 响应处理的核心特性
- @ResponseBody:方法返回值直接写入响应体
- HttpStatus:通过
ResponseEntity设置 HTTP 状态码 - 内容协商:自动根据 Accept 头选择响应格式
- 异常处理:通过
@ExceptionHandler统一处理异常
七、原理流程图以及原理解释
7.1 注解处理流程图
┌──────────────┐ 组件扫描 ┌──────────────────┐
│ @RestController │ ─────────▶ │ Spring 容器注册 │
└──────────────┘ │ Controller Bean │
└──────────────────┘
│
▼
┌──────────────────┐ HTTP 请求 │──────────────┐
│ DispatcherServlet│ ─────────▶ │ 路径匹配 │
└──────────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────────┐ 找到匹配 │──────────────┐
│ HandlerMapping │ ─────────▶ │ @RequestMapping│
│ (RequestMappingHandlerMapping)│ 方法 │
└──────────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────────┐ 参数解析 │──────────────┐
│ HandlerAdapter │ ─────────▶ │ 参数解析器 │
│ (执行目标方法) │ │ (ArgumentResolver)│
└──────────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────────┐ 返回值处理 │──────────────┐
│ HttpMessageConverter│ ◀──────── │ @ResponseBody │
│ (序列化为 JSON) │ │ 处理 │
└──────────────────┘ └──────────────┘
7.2 请求处理时序图
Client ──HTTP Request──▶ DispatcherServlet
│ │
│ ├─► HandlerMapping.getHandler()
│ │ └─► 匹配 @RequestMapping
│ │
│ ├─► HandlerAdapter.handle()
│ │ ├─► 参数解析 (resolveArgument)
│ │ ├─► 调用 Controller 方法
│ │ └─► 处理返回值 (handleReturnValue)
│ │ └─► HttpMessageConverter.write()
│ │
│ HTTP Response ◀──┘
│
└─► 渲染 JSON 响应
八、环境准备
8.1 开发环境要求
- JDK: 11+(推荐 JDK 17 LTS)
- Maven: 3.6+
- IDE: IntelliJ IDEA Ultimate 或 Eclipse
- Postman: API 测试工具
8.2 Maven 配置优化
~/.m2/settings.xml 优化配置:
<settings>
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>jdk11</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>11</jdk>
</activation>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</profile>
</profiles>
</settings>
8.3 项目初始化步骤
# 1. 创建项目目录
mkdir rest-api-demo && cd rest-api-demo
# 2. 复制上面的 pom.xml 内容到 pom.xml
# 3. 创建包结构
mkdir -p src/main/java/com/example/restapidemo/{controller,model,dto,service,service/impl,exception,config}
mkdir -p src/main/resources
# 4. 复制各个 Java 文件到对应目录
# 5. 复制配置文件
# 6. 编译项目
mvn clean compile
# 7. 运行测试
mvn test
# 8. 启动应用
mvn spring-boot:run
九、实际详细应用代码示例实现(补充)
9.1 产品控制器
src/main/java/com/example/restapidemo/controller/ProductController.java
package com.example.restapidemo.controller;
import com.example.restapidemo.model.ApiResponse;
import com.example.restapidemo.model.Product;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 产品控制器
* 演示更复杂的业务逻辑和参数验证
*/
@RestController
@RequestMapping("/api/products")
@Tag(name = "产品管理", description = "产品资源的 CRUD 操作")
public class ProductController {
private final ConcurrentHashMap<Long, Product> productDatabase = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public ProductController() {
initializeTestData();
}
private void initializeTestData() {
Product product1 = Product.builder()
.id(idGenerator.getAndIncrement())
.name("iPhone 14 Pro")
.description("苹果最新旗舰手机")
.price(new BigDecimal("7999.00"))
.stock(100)
.category("手机")
.sku("IPHONE14PRO-001")
.isActive(true)
.build();
Product product2 = Product.builder()
.id(idGenerator.getAndIncrement())
.name("MacBook Air M2")
.description("搭载 M2 芯片的 MacBook Air")
.price(new BigDecimal("9499.00"))
.stock(50)
.category("笔记本")
.sku("MBA-M2-001")
.isActive(true)
.build();
productDatabase.put(product1.getId(), product1);
productDatabase.put(product2.getId(), product2);
}
/**
* GET /api/products - 获取所有产品
*/
@Operation(summary = "获取所有产品", description = "返回所有产品的列表")
@GetMapping
public ResponseEntity<ApiResponse<List<Product>>> getAllProducts() {
List<Product> products = new ArrayList<>(productDatabase.values());
return ResponseEntity.ok(ApiResponse.success(products, "获取产品列表成功"));
}
/**
* GET /api/products/{id} - 根据 ID 获取产品
*/
@Operation(summary = "根据ID获取产品", description = "根据产品ID返回详细信息")
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Product>> getProductById(@PathVariable Long id) {
Product product = productDatabase.get(id);
if (product != null) {
return ResponseEntity.ok(ApiResponse.success(product));
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("产品不存在: " + id));
}
}
/**
* GET /api/products/search - 搜索产品
*/
@Operation(summary = "搜索产品", description = "根据条件搜索产品")
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<Product>>> searchProducts(
@RequestParam(required = false) String name,
@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice) {
List<Product> filteredProducts = productDatabase.values().stream()
.filter(product -> name == null || product.getName().contains(name))
.filter(product -> category == null || product.getCategory().equals(category))
.filter(product -> minPrice == null || product.getPrice().compareTo(minPrice) >= 0)
.filter(product -> maxPrice == null || product.getPrice().compareTo(maxPrice) <= 0)
.toList();
return ResponseEntity.ok(ApiResponse.success(filteredProducts,
String.format("搜索完成,共找到 %d 个产品", filteredProducts.size())));
}
/**
* POST /api/products - 创建产品
*/
@Operation(summary = "创建产品", description = "创建一个新的产品")
@PostMapping
public ResponseEntity<ApiResponse<Product>> createProduct(@Valid @RequestBody Product product) {
// 检查 SKU 是否已存在
boolean skuExists = productDatabase.values().stream()
.anyMatch(p -> p.getSku() != null && p.getSku().equals(product.getSku()));
if (skuExists) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error("SKU 已存在: " + product.getSku()));
}
// 设置 ID 和时间戳
product.setId(idGenerator.getAndIncrement());
product.setCreatedAt(java.time.LocalDateTime.now());
product.setUpdatedAt(java.time.LocalDateTime.now());
productDatabase.put(product.getId(), product);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(product, "产品创建成功"));
}
/**
* PUT /api/products/{id} - 更新产品
*/
@Operation(summary = "更新产品", description = "更新指定产品的完整信息")
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<Product>> updateProduct(
@PathVariable Long id, @Valid @RequestBody Product product) {
if (!productDatabase.containsKey(id)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("产品不存在: " + id));
}
// 检查 SKU 是否被其他产品使用
boolean skuConflict = productDatabase.values().stream()
.anyMatch(p -> !p.getId().equals(id) &&
p.getSku() != null && p.getSku().equals(product.getSku()));
if (skuConflict) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error("SKU 已被其他产品使用: " + product.getSku()));
}
// 保留原有的创建时间和 ID
Product existingProduct = productDatabase.get(id);
product.setId(id);
product.setCreatedAt(existingProduct.getCreatedAt());
product.setUpdatedAt(java.time.LocalDateTime.now());
productDatabase.put(id, product);
return ResponseEntity.ok(ApiResponse.success(product, "产品更新成功"));
}
/**
* PATCH /api/products/{id}/stock - 更新库存
*/
@Operation(summary = "更新库存", description = "部分更新产品库存")
@PatchMapping("/{id}/stock")
public ResponseEntity<ApiResponse<Product>> updateStock(
@PathVariable Long id, @RequestParam Integer quantity) {
Product product = productDatabase.get(id);
if (product == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("产品不存在: " + id));
}
int newStock = product.getStock() + quantity;
if (newStock < 0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error("库存不足"));
}
product.setStock(newStock);
product.setUpdatedAt(java.time.LocalDateTime.now());
return ResponseEntity.ok(ApiResponse.success(product, "库存更新成功"));
}
/**
* DELETE /api/products/{id} - 删除产品
*/
@Operation(summary = "删除产品", description = "删除指定的产品")
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteProduct(@PathVariable Long id) {
if (productDatabase.containsKey(id)) {
productDatabase.remove(id);
return ResponseEntity.ok(ApiResponse.success(null, "产品删除成功"));
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("产品不存在: " + id));
}
}
}
9.2 高级参数绑定示例
src/main/java/com/example/restapidemo/controller/AdvancedController.java
package com.example.restapidemo.controller;
import com.example.restapidemo.model.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
* 高级参数绑定控制器
* 演示各种复杂的参数绑定场景
*/
@RestController
@RequestMapping("/api/advanced")
@Tag(name = "高级参数", description = "演示各种高级参数绑定技巧")
public class AdvancedController {
/**
* 演示日期时间参数绑定
*/
@Operation(summary = "日期时间参数", description = "演示日期时间参数的绑定")
@GetMapping("/datetime")
public ApiResponse<String> dateTimeParams(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime dateTime) {
String result = String.format("日期: %s, 日期时间: %s", date, dateTime);
return ApiResponse.success(result);
}
/**
* 演示数组和列表参数绑定
*/
@Operation(summary = "数组列表参数", description = "演示数组和列表参数的绑定")
@GetMapping("/list")
public ApiResponse<List<String>> listParams(
@RequestParam List<String> tags,
@RequestParam String[] categories) {
List<String> result = Arrays.asList(
"Tags: " + String.join(", ", tags),
"Categories: " + String.join(", ", categories)
);
return ApiResponse.success(result);
}
/**
* 演示请求头参数绑定
*/
@Operation(summary = "请求头参数", description = "演示请求头参数的绑定")
@GetMapping("/header")
public ApiResponse<HttpHeaders> headerParams(@RequestHeader HttpHeaders headers) {
String userAgent = headers.getFirst("User-Agent");
String contentType = headers.getFirst("Content-Type");
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("X-Custom-Header", "custom-value");
return ApiResponse.success(responseHeaders,
String.format("User-Agent: %s, Content-Type: %s", userAgent, contentType));
}
/**
* 演示 Cookie 参数绑定
*/
@Operation(summary = "Cookie 参数", description = "演示 Cookie 参数的绑定")
@GetMapping("/cookie")
public ApiResponse<String> cookieParams(@CookieValue String sessionId) {
return ApiResponse.success("Session ID: " + sessionId);
}
/**
* 演示矩阵变量(Matrix Variables)
*/
@Operation(summary = "矩阵变量", description = "演示矩阵变量的使用")
@GetMapping("/matrix/{path}")
public ApiResponse<String> matrixVariables(
@PathVariable String path,
@MatrixVariable String color,
@MatrixVariable Integer weight) {
String result = String.format("Path: %s, Color: %s, Weight: %d", path, color, weight);
return ApiResponse.success(result);
}
/**
* 演示 Servlet API 参数
*/
@Operation(summary = "Servlet API", description = "演示 Servlet API 参数的使用")
@GetMapping("/servlet")
public ApiResponse<String> servletApi(HttpServletRequest request) {
String clientIp = request.getRemoteAddr();
String method = request.getMethod();
String requestUri = request.getRequestURI();
String result = String.format("Client IP: %s, Method: %s, URI: %s",
clientIp, method, requestUri);
return ApiResponse.success(result);
}
/**
* 演示响应头设置
*/
@Operation(summary = "响应头设置", description = "演示如何设置响应头")
@GetMapping("/response-header")
public ResponseEntity<ApiResponse<String>> responseHeaders() {
ApiResponse<String> response = ApiResponse.success("响应头示例");
return ResponseEntity.ok()
.header("X-API-Version", "1.0")
.header("X-Rate-Limit", "100")
.header("Cache-Control", "no-cache")
.body(response);
}
}
十、运行结果
10.1 应用启动日志
$ mvn spring-boot:run
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.14)
2024-01-20 15:30:00.123 INFO 12345 --- [ main] c.e.restapidemo.RestApiDemoApplication : Starting RestApiDemoApplication using Java 11.0.16 on mycomputer with PID 12345
2024-01-20 15:30:00.125 INFO 12345 --- [ main] c.e.restapidemo.RestApiDemoApplication : No active profile set, falling back to 1 default profile: "default"
2024-01-20 15:30:01.234 INFO 12345 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2024-01-20 15:30:01.345 INFO 12345 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-01-20 15:30:01.346 INFO 12345 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.65]
2024-01-20 15:30:01.456 INFO 12345 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-01-20 15:30:01.457 INFO 12345 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1456 ms
2024-01-20 15:30:02.123 INFO 12345 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2024-01-20 15:30:02.124 INFO 12345 --- [ main] c.e.restapidemo.RestApiDemoApplication : Started RestApiDemoApplication in 2.456 seconds (JVM running for 3.123)
10.2 API 调用示例与响应
10.2.1 基础 API 测试
# 1. 获取应用信息
$ curl -X GET http://localhost:8080/api/info
{
"success": true,
"message": "获取应用信息成功",
"data": {
"name": "REST API Demo",
"version": "1.0.0",
"author": "Spring Boot Team",
"description": "深入理解 @RestController 与 @RequestMapping"
},
"timestamp": "2024-01-20T07:30:15.123",
"path": null
}
# 2. 个性化问候
$ curl -X GET http://localhost:8080/api/greeting/World
{
"success": true,
"message": "操作成功",
"data": "Hello, World! Welcome to Spring Boot REST API!",
"timestamp": "2024-01-20T07:30:20.456",
"path": null
}
# 3. 数学运算
$ curl -X GET http://localhost:8080/api/math/add/10/20
{
"success": true,
"message": "计算完成",
"data": {
"operation": "10 + 20",
"result": 30,
"timestamp": "1705747820123"
},
"timestamp": "2024-01-20T07:30:25.789",
"path": null
}
10.2.2 用户 API 测试
# 1. 获取所有用户
$ curl -X GET http://localhost:8080/api/users
{
"success": true,
"message": "获取用户列表成功",
"data": [
{
"id": 1,
"username": "zhangsan",
"email": "zhangsan@example.com",
"fullName": "张三",
"age": 28,
"status": "ACTIVE",
"createdAt": "2024-01-20T07:30:02.123",
"updatedAt": "2024-01-20T07:30:02.123"
},
{
"id": 2,
"username": "lisi",
"email": "lisi@example.com",
"fullName": "李四",
"age": 32,
"status": "ACTIVE",
"createdAt": "2024-01-20T07:30:02.123",
"updatedAt": "2024-01-20T07:30:02.123"
}
],
"timestamp": "2024-01-20T07:35:00.123",
"path": null
}
# 2. 创建新用户
$ curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"username": "wangwu",
"email": "wangwu@example.com",
"password": "password789",
"fullName": "王五",
"age": 25
}'
{
"success": true,
"message": "用户创建成功",
"data": {
"id": 3,
"username": "wangwu",
"email": "wangwu@example.com",
"fullName": "王五",
"age": 25,
"status": "ACTIVE",
"createdAt": "2024-01-20T07:36:00.123",
"updatedAt": "2024-01-20T07:36:00.123"
},
"timestamp": "2024-01-20T07:36:00.123",
"path": null
}
# 3. 根据 ID 获取用户
$ curl -X GET http://localhost:8080/api/users/1
{
"success": true,
"message": "获取用户信息成功",
"data": {
"id": 1,
"username": "zhangsan",
"email": "zhangsan@example.com",
"fullName": "张三",
"age": 28,
"status": "ACTIVE",
"createdAt": "2024-01-20T07:30:02.123",
"updatedAt": "2024-01-20T07:30:02.123"
},
"timestamp": "2024-01-20T07:37:00.123",
"path": null
}
# 4. 更新用户状态
$ curl -X PATCH "http://localhost:8080/api/users/1/status?status=SUSPENDED"
{
"success": true,
"message": "用户状态更新成功",
"data": {
"id": 1,
"status": "SUSPENDED",
"updatedAt": "1705748220456"
},
"timestamp": "2024-01-20T07:37:00.456",
"path": null
}
# 5. 搜索用户
$ curl -X GET "http://localhost:8080/api/users/search?minAge=25&maxAge=30"
{
"success": true,
"message": "搜索完成,共找到 2 个用户",
"data": [
{
"id": 1,
"username": "zhangsan",
"email": "zhangsan@example.com",
"fullName": "张三",
"age": 28,
"status": "SUSPENDED",
"createdAt": "2024-01-20T07:30:02.123",
"updatedAt": "2024-01-20T07:37:00.456"
},
{
"id": 3,
"username": "wangwu",
"email": "wangwu@example.com",
"fullName": "王五",
"age": 25,
"status": "ACTIVE",
"createdAt": "2024-01-20T07:36:00.123",
"updatedAt": "2024-01-20T07:36:00.123"
}
],
"timestamp": "2024-01-20T07:38:00.123",
"path": null
}
10.2.3 错误处理测试
# 1. 参数验证失败
$ curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"username": "ab", # 用户名太短
"email": "invalid-email", # 邮箱格式错误
"password": "123", # 密码太短
"fullName": "", # 姓名为空
"age": -5 # 年龄无效
}'
{
"success": false,
"message": "参数验证失败",
"data": {
"username": "用户名长度必须在 2-50 个字符之间",
"email": "邮箱格式不正确",
"password": "密码长度不能少于 6 个字符",
"fullName": "姓名不能为空",
"age": "年龄必须大于 0"
},
"timestamp": "2024-01-20T07:40:00.123",
"path": null
}
# 2. 资源不存在
$ curl -X GET http://localhost:8080/api/users/999
{
"success": false,
"message": "用户不存在: 999",
"data": null,
"timestamp": "2024-01-20T07:41:00.123",
"path": null
}
十一、测试步骤以及详细代码
11.1 单元测试
src/test/java/com/example/restapidemo/controller/UserControllerTest.java
package com.example.restapidemo.controller;
import com.example.restapidemo.dto.CreateUserRequest;
import com.example.restapidemo.dto.UserDto;
import com.example.restapidemo.model.User;
import com.example.restapidemo.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(MockitoExtension.class)
class UserControllerTest {
private MockMvc mockMvc;
private ObjectMapper objectMapper = new ObjectMapper();
@Mock
private UserService userService;
@InjectMocks
private UserController userController;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
@Test
void testGetAllUsers() throws Exception {
// 准备测试数据
UserDto user1 = UserDto.builder()
.id(1L)
.username("zhangsan")
.email("zhangsan@example.com")
.fullName("张三")
.age(28)
.build();
UserDto user2 = UserDto.builder()
.id(2L)
.username("lisi")
.email("lisi@example.com")
.fullName("李四")
.age(32)
.build();
List<UserDto> users = Arrays.asList(user1, user2);
// Mock 服务层调用
when(userService.getAllUsers()).thenReturn(users);
// 执行测试
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.message").value("获取用户列表成功"))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(2))
.andExpect(jsonPath("$.data[0].username").value("zhangsan"))
.andExpect(jsonPath("$.data[1].username").value("lisi"));
// 验证服务层调用
verify(userService, times(1)).getAllUsers();
}
@Test
void testGetUserById_Success() throws Exception {
// 准备测试数据
UserDto user = UserDto.builder()
.id(1L)
.username("zhangsan")
.email("zhangsan@example.com")
.fullName("张三")
.age(28)
.build();
// Mock 服务层调用
when(userService.getUserById(1L)).thenReturn(Optional.of(user));
// 执行测试
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.message").value("获取用户信息成功"))
.andExpect(jsonPath("$.data.username").value("zhangsan"))
.andExpect(jsonPath("$.data.email").value("zhangsan@example.com"));
verify(userService, times(1)).getUserById(1L);
}
@Test
void testGetUserById_NotFound() throws Exception {
// Mock 服务层调用
when(userService.getUserById(999L)).thenReturn(Optional.empty());
// 执行测试
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").value("用户不存在: 999"));
verify(userService, times(1)).getUserById(999L);
}
@Test
void testCreateUser_Success() throws Exception {
// 准备请求数据
CreateUserRequest request = new CreateUserRequest();
request.setUsername("wangwu");
request.setEmail("wangwu@example.com");
request.setPassword("password789");
request.setFullName("王五");
request.setAge(25);
// 准备响应数据
UserDto createdUser = UserDto.builder()
.id(3L)
.username("wangwu")
.email("wangwu@example.com")
.fullName("王五")
.age(25)
.build();
// Mock 服务层调用
when(userService.createUser(any(CreateUserRequest.class))).thenReturn(createdUser);
// 执行测试
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.message").value("用户创建成功"))
.andExpect(jsonPath("$.data.id").value(3))
.andExpect(jsonPath("$.data.username").value("wangwu"));
verify(userService, times(1)).createUser(any(CreateUserRequest.class));
}
@Test
void testCreateUser_BadRequest() throws Exception {
// 准备无效的请求数据
CreateUserRequest request = new CreateUserRequest();
request.setUsername("ab"); // 太短
request.setEmail("invalid-email"); // 格式错误
request.setPassword("123"); // 太短
request.setFullName(""); // 为空
request.setAge(-5); // 无效
// Mock 服务层抛出异常
when(userService.createUser(any(CreateUserRequest.class)))
.thenThrow(new IllegalArgumentException("用户名长度必须在 2-50 个字符之间"));
// 执行测试
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").value("用户名长度必须在 2-50 个字符之间"));
verify(userService, times(1)).createUser(any(CreateUserRequest.class));
}
@Test
void testUpdateUserStatus() throws Exception {
// 准备响应数据
UserDto updatedUser = UserDto.builder()
.id(1L)
.username("zhangsan")
.status(User.Status.SUSPENDED)
.build();
// Mock 服务层调用
when(userService.updateUserStatus(eq(1L), eq(User.Status.SUSPENDED)))
.thenReturn(updatedUser);
// 执行测试
mockMvc.perform(patch("/api/users/1/status")
.param("status", "SUSPENDED"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.message").value("用户状态更新成功"))
.andExpect(jsonPath("$.data.status").value("SUSPENDED"));
verify(userService, times(1)).updateUserStatus(eq(1L), eq(User.Status.SUSPENDED));
}
@Test
void testDeleteUser_Success() throws Exception {
// Mock 服务层调用
doNothing().when(userService).deleteUser(1L);
// 执行测试
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.message").value("用户删除成功"));
verify(userService, times(1)).deleteUser(1L);
}
@Test
void testDeleteUser_NotFound() throws Exception {
// Mock 服务层抛出异常
doThrow(new IllegalArgumentException("用户不存在: 999"))
.when(userService).deleteUser(999L);
// 执行测试
mockMvc.perform(delete("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").value("用户不存在: 999"));
verify(userService, times(1)).deleteUser(999L);
}
}
11.2 集成测试
src/test/java/com/example/restapidemo/RestApiDemoIntegrationTest.java
package com.example.restapidemo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("dev")
class RestApiDemoIntegrationTest {
@LocalServerPort
private int port;
private TestRestTemplate restTemplate = new TestRestTemplate();
@Test
void contextLoads() {
// 测试应用上下文是否正常加载
assertThat(port).isGreaterThan(0);
}
@Test
void testHealthCheck() {
// 测试健康检查端点
String url = String.format("http://localhost:%d/api/health", port);
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("UP");
}
@Test
void testGetAppInfo() {
// 测试应用信息端点
String url = String.format("http://localhost:%d/api/info", port);
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("REST API Demo");
assertThat(response.getBody()).contains("1.0.0");
}
@Test
void testCreateAndGetUser() {
// 测试创建用户
String createUrl = String.format("http://localhost:%d/api/users", port);
String userJson = """
{
"username": "testuser",
"email": "test@example.com",
"password": "password123",
"fullName": "测试用户",
"age": 30
}
""";
ResponseEntity<String> createResponse = restTemplate.postForEntity(
createUrl, userJson, String.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).contains("用户创建成功");
assertThat(createResponse.getBody()).contains("testuser");
// 注意:由于我们使用的是模拟服务,这里只是验证 API 调用成功
// 实际应用中可以从响应中提取 ID 并进行后续测试
}
}
11.3 测试脚本
scripts/api-test.sh
#!/bin/bash
# API 测试脚本
BASE_URL="http://localhost:8080"
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo "🧪 开始 API 测试..."
# 测试函数
test_endpoint() {
local method=$1
local endpoint=$2
local data=$3
local expected_status=$4
local description=$5
echo -e "\n${description}"
echo "Testing: $method $endpoint"
if [ -n "$data" ]; then
response=$(curl -s -w "\n%{http_code}" -X $method "$BASE_URL$endpoint" \
-H "Content-Type: application/json" \
-d "$data")
else
response=$(curl -s -w "\n%{http_code}" -X $method "$BASE_URL$endpoint")
fi
# 分离响应体和状态码
body=$(echo "$response" | head -n -1)
status=$(echo "$response" | tail -n 1)
if [ "$status" -eq "$expected_status" ]; then
echo -e "${GREEN}✅ PASS${NC} - Status: $status"
echo "Response: $body"
else
echo -e "${RED}❌ FAIL${NC} - Expected: $expected_status, Got: $status"
echo "Response: $body"
return 1
fi
}
# 健康检查
test_endpoint "GET" "/api/health" "" 200 "1. 健康检查"
# 应用信息
test_endpoint "GET" "/api/info" "" 200 "2. 获取应用信息"
# 个性化问候
test_endpoint "GET" "/api/greeting/TestUser" "" 200 "3. 个性化问候"
# 数学运算
test_endpoint "GET" "/api/math/add/5/3" "" 200 "4. 数学运算"
# 获取所有用户
test_endpoint "GET" "/api/users" "" 200 "5. 获取所有用户"
# 创建用户
create_user_data='{
"username": "apitest",
"email": "apitest@example.com",
"password": "password123",
"fullName": "API 测试用户",
"age": 28
}'
test_endpoint "POST" "/api/users" "$create_user_data" 201 "6. 创建用户"
# 获取单个用户
test_endpoint "GET" "/api/users/1" "" 200 "7. 获取单个用户"
# 更新用户状态
test_endpoint "PATCH" "/api/users/1/status?status=SUSPENDED" "" 200 "8. 更新用户状态"
# 搜索用户
test_endpoint "GET" "/api/users/search?minAge=25&maxAge=35" "" 200 "9. 搜索用户"
# 参数验证测试(应该失败)
invalid_user_data='{
"username": "a",
"email": "invalid",
"password": "123",
"fullName": "",
"age": -1
}'
test_endpoint "POST" "/api/users" "$invalid_user_data" 400 "10. 参数验证测试(期望失败)"
# 资源不存在测试(应该失败)
test_endpoint "GET" "/api/users/999" "" 404 "11. 资源不存在测试(期望失败)"
echo -e "\n🎉 API 测试完成!"
运行测试脚本:
chmod +x scripts/api-test.sh
./scripts/api-test.sh
十二、部署场景
12.1 传统服务器部署
12.1.1 Systemd 服务配置
/etc/systemd/system/rest-api-demo.service
[Unit]
Description=Spring Boot REST API Demo
After=network.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/rest-api-demo
ExecStart=/usr/bin/java -Xmx512m -jar rest-api-demo-1.0.0.jar --spring.profiles.active=prod
SuccessExitStatus=143
Restart=always
RestartSec=10
# 日志配置
StandardOutput=journal
StandardError=journal
SyslogIdentifier=rest-api-demo
# 安全配置
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/rest-api-demo/logs
# 环境变量
Environment=SPRING_PROFILES_ACTIVE=prod
Environment=JAVA_OPTS=-Xmx512m
[Install]
WantedBy=multi-user.target
12.1.2 部署脚本
scripts/deploy.sh
#!/bin/bash
set -e
APP_NAME="rest-api-demo"
APP_VERSION="1.0.0"
APP_DIR="/opt/$APP_NAME"
SERVICE_NAME="$APP_NAME"
JAR_FILE="target/$APP_NAME-$APP_VERSION.jar"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${YELLOW}🚀 开始部署 $APP_NAME...${NC}"
# 检查 JAR 文件
if [ ! -f "$JAR_FILE" ]; then
echo -e "${RED}❌ JAR 文件不存在: $JAR_FILE${NC}"
echo "请先运行: mvn clean package"
exit 1
fi
# 创建应用目录
echo -e "${YELLOW}📁 创建应用目录...${NC}"
sudo mkdir -p $APP_DIR
sudo mkdir -p $APP_DIR/logs
# 停止现有服务
if sudo systemctl is-active --quiet $SERVICE_NAME; then
echo -e "${YELLOW}⏹️ 停止现有服务...${NC}"
sudo systemctl stop $SERVICE_NAME
fi
# 备份现有版本
if [ -f "$APP_DIR/$APP_NAME-$APP_VERSION.jar" ]; then
echo -e "${YELLOW}💾 备份现有版本...${NC}"
sudo cp "$APP_DIR/$APP_NAME-$APP_VERSION.jar" "$APP_DIR/$APP_NAME-$APP_VERSION.jar.backup.$(date +%Y%m%d_%H%M%S)"
fi
# 复制新版本
echo -e "${YELLOW}📦 复制新版本...${NC}"
sudo cp $JAR_FILE $APP_DIR/
sudo chown appuser:appuser $APP_DIR/*.jar
sudo chmod 755 $APP_DIR/*.jar
# 复制 systemd 服务文件
if [ -f "packaging-demo.service" ]; then
echo -e "${YELLOW}⚙️ 配置系统服务...${NC}"
sudo cp packaging-demo.service /etc/systemd/system/$SERVICE_NAME.service
fi
# 重载 systemd
echo -e "${YELLOW}🔄 重载系统服务配置...${NC}"
sudo systemctl daemon-reload
# 启用并启动服务
echo -e "${YELLOW}▶️ 启动服务...${NC}"
sudo systemctl enable $SERVICE_NAME
sudo systemctl start $SERVICE_NAME
# 等待服务启动
sleep 10
# 检查服务状态
if sudo systemctl is-active --quiet $SERVICE_NAME; then
echo -e "${GREEN}✅ 部署成功!服务正在运行${NC}"
sudo systemctl status $SERVICE_NAME --no-pager
else
echo -e "${RED}❌ 部署失败!服务启动失败${NC}"
sudo journalctl -u $SERVICE_NAME --no-pager -l
exit 1
fi
# 健康检查
echo -e "${YELLOW}🩺 执行健康检查...${NC}"
sleep 5
if curl -f http://localhost:8080/api/health; then
echo -e "${GREEN}✅ 健康检查通过${NC}"
else
echo -e "${RED}❌ 健康检查失败${NC}"
exit 1
fi
echo -e "${GREEN}🎉 部署完成!${NC}"
echo "服务访问地址: http://localhost:8080"
echo "查看日志: sudo journalctl -u $SERVICE_NAME -f"
12.2 Docker 容器部署
12.2.1 Dockerfile
# 多阶段构建
FROM eclipse-temurin:11-jre-jammy AS base
WORKDIR /app
EXPOSE 8080
FROM maven:3.8.6-openjdk-11 AS build
WORKDIR /workspace
# 复制 POM 文件
COPY pom.xml .
COPY src ./src
# 构建应用
RUN mvn clean package -DskipTests
# 生产阶段
FROM base AS production
VOLUME /tmp
# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 复制 JAR 文件
COPY --from=build /workspace/target/rest-api-demo-1.0.0.jar app.jar
# 设置权限
RUN chown appuser:appuser app.jar && chmod 755 app.jar
# 切换到非 root 用户
USER appuser
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/api/health || exit 1
# 启动应用
ENTRYPOINT ["java", "-Xmx512m", "-XX:+UseContainerSupport", "-jar", "app.jar"]
12.2.2 Docker Compose 配置
docker-compose.yml
version: '3.8'
services:
rest-api-demo:
build:
context: .
dockerfile: Dockerfile
image: rest-api-demo:1.0.0
container_name: rest-api-demo-app
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- JAVA_OPTS=-Xmx512m
volumes:
- app-logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
restart: unless-stopped
deploy:
resources:
limits:
memory: 1G
cpus: '0.5'
reservations:
memory: 512M
cpus: '0.25'
volumes:
app-logs:
12.2.3 Kubernetes 部署
k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: rest-api-demo
labels:
app: rest-api-demo
spec:
replicas: 3
selector:
matchLabels:
app: rest-api-demo
template:
metadata:
labels:
app: rest-api-demo
spec:
containers:
- name: rest-api-demo
image: rest-api-demo:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_OPTS
value: "-Xmx512m -XX:+UseContainerSupport"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
volumeMounts:
- name: app-logs
mountPath: /app/logs
volumes:
- name: app-logs
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: rest-api-demo-service
spec:
selector:
app: rest-api-demo
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rest-api-demo-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
rules:
- host: api.example.com
http:
paths:
- path: /api/(.*)
pathType: Prefix
backend:
service:
name: rest-api-demo-service
port:
number: 80
12.3 云平台部署
12.3.1 AWS Elastic Beanstalk 配置
.ebextensions/01-java.config
option_settings:
aws:elasticbeanstalk:container:java:
JVM Options: '-Xmx512m -XX:+UseContainerSupport'
MemoryReservation: 512
aws:elasticbeanstalk:application:environment:
SPRING_PROFILES_ACTIVE: prod
JAVA_OPTS: '-Xmx512m'
12.3.2 阿里云 EDAS 配置
Dockerfile(用于EDAS容器服务)
FROM registry.cn-hangzhou.aliyuncs.com/edas-java/jdk:11
MAINTAINER Your Name "your.email@example.com"
ADD target/rest-api-demo-1.0.0.jar app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]
EXPOSE 8080
十三、疑难解答
13.1 常见问题及解决方案
13.1.1 404 Not Found 错误
问题描述:访问 API 时返回 404 错误。
可能原因:
- 控制器类未添加
@RestController注解 - 请求路径与
@RequestMapping配置不匹配 - 应用上下文路径配置错误
解决方案:
// 检查控制器注解
@RestController // 确保添加了此注解
@RequestMapping("/api/users")
public class UserController {
// ...
}
// 检查应用配置
server:
servlet:
context-path: / # 默认为空,如有配置需包含在请求路径中
13.1.2 405 Method Not Allowed 错误
问题描述:请求方法不被允许(如使用 GET 访问只支持 POST 的端点)。
可能原因:
- HTTP 方法与
@RequestMapping的method属性不匹配 - 使用了错误的 HTTP 方法
解决方案:
// 明确指定支持的 HTTP 方法
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping // 只允许 POST 方法
public ResponseEntity<?> createUser() {
// ...
}
// 或者使用更具体的注解
@GetMapping
public ResponseEntity<?> getUsers() {
// ...
}
}
13.1.3 参数绑定失败
问题描述:请求参数无法正确绑定到方法参数。
可能原因:
- 缺少
@RequestBody、@RequestParam等注解 - 参数类型不匹配
- 缺少验证注解的依赖
解决方案:
// 确保添加正确的注解
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) { // @RequestBody 注解
// ...
}
// 检查依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
13.1.4 JSON 序列化/反序列化错误
问题描述:JSON 数据无法正确转换。
可能原因:
- 缺少 Jackson 依赖
- 日期格式不匹配
- 循环引用
解决方案:
<!-- 确保包含 Web 依赖(包含 Jackson) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// 配置日期格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
// 处理循环引用
@JsonIgnore
private User createdBy;
13.1.5 CORS 跨域问题
问题描述:前端应用无法访问 API(跨域错误)。
解决方案:
// 方法一:使用 @CrossOrigin 注解
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*") // 允许所有来源
public class UserController {
// ...
}
// 方法二:全局 CORS 配置(推荐)
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://youfrontend.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}
13.2 性能优化建议
- 使用异步处理:
@RestController
public class AsyncController {
@GetMapping("/async")
public Callable<ResponseEntity<?>> asyncMethod() {
return () -> {
// 长时间运行的任务
Thread.sleep(5000);
return ResponseEntity.ok("完成");
};
}
}
- 启用 GZIP 压缩:
server:
compression:
enabled: true
mime-types: application/json,application/xml,text/html,text/css,text/javascript
min-response-size: 1024
- 连接池配置:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
十四、未来展望
14.1 技术趋势
-
GraphQL 集成:
- Spring Boot 对 GraphQL 的原生支持越来越完善
- 替代传统的 REST API,提供更灵活的数据查询
-
响应式编程:
- WebFlux 提供响应式 Web 编程模型
- 更好的并发性能和资源利用率
-
云原生 API 网关:
- Spring Cloud Gateway 成为微服务架构的标准组件
- 统一的路由、过滤、限流、熔断
-
API 安全增强:
- OAuth2.0 / OpenID Connect 成为标配
- JWT、API Key、证书等多种认证方式
-
自动化 API 文档:
- OpenAPI 3.0 规范普及
- Swagger UI、Redoc 等工具集成
14.2 Spring Boot 注解的发展方向
-
组合注解的增强:
- 更多场景化的组合注解
- 减少样板代码
-
声明式 API 设计:
- 基于注解的声明式事务、缓存、重试等
- 函数式编程模型的完善
-
原生镜像支持:
- Spring Boot 3.x 对 GraalVM Native Image 的支持
- 更快的启动速度和更低的内存占用
-
AI 辅助开发:
- 注解的智能提示和自动补全
- 基于代码分析的 API 设计建议
14.3 挑战与机遇
-
微服务治理复杂性:
- 服务拆分粒度的问题
- 分布式事务、一致性保证
-
API 版本管理:
- 向后兼容性的维护
- 平滑升级策略
-
性能与可维护性平衡:
- 过度设计 vs 快速迭代
- 团队技能水平的差异
-
安全威胁应对:
- API 安全攻击防护
- 数据隐私保护合规
十五、总结
通过本文的全面探讨,我们从 @RestController 和 @RequestMapping 的基础概念出发,逐步深入到复杂的 RESTful API 设计与实现,涵盖了从简单的 “Hello World” 到完整的 CRUD 操作、参数验证、异常处理、测试部署等全生命周期的实践。
15.1 核心收获
-
注解的本质理解:
@RestController是@Controller + @ResponseBody的组合@RequestMapping及其派生注解提供了灵活的请求映射机制- 注解驱动开发是 Spring Boot 的核心思想
-
RESTful API 设计原则:
- 资源导向的 URL 设计
- 合适的 HTTP 方法使用
- 统一的响应格式和错误处理
- 版本管理和向后兼容性
-
实践技能提升:
- 完整的项目结构和代码组织
- 参数绑定和验证的最佳实践
- 异常处理和日志记录
- 测试策略和部署方案
15.2 最佳实践总结
- 分层清晰:Controller → Service → Repository 的职责分离
- DTO 模式:使用专门的 DTO 进行输入输出,避免暴露实体类
- 统一响应:采用标准化的 API 响应格式
- 参数验证:充分利用 Bean Validation 注解
- 异常处理:全局异常处理提高代码复用性
- 测试覆盖:单元测试、集成测试、API 测试相结合
- 文档完备:使用 Swagger/OpenAPI 自动生成 API 文档
15.3 进阶学习路径
- 深入 Spring Framework:理解 IoC、AOP、事务管理等核心机制
- 微服务架构:学习 Spring Cloud 生态系统
- 性能优化:JVM 调优、数据库优化、缓存策略
- 云原生技术:Docker、Kubernetes、服务网格
- 安全防护:OAuth2、JWT、API 网关、WAF
@RestController 和 @RequestMapping 只是 Spring Boot Web 开发的起点,但它们体现了 Spring Boot “约定优于配置” 的设计哲学。掌握这些基础注解的原理和用法,将为你在微服务、云原生等领域的深入发展奠定坚实的基础。
记住,优秀的 API 设计不仅要功能正确,更要考虑易用性、可维护性和安全性。持续学习和实践是成为优秀后端开发者的必经之路。
更多推荐




所有评论(0)