一、什么是RESTful风格?为什么它如此重要?

想象一下你去餐厅点餐的过程:你告诉服务员你想要什么(请求),服务员根据你的要求准备食物并端给你(响应)。RESTful风格就像这种点餐方式,只不过是在网络上进行的。

RESTful(Representational State Transfer,表述性状态转移)是一种设计Web服务的架构风格,由Roy Fielding博士在2000年提出。它不是标准,而是一套设计原则和约束条件,让网络服务更加简洁、可扩展。

为什么叫"表述性状态转移"?

听起来很高大上,其实很简单:

  • 表述性:客户端和服务器交换的是资源的表述(如JSON、XML),不是直接操作数据库

  • 状态转移:通过请求让资源的状态从服务器转移到客户端(或相反)

RESTful的六大原则

  1. 客户端-服务器分离:前端和后端各司其职

  2. 无状态:每个请求包含所有必要信息,服务器不保存客户端状态

  3. 可缓存:响应应明确是否可缓存

  4. 统一接口:使用统一的交互方式(如HTTP方法)

  5. 分层系统:客户端不知道是和服务器直接通信还是通过中间层

  6. 按需代码(可选):服务器可以临时扩展客户端功能(如JavaScript)

RESTful vs 传统API

传统API可能是这样的:

/getUserById?id=123
/createUser
/updateUser?id=123
/deleteUser?id=123

RESTful风格则是这样的:

GET /users/123
POST /users
PUT /users/123
DELETE /users/123

为什么企业都流行RESTful?

  1. 简单直观:使用标准的HTTP方法,学习成本低

  2. 轻量级:通常使用JSON,比SOAP等更轻量

  3. 前后端分离:前端可以独立开发,只需约定接口

  4. 可扩展性强:易于添加新功能而不破坏现有系统

  5. 跨平台:任何能发HTTP请求的设备都能使用

  6. 利于缓存:充分利用HTTP协议本身的缓存机制

二、Spring Boot中实现RESTful服务的示例

让我们通过一个完整的"图书管理系统"示例,看看如何在Spring Boot中实现RESTful服务。

1. 环境准备

首先创建一个Spring Boot项目(也可以这个:  Spring Initializr),添加以下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 定义资源模型

@Entity
@Data
@NoArgsConstructor
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
    private String author;
    private String isbn;
    private LocalDate publishDate;
    
    // 构造方法、getter和setter由Lombok自动生成
}

3. 创建Repository接口

public interface BookRepository extends JpaRepository<Book, Long> {
    List<Book> findByAuthor(String author);
}

4. 实现RESTful控制器

@RestController
@RequestMapping("/api/books")
public class BookController {
    
    private final BookRepository bookRepository;
    
    // 构造器注入
    public BookController(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    
    // 获取所有图书
    @GetMapping
    public ResponseEntity<List<Book>> getAllBooks() {
        return ResponseEntity.ok(bookRepository.findAll());
    }
    
    // 获取特定图书
    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        return bookRepository.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
    
    // 根据作者查询图书
    @GetMapping("/search")
    public ResponseEntity<List<Book>> getBooksByAuthor(@RequestParam String author) {
        return ResponseEntity.ok(bookRepository.findByAuthor(author));
    }
    
    // 创建新图书
    @PostMapping
    public ResponseEntity<Book> createBook(@Valid @RequestBody Book book) {
        Book savedBook = bookRepository.save(book);
        return ResponseEntity.created(URI.create("/api/books/" + savedBook.getId()))
                .body(savedBook);
    }
    
    // 更新图书
    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @Valid @RequestBody Book bookDetails) {
        return bookRepository.findById(id)
                .map(book -> {
                    book.setTitle(bookDetails.getTitle());
                    book.setAuthor(bookDetails.getAuthor());
                    book.setIsbn(bookDetails.getIsbn());
                    book.setPublishDate(bookDetails.getPublishDate());
                    return ResponseEntity.ok(bookRepository.save(book));
                })
                .orElse(ResponseEntity.notFound().build());
    }
    
    // 删除图书
    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteBook(@PathVariable Long id) {
        return bookRepository.findById(id)
                .map(book -> {
                    bookRepository.delete(book);
                    return ResponseEntity.noContent().build();
                })
                .orElse(ResponseEntity.notFound().build());
    }
}

