如何使用 Spring Boot 构建一个 RESTful Web 服务插图

本文将介绍如何使用 Spring Boot 构建 RESTful Web 服务,主要关注项目的结构、注解的使用和单元测试代码的编写,并由此探索 Spring Boot 的设计理念与使用方法。

项目结构

本文使用三层架构代码结构,其中 src/main/java 目录下主要分三个目录:controllermodelservice。处理请求和返回响应的逻辑控制器代码需要放在 controller 目录下;表示数据对象的 POJO 类需要方在 model 目录下;主要的业务逻辑代码需要抽取到服务中,然后放在 service 目录下(service 目录下是接口类,impl 目录下是实现类)。

如何使用 Spring Boot 构建一个 RESTful Web 服务插图1
spring-boot-restful-service-demo
├─ src/main/java
│   └─ com.example.demo
│       ├─ controller
│       │   └─ UserController.java
│       ├─ model
│       │   └─ User.java
│       ├─ service
│       │   ├─ UserService.java
│       │   └─ impl
│       │       └─ UserServiceImpl.java
│       └─ DemoApplication.java
├─ src/test/java
│   └─ com.example.demo
│       └─ controller
│           └─ UserControllerTest.java
└─ pom.xml

此外,src/test/java 用于存放测试代码,测试代码应与被测试代码使用相同的包名。

源码分析

下面分析该项目的源码,以期对 Spring Boot 的使用有一个基本的了解。

pom.xml 代码

Spring Boot 提供各类封装好的 Starter(以 spring-boot-starter-* 格式命名)供我们去使用,当需要某项依赖时,直接在 pom.xml 引用对应的 Starter 即可。

本文使用 Maven 管理依赖,pom.xml 源码如下:

<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
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.0</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

可以看到,本文的示例项目使用了三个 Starter:spring-boot-starter-webspring-boot-starter-validationspring-boot-starter-test

  • spring-boot-starter-web 包含了编写 Spring Web 程序相关的所有依赖,如编写 RESTful 接口相关的依赖、Spring MVC 相关的依赖、程序的运行时服务器(默认为 Apache Tomcat)相关的依赖等;
  • spring-boot-starter-validation 包含了请求参数校验相关的所有依赖;
  • spring-boot-starter-test 包含了测试 Spring Boot 程序的所有依赖,如 JUnit Jupiter、Hamcrest 和 Mockito 等。

此外,还使用了一个插件 spring-boot-maven-plugin,提供了对程序打包和运行的支持。

如何使用 Spring Boot 构建一个 RESTful Web 服务插图2

启动类代码

程序入口类 src/main/java/com/example/demo/DemoApplication.java 的代码如下:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

从启动类即可以看到,Spring Boot 应用程序无须 web.xml 等冗长的配置文件,使用纯 Java 注解的方式即可进行配置。

可以看到,该类只使用了一个注解:@SpringBootApplication,该注解是一个便捷注解,其包含了如下三个注解:

  1. @Configuration:用于定义配置类;
  2. @EnableAutoConfiguration:用于自动装入应用程序所需的所有 Bean;
  3. @ComponentScan:扫描指定路径,将其中带有 @Controller@Service@Repository@Component 注解的类注册到 Spring 容器中。

因此,@SpringBootApplication 可以简化一些常见配置的使用,使得程序的启动更为便捷。

控制器代码

src/main/java/com/example/demo/controller/UserController.java 的代码如下:

package com.example.demo.controller;

import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("")
    public List<User> getUserList() {
        return userService.getUserList();
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }

    @PostMapping("")
    public User createUser(@Validated @RequestBody User user) {
        return userService.createUser(user);
    }

    @PutMapping("/{id}")
    public User updateUserById(@PathVariable Long id, @Validated @RequestBody User user) {
        return userService.updateUserById(id, user);
    }

    @DeleteMapping("/{id}")
    public void deleteUserById(@PathVariable Long id) {
        userService.deleteUserById(id);
    }
}

该类使用了 @RestController 注解,并在类上使用了 @RequestMapping("/users"),表示映射到 /users 路径下。

其中,@Autowired 注解实现了自动装配,将 UserService 接口的实现类 UserServiceImpl 注入到了 UserController 类中。

该类包含五个方法,分别对应了 RESTful API 的 GET、POST、PUT 和 DELETE 请求。

  • getUserList() 方法映射了 GET 请求,返回所有用户的列表;
  • getUserById(@PathVariable Long id) 方法映射了 GET 请求,通过 id 返回指定用户;
  • createUser(@Validated @RequestBody User user) 方法映射了 POST 请求,创建一个新的用户并返回创建后的用户对象;
  • updateUserById(@PathVariable Long id, @Validated @RequestBody User user) 方法映射了 PUT 请求,通过 id 更新用户信息,并返回更新后的用户对象;
  • deleteUserById(@PathVariable Long id) 方法映射了 DELETE 请求,通过 id 删除指定用户。

