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 "";
}

核心机制

  1. @Controller:标记该类为 Spring MVC 控制器,使其能够被组件扫描自动检测并注册为 Spring Bean
  2. @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 {};
}

映射匹配过程

  1. 路径匹配:将请求的 URL 与注解中指定的路径进行匹配
  2. 方法匹配:检查 HTTP 请求方法(GET、POST 等)是否与 method() 属性匹配
  3. 参数匹配:检查请求参数是否满足 params() 条件
  4. 头部匹配:检查请求头是否满足 headers() 条件
  5. 内容协商:根据 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 实现来处理不同类型的参数:

  • @PathVariablePathVariableMethodArgumentResolver
  • @RequestParamRequestParamMethodArgumentResolver
  • @RequestBodyRequestResponseBodyMethodProcessor
  • @RequestHeaderRequestHeaderMethodArgumentResolver

六、核心特性

6.1 请求映射的核心特性

  1. HTTP 方法限定:通过 method 属性或专用注解限定请求方法
  2. 路径模板:支持 {variable} 语法提取路径参数
  3. 正则表达式:路径变量支持正则表达式约束
  4. 通配符匹配:支持 *** 通配符
  5. 内容协商:通过 producesconsumes 指定媒体类型

6.2 参数绑定的核心特性

  1. 路径变量绑定@PathVariable
  2. 请求参数绑定@RequestParam
  3. 请求体绑定@RequestBody
  4. 请求头绑定@RequestHeader
  5. Cookie 绑定@CookieValue
  6. 表单数据绑定@ModelAttribute

6.3 响应处理的核心特性

  1. @ResponseBody:方法返回值直接写入响应体
  2. HttpStatus:通过 ResponseEntity 设置 HTTP 状态码
  3. 内容协商:自动根据 Accept 头选择响应格式
  4. 异常处理:通过 @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 方法与 @RequestMappingmethod 属性不匹配
  • 使用了错误的 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 性能优化建议

  1. 使用异步处理
@RestController
public class AsyncController {
    
    @GetMapping("/async")
    public Callable<ResponseEntity<?>> asyncMethod() {
        return () -> {
            // 长时间运行的任务
            Thread.sleep(5000);
            return ResponseEntity.ok("完成");
        };
    }
}
  1. 启用 GZIP 压缩
server:
  compression:
    enabled: true
    mime-types: application/json,application/xml,text/html,text/css,text/javascript
    min-response-size: 1024
  1. 连接池配置
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000

十四、未来展望

14.1 技术趋势

  1. GraphQL 集成

    • Spring Boot 对 GraphQL 的原生支持越来越完善
    • 替代传统的 REST API,提供更灵活的数据查询
  2. 响应式编程

    • WebFlux 提供响应式 Web 编程模型
    • 更好的并发性能和资源利用率
  3. 云原生 API 网关

    • Spring Cloud Gateway 成为微服务架构的标准组件
    • 统一的路由、过滤、限流、熔断
  4. API 安全增强

    • OAuth2.0 / OpenID Connect 成为标配
    • JWT、API Key、证书等多种认证方式
  5. 自动化 API 文档

    • OpenAPI 3.0 规范普及
    • Swagger UI、Redoc 等工具集成

14.2 Spring Boot 注解的发展方向

  1. 组合注解的增强

    • 更多场景化的组合注解
    • 减少样板代码
  2. 声明式 API 设计

    • 基于注解的声明式事务、缓存、重试等
    • 函数式编程模型的完善
  3. 原生镜像支持

    • Spring Boot 3.x 对 GraalVM Native Image 的支持
    • 更快的启动速度和更低的内存占用
  4. AI 辅助开发

    • 注解的智能提示和自动补全
    • 基于代码分析的 API 设计建议

14.3 挑战与机遇

  1. 微服务治理复杂性

    • 服务拆分粒度的问题
    • 分布式事务、一致性保证
  2. API 版本管理

    • 向后兼容性的维护
    • 平滑升级策略
  3. 性能与可维护性平衡

    • 过度设计 vs 快速迭代
    • 团队技能水平的差异
  4. 安全威胁应对

    • API 安全攻击防护
    • 数据隐私保护合规

十五、总结

通过本文的全面探讨,我们从 @RestController@RequestMapping 的基础概念出发,逐步深入到复杂的 RESTful API 设计与实现,涵盖了从简单的 “Hello World” 到完整的 CRUD 操作、参数验证、异常处理、测试部署等全生命周期的实践。

15.1 核心收获

  1. 注解的本质理解

    • @RestController@Controller + @ResponseBody 的组合
    • @RequestMapping 及其派生注解提供了灵活的请求映射机制
    • 注解驱动开发是 Spring Boot 的核心思想
  2. RESTful API 设计原则

    • 资源导向的 URL 设计
    • 合适的 HTTP 方法使用
    • 统一的响应格式和错误处理
    • 版本管理和向后兼容性
  3. 实践技能提升

    • 完整的项目结构和代码组织
    • 参数绑定和验证的最佳实践
    • 异常处理和日志记录
    • 测试策略和部署方案

15.2 最佳实践总结

  1. 分层清晰:Controller → Service → Repository 的职责分离
  2. DTO 模式:使用专门的 DTO 进行输入输出,避免暴露实体类
  3. 统一响应:采用标准化的 API 响应格式
  4. 参数验证:充分利用 Bean Validation 注解
  5. 异常处理:全局异常处理提高代码复用性
  6. 测试覆盖:单元测试、集成测试、API 测试相结合
  7. 文档完备:使用 Swagger/OpenAPI 自动生成 API 文档

15.3 进阶学习路径

  1. 深入 Spring Framework:理解 IoC、AOP、事务管理等核心机制
  2. 微服务架构:学习 Spring Cloud 生态系统
  3. 性能优化:JVM 调优、数据库优化、缓存策略
  4. 云原生技术:Docker、Kubernetes、服务网格
  5. 安全防护:OAuth2、JWT、API 网关、WAF

@RestController@RequestMapping 只是 Spring Boot Web 开发的起点,但它们体现了 Spring Boot “约定优于配置” 的设计哲学。掌握这些基础注解的原理和用法,将为你在微服务、云原生等领域的深入发展奠定坚实的基础。

记住,优秀的 API 设计不仅要功能正确,更要考虑易用性、可维护性和安全性。持续学习和实践是成为优秀后端开发者的必经之路。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