5. 添加全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<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);
        });
        return ResponseEntity.badRequest().body(errors);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralExceptions(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("服务器内部错误: " + ex.getMessage());
    }
}

三、实现RESTful服务时的注意事项

1. 资源命名规范

  1. 使用名词复数形式(如/books而不是/book

  2. 避免动词(用HTTP方法表示动作)

  3. 层级关系表达:/authors/{authorId}/books

不好的例子

/getAllBooks
/createNewBook
/updateBookInfo

好的例子

GET /books
POST /books
PUT /books/{id}

2. HTTP方法正确使用

解释一下:幂等操作指的是对同一资源进行一次请求和重复多次请求,所产生的效果(对资源状态的改变)是相同的,并且不会额外产生副作用。也就是说,无论执行一次还是多次该操作,最终的结果都是一样的。

幂等性的重要性

  1. 提高系统的可靠性:在网络不稳定的情况下,客户端可能会重复发送请求。如果操作是幂等的,服务器可以安全地处理这些重复请求,而不会产生额外的错误或数据不一致的问题。
  2. 简化设计和开发:幂等性使得开发者不需要担心重复请求对系统状态的影响,降低了系统设计和开发的复杂度。
  3. 便于缓存和重试机制的实现:幂等操作可以更容易地实现缓存和重试机制,因为重复执行不会改变系统的状态。例如,对于 GET 请求,可以将响应结果缓存起来,下次相同的请求可以直接从缓存中获取结果,提高系统的性能。
方法 用途 是否幂等 是否有主体
GET 获取资源
POST 创建资源
PUT 更新完整资源
PATCH 部分更新资源
DELETE 删除资源

3. 状态码使用规范 (HTTP状态码详解及其解决方案404,403,500等

状态码 含义 使用场景
200 OK 成功GET、PUT或DELETE
201 Created 成功POST,应在响应头包含Location
204 No Content 成功DELETE或不需要返回内容的请求
400 Bad Request 客户端请求错误
401 Unauthorized 需要认证但未提供
403 Forbidden 认证成功但无权限
404 Not Found 资源不存在
405 Method Not Allowed 不支持的HTTP方法
409 Conflict 资源状态冲突(如重复创建)
500 Internal Server Error 服务器内部错误

4. 版本控制策略

API应该考虑版本控制,常见方法:

  1. URI路径版本控制(最常用)

    /v1/books
    /v2/books
  2. 查询参数版本控制

    /books?version=1
  3. 请求头版本控制

    Accept: application/vnd.myapi.v1+json

5. 分页、排序和过滤

对于返回集合的接口,应该支持分页:

@GetMapping
public ResponseEntity<Page<Book>> getAllBooks(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "id") String sort) {
    Pageable pageable = PageRequest.of(page, size, Sort.by(sort));
    return ResponseEntity.ok(bookRepository.findAll(pageable));
}
GET /api/books?page=0&size=5&sort=title,desc

6. HATEOAS考虑

HATEOAS(Hypermedia as the Engine of Application State)是REST的一个高级特性,让响应中包含相关操作的链接。

添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

示例:

@GetMapping("/{id}")
public EntityModel<Book> getBookById(@PathVariable Long id) {
    return bookRepository.findById(id)
            .map(book -> {
                EntityModel<Book> model = EntityModel.of(book);
                model.add(linkTo(methodOn(BookController.class).getBookById(id)).withSelfRel());
                model.add(linkTo(methodOn(BookController.class).getAllBooks()).withRel("books"));
                return model;
            })
            .orElseThrow(() -> new ResourceNotFoundException("Book not found"));
}

响应示例:

{
    "id": 1,
    "title": "Spring in Action",
    "author": "Craig Walls",
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/books/1"
        },
        "books": {
            "href": "http://localhost:8080/api/books"
        }
    }
}

