RESTful 风格是什么?现在企业都流行这个吗?通俗易懂的RESTful风格讲解:让你不迷糊!
想象一下你去餐厅点餐的过程:你告诉服务员你想要什么(请求),服务员根据你的要求准备食物并端给你(响应)。RESTful风格就像这种点餐方式,只不过是在网络上进行的。RESTful(Representational State Transfer,表述性状态转移)是一种设计Web服务的架构风格,由Roy Fielding博士在2000年提出。它不是标准,而是一套设计原则和约束条件,让网络服务更加简洁、
一、什么是RESTful风格?为什么它如此重要?
想象一下你去餐厅点餐的过程:你告诉服务员你想要什么(请求),服务员根据你的要求准备食物并端给你(响应)。RESTful风格就像这种点餐方式,只不过是在网络上进行的。
RESTful(Representational State Transfer,表述性状态转移)是一种设计Web服务的架构风格,由Roy Fielding博士在2000年提出。它不是标准,而是一套设计原则和约束条件,让网络服务更加简洁、可扩展。
为什么叫"表述性状态转移"?
听起来很高大上,其实很简单:
-
表述性:客户端和服务器交换的是资源的表述(如JSON、XML),不是直接操作数据库
-
状态转移:通过请求让资源的状态从服务器转移到客户端(或相反)
RESTful的六大原则
-
客户端-服务器分离:前端和后端各司其职
-
无状态:每个请求包含所有必要信息,服务器不保存客户端状态
-
可缓存:响应应明确是否可缓存
-
统一接口:使用统一的交互方式(如HTTP方法)
-
分层系统:客户端不知道是和服务器直接通信还是通过中间层
-
按需代码(可选):服务器可以临时扩展客户端功能(如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?
简单直观:使用标准的HTTP方法,学习成本低
轻量级:通常使用JSON,比SOAP等更轻量
前后端分离:前端可以独立开发,只需约定接口
可扩展性强:易于添加新功能而不破坏现有系统
跨平台:任何能发HTTP请求的设备都能使用
利于缓存:充分利用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. 资源命名规范
-
使用名词复数形式(如
/books而不是/book) -
避免动词(用HTTP方法表示动作)
-
层级关系表达:
/authors/{authorId}/books
不好的例子:
/getAllBooks /createNewBook /updateBookInfo
好的例子:
GET /books
POST /books
PUT /books/{id}
2. HTTP方法正确使用
解释一下:幂等操作指的是对同一资源进行一次请求和重复多次请求,所产生的效果(对资源状态的改变)是相同的,并且不会额外产生副作用。也就是说,无论执行一次还是多次该操作,最终的结果都是一样的。
幂等性的重要性
- 提高系统的可靠性:在网络不稳定的情况下,客户端可能会重复发送请求。如果操作是幂等的,服务器可以安全地处理这些重复请求,而不会产生额外的错误或数据不一致的问题。
- 简化设计和开发:幂等性使得开发者不需要担心重复请求对系统状态的影响,降低了系统设计和开发的复杂度。
- 便于缓存和重试机制的实现:幂等操作可以更容易地实现缓存和重试机制,因为重复执行不会改变系统的状态。例如,对于
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应该考虑版本控制,常见方法:
-
URI路径版本控制(最常用)
/v1/books /v2/books
-
查询参数版本控制
/books?version=1
-
请求头版本控制
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. 安全性考虑
-
使用HTTPS:所有RESTful API都应该通过HTTPS提供
-
认证和授权:集成Spring Security (Spring Security的学习)
-
输入验证:防止注入攻击
-
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文档和测试界面。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)