了解如何在 Spring Boot 应用程序中使用 Spring MVC @RestController注释创建 REST API 控制器。我们将学习编写用于执行 CRUD(创建、读取、更新、删除)操作的 REST API。

1. Maven

在开始编写实际的 REST 控制器逻辑之前,我们必须在项目中导入必要的依赖项。Spring boot 的 spring-boot-starter-web 模块可传递导入所有必要的依赖项,例如用于 REST API 相关注解的 spring-webmvc、用于嵌入式服务器的 spring-boot-starter-tomcat 以及用于 JSON 请求和响应格式的 spring-boot-starter-json。

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

此外,还添加了用于编写单元测试的spring-boot-starter-test模块。

1
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

出于演示目的,我们添加了以下 Item 类,其中只有两个字段:id 和 name。我们添加了 Lombok 注解,这些注解可以生成必要的样板方法,例如 getters、setter、constructors、toString() 等。

1
@Entity @Data @NoArgsConstructor @AllArgsConstructor public class Item { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; }

 3. REST控制器

Spring REST 控制器标有注释@RestController注释。它是一种方便的注释,本身带有@Controller和@ResponseBody注释。您可以在链接的文章中详细阅读@Controller注释和@RestController注释之间的区别。

 3.1. 方法注解

在 REST 控制器中,我们使用特定于 HTTP 方法的注释来注释请求处理程序方法,这些注释将所有传入请求转发到相应的方法:

  • @GetMapping:用于映射 HTTP GET 请求。
  • @PostMapping:用于映射 HTTP POST 请求。
  • @PutMapping:用于映射 HTTP PUT 请求。
  • @DeleteMapping:用于映射 HTTP DELETE 请求。

除了上述注解之外,我们还使用多个注解来接受请求参数、标头、正文和异常处理。

  • @PathVariable:将方法参数绑定到 URI 模板变量。
  • @RequestHeader:将指定的请求头的值绑定到方法参数。
  • @RequestParam:将指定的请求参数的值绑定到方法参数。
  • @RequestBody:将 Web 请求的正文绑定到 method 参数。

 3.2. REST API

对于 Item 资源,我们可以编写以下 REST API:

 REST 接口 Description
 获取 /items  获取所有项目
 POST /项目  创建新项目
 获取 /items/{id} 按 ID 获取项目
 输入 /items/{id} 按 ID 更新项目
DEPETE /items/{id} 按 ID 删除项目

让我们在 REST 控制器中为这些方法编写最简单的逻辑:

1
import com.howtodoinjava.dao.model.Item; import com.howtodoinjava.dao.model.ItemRepository; import com.howtodoinjava.web.errors.ItemNotFoundException; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class ItemController { @Autowired ItemRepository itemRepository; @GetMapping("/items") List<Item> all() { return itemRepository.findAll(); } @GetMapping("/items/{id}") Item getById(@PathVariable Long id) { return itemRepository.findById(id) .orElseThrow(() -> new ItemNotFoundException(id)); } @PostMapping("/items") Item createNew(@RequestBody Item newItem) { return itemRepository.save(newItem); } @DeleteMapping("/items/{id}") void delete(@PathVariable Long id) { itemRepository.deleteById(id); } @PutMapping("/items/{id}") Item updateOrCreate(@RequestBody Item newItem, @PathVariable Long id) { return itemRepository.findById(id) .map(item -> { item.setName(newItem.getName()); return itemRepository.save(item); }) .orElseGet(() -> { newItem.setId(id); return itemRepository.save(newItem); }); } }

我们可以在此步骤运行应用程序,如果数据访问设置正确,则可以使用“GET /items”API 从数据库中获取项目,如下所示:

 4. 请求验证

很多时候,API 用户会发送请求负载以保存在数据库中。验证此类请求是否存在任何格式错误或缺失的信息至关重要。Spring Boot 的 spring-boot-starter-validation 模块有助于实现此目的。

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

接下来,我们在模型类(即 Item)中添加 Jakarta 验证注释。在我们的例子中,项目名称不能为空才能成为有效的项目。

1
import jakarta.validation.constraints.NotBlank; //... public class Item { //... @NotBlank(message = "Item name must not be blank") private String name; }

接下来,我们在处理程序方法“POST /items”中添加@Valid注释。此批注在调用处理程序方法时触发批注请求参数的验证。