7. 安全性考虑

  1. 使用HTTPS:所有RESTful API都应该通过HTTPS提供

  2. 认证和授权:集成Spring Security  (Spring Security的学习

  3. 输入验证:防止注入攻击

  4. CORS配置:明确允许的源

基本Spring Security配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // 对REST API通常禁用CSRF
            .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/api/books/**").permitAll()
                .antMatchers(HttpMethod.POST, "/api/books").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .httpBasic();
    }
}

四、处理请求和响应

@GetMapping("/{id}/reviews")
public ResponseEntity<List<Review>> getBookReviews(
        @PathVariable Long id, // 路径参数
        @RequestParam(required = false) Integer minRating, // 可选查询参数
        @RequestParam(defaultValue = "date") String sortBy) { // 默认值
    // 实现逻辑
}

1. 请求处理

路径参数和查询参数

请求:

GET /api/books/1/reviews?minRating=4&sortBy=rating
请求体验证

使用Java Bean Validation:

@Data
public class Book {
    @NotBlank(message = "书名不能为空")
    private String title;
    
    @NotBlank
    private String author;
    
    @Pattern(regexp = "^(97(8|9))?\\d{9}(\\d|X)$", message = "ISBN格式不正确")
    private String isbn;
    
    @PastOrPresent(message = "出版日期不能是未来")
    private LocalDate publishDate;
}

然后在控制器方法参数前加@Valid注解:

@PostMapping
public ResponseEntity<Book> createBook(@Valid @RequestBody Book book) {
    // ...
}

2. 响应处理

统一响应格式

创建通用响应类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(true, null, data);
    }
    
    public static ApiResponse<?> error(String message) {
        return new ApiResponse<>(false, message, null);
    }
}

示例:

@GetMapping("/{id}")
public ApiResponse<Book> getBookById(@PathVariable Long id) {
    return bookRepository.findById(id)
            .map(ApiResponse::success)
            .orElse(ApiResponse.error("图书不存在"));
}
文件上传下载

文件上传:

@PostMapping("/{id}/cover")
public ResponseEntity<?> uploadCover(@PathVariable Long id, 
                                   @RequestParam("file") MultipartFile file) {
    // 保存文件逻辑
    return ResponseEntity.ok().build();
}

文件下载:

@GetMapping("/{id}/cover")
public ResponseEntity<Resource> downloadCover(@PathVariable Long id) {
    // 获取文件逻辑
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
            .body(fileResource);
}

五、测试RESTful服务

1. 单元测试

测试控制器:

@WebMvcTest(BookController.class)
public class BookControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private BookRepository bookRepository;
    
    @Test
    public void shouldReturnBookWhenExists() throws Exception {
        Book mockBook = new Book(1L, "Test Book", "Test Author", "1234567890", LocalDate.now());
        when(bookRepository.findById(1L)).thenReturn(Optional.of(mockBook));
        
        mockMvc.perform(get("/api/books/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("Test Book"));
    }
    
    @Test
    public void shouldReturn404WhenBookNotExists() throws Exception {
        when(bookRepository.findById(999L)).thenReturn(Optional.empty());
        
        mockMvc.perform(get("/api/books/999"))
                .andExpect(status().isNotFound());
    }
}

2. 集成测试

@SpringBootTest
@AutoConfigureMockMvc
public class BookIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private BookRepository bookRepository;
    
    @AfterEach
    public void cleanup() {
        bookRepository.deleteAll();
    }
    
    @Test
    public void shouldCreateAndRetrieveBook() throws Exception {
        // 创建图书
        String bookJson = "{\"title\":\"Integration Test\",\"author\":\"Tester\",\"isbn\":\"1234567890\"}";
        
        mockMvc.perform(post("/api/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content(bookJson))
                .andExpect(status().isCreated());
        
        // 获取图书列表
        mockMvc.perform(get("/api/books"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].title").value("Integration Test"));
    }
}

3. 使用TestRestTemplate测试

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BookRestTemplateTest {
    
    @LocalServerPort
    private int port;
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    public void shouldReturnBooksList() {
        ResponseEntity<List<Book>> response = restTemplate.exchange(
                "http://localhost:" + port + "/api/books",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Book>>() {});
        
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
    }
}

4. 使用Swagger UI测试

添加依赖:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.9</version>
</dependency>

访问http://localhost:8080/swagger-ui.html即可看到API文档和测试界面。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