这些方法的注解说明如下:

  • @GetMapping("/{id}"):表示映射 GET 请求,其中 {id} 部分为路径变量;
  • @PostMapping(""):表示映射 POST 请求,其中空字符串表示该请求路径为 /users
  • @PutMapping("/{id}"):表示映射 PUT 请求,其中 {id} 部分为路径变量;
  • @DeleteMapping("/{id}"):表示映射 DELETE 请求,其中 {id} 部分为路径变量;
  • @Validated:表示启用方法参数验证;
  • @RequestBody:表示将 HTTP 请求正文转换为 Java 对象。

服务代码

src/main/java/com/example/demo/service/impl/UserServiceImpl.java 的代码如下:

package com.example.demo.service.impl;

import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class UserServiceImpl implements UserService {
    // 模拟数据库
    private static Map<Long, User> userMap = new HashMap<>();

    @Override
    public List<User> getUserList() {
        return new ArrayList<>(userMap.values());
    }

    @Override
    public User getUserById(Long id) {
        return userMap.get(id);
    }

    @Override
    public User createUser(User user) {
        Long id = (long) (userMap.size() + 1);
        user.setId(id);
        userMap.put(id, user);
        return user;
    }

    @Override
    public User updateUserById(Long id, User user) {
        User oldUser = userMap.get(id);
        oldUser.setName(user.getName());
        oldUser.setAge(user.getAge());
        oldUser.setAddress(user.getAddress());
        userMap.put(id, oldUser);
        return oldUser;
    }

    @Override
    public void deleteUserById(Long id) {
        userMap.remove(id);
    }
}

该类实现了 UserService 接口,并使用了 @Service 注解,表示将其注册到 Spring 容器中。

其中,定义了一个静态变量 userMap,用于存储模拟的用户数据。该类实现了 getUserList()getUserById()createUser()updateUserById()deleteUserById() 方法,这些方法的具体实现可以根据业务需求进行修改。

单元测试代码

src/test/java/com/example/demo/controller/UserControllerTest.java 的代码如下:

package com.example.demo.controller;

import com.example.demo.model.User;
import com.example.demo.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
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.ArrayList;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@SpringJUnitConfig
@AutoConfigureMockMvc
@SpringBootTest
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Mock
    private UserService userService;
    @InjectMocks
    private UserController userController;
    private List<User> users = new ArrayList<>();
    private User user1, user2;

    @BeforeEach
    public void setup() {
        user1 = new User(1L, "张三", 20, "北京市海淀区");
        user2 = new User(2L, "李四", 22, "北京市朝阳区");
        users.add(user1);
        users.add(user2);
    }

    @Test
    public void getUserList() throws Exception {
        when(userService.getUserList()).thenReturn(users);

        mockMvc.perform(MockMvcRequestBuilders.get("/users"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.size()").value(2))
                .andExpect(MockMvcResultMatchers.jsonPath("$.[0].name").value("张三"))
                .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    public void getUserById() throws Exception {
        when(userService.getUserById(1L)).thenReturn(user1);

        mockMvc.perform(MockMvcRequestBuilders.get("/users/1"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("张三"))
                .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    public void createUser() throws Exception {
        User user = new User(null, "王五", 25, "北京市丰台区");
        User savedUser = new User(3L, "王五", 25, "北京市丰台区");
        when(userService.createUser(any(User.class))).thenReturn(savedUser);

        mockMvc.perform(MockMvcRequestBuilders.post("/users")
                .content(new ObjectMapper().writeValueAsString(user))
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(3))
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("王五"))
                .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    public void updateUserById() throws Exception {
        User newUser = new User(null, "王五", 25, "北京市丰台区");
        User updatedUser = new User(1L, "王五", 25, "北京市朝阳区");
        when(userService.updateUserById(1L, newUser)).thenReturn(updatedUser);

        mockMvc.perform(MockMvcRequestBuilders.put("/users/1")
                .content(new ObjectMapper().writeValueAsString(newUser))
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1))
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("王五"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.address").value("北京市朝阳区"))
                .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    public void deleteUserById() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.delete("/users/2"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

该测试类使用了 Spring Boot 提供的测试框架,其中使用了 @Mock 注解和 @InjectMocks 注解,分别用于模拟 UserServiceUserController

测试类包含了四个测试方法,分别测试了四种 HTTP 请求。其中,通过 MockMvc 对象模拟了 HTTP 请求,并使用 andExpect() 方法对返回结果进行判断,判断响应头、响应状态码、响应内容等是否符合预期。

总结

本文介绍了如何使用 Spring Boot 构建 RESTful Web 服务。通过实现控制器、服务和单元测试代码,展示了 Spring Boot 的注解使用、自动装配和启动方式。

在实际开发中,这种结构清晰的代码架构可以使项目更加易于维护和扩展,同时也提高了代码的可读性和可测试性,为后续的项目迭代提供了一定的便利。