1
@PostMapping("/items") Item createNew(@Valid @RequestBody Item newItem) { return itemRepository.save(newItem); }

我们可以通过发送缺少项目名称的请求来测试验证支持:

 5. 错误处理

这些请求不会每次都成功。有时它们会由于未知原因而失败,有时我们需要故意使它们失败,以通知用户服务器端的不良情况。

在这两种情况下,我们都会捕获异常(或手动抛出它),然后让异常处理程序捕获它并通知用户。

我们可以在“GET /items/{id}”API 中看到类似的用例。如果数据库中不存在项 ID,则从控制器方法中抛出 ItemNotFoundException。

此异常由已使用@RestControllerAdvice批注进行批注的 ApplicationExceptionHandler 类捕获。此类定义一个异常处理程序,以便在 REST 控制器引发异常时调用方法。

1
import org.springframework.http.HttpStatusCode; import org.springframework.http.ProblemDetail; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import java.net.URI; @RestControllerAdvice public class ApplicationExceptionHandler { @ExceptionHandler(ItemNotFoundException.class) public ProblemDetail handleItemNotFoundException(ItemNotFoundException ex, WebRequest request) { ProblemDetail body = ProblemDetail .forStatusAndDetail(HttpStatusCode.valueOf(404), ex.getLocalizedMessage()); body.setType(URI.create("http://my-app-host.com/errors/not-found")); body.setTitle("Item Not Found"); body.setProperty("hostname", "localhost"); return body; } }

 6. 单元测试

在我们为它编写测试之前,代码无法完成。在 Spring Boot MVC 中,我们使用包含 REST 控制器单元测试@WebMvcTest注释来注释测试类。此批注将禁用完全自动配置,而是仅应用与 MVC 测试(@Controller、@ControllerAdvice 等)相关的配置。它还自动配置可用于调用处理程序方法的 MockMvc 实例。

@WebMvcTest注解还会配置 MockBean 注解,以将模拟的依赖项注入测试类。

下面的测试类具有 ItemController REST 控制器中处理程序方法的测试方法。这些方法是不言自明的,但是,如果您有疑问,请给我留言。

1
import com.fasterxml.jackson.databind.ObjectMapper; import com.howtodoinjava.dao.model.Item; import com.howtodoinjava.dao.model.ItemRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.Arrays; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; @WebMvcTest(ItemController.class) public class ItemControllerTest { @Autowired private MockMvc mockMvc; @MockBean private ItemRepository itemRepository; @Autowired private ObjectMapper objectMapper; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); } @Test public void testGetAllItems() throws Exception { when(itemRepository.findAll()).thenReturn(Arrays.asList(new Item(), new Item())); mockMvc.perform(MockMvcRequestBuilders.get("/items") .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2)); } @Test public void testGetItemById() throws Exception { Item item = new Item(); item.setId(1L); when(itemRepository.findById(1L)).thenReturn(Optional.of(item)); mockMvc.perform(MockMvcRequestBuilders.get("/items/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1)); } @Test public void testGetItemById_NotFound() throws Exception { when(itemRepository.findById(anyLong())).thenReturn(Optional.empty()); mockMvc.perform(MockMvcRequestBuilders.get("/items/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isNotFound()); } @Test public void testCreateNewItem() throws Exception { Item newItem = new Item(); newItem.setName("Test Item"); when(itemRepository.save(any(Item.class))).thenReturn(newItem); mockMvc.perform(MockMvcRequestBuilders.post("/items") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(newItem))) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Test Item")); } @Test public void testUpdateOrCreateItem() throws Exception { Item newItem = new Item(); newItem.setId(1L); newItem.setName("Updated Item"); when(itemRepository.findById(1L)).thenReturn(Optional.of(new Item())); when(itemRepository.save(any(Item.class))).thenReturn(newItem); mockMvc.perform(MockMvcRequestBuilders.put("/items/1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(newItem))) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Updated Item")); } @Test public void testDeleteItem() throws Exception { mockMvc.perform(MockMvcRequestBuilders.delete("/items/1")) .andExpect(MockMvcResultMatchers.status().isOk()); } }

 7. 结论

此Spring Boot教程演示了如何创建REST API控制器。它讨论了创建 API 处理程序方法、添加验证和错误处理。

此外,它还展示了 REST 控制器方法的初始单元测试,我们可以根据业务需求添加更多单元测试。

 祝您学习愉快!!

Github 上的源代码