1. 引言

后端HTTP API开发的重要能力之一是处理来自前端的查询参数。本教程将介绍如何直接从HttpServletRequest获取这些参数,并使用Spring MVC提供的简洁方法。

2. HttpServletRequest的方法

2.1. HttpServletRequest#getQueryString()

下面的例子展示了通过调用HttpServletRequest#getQueryString()方法从URL中直接获取的内容:

@GetMapping("/api/byGetQueryString")
public String byGetQueryString(HttpServletRequest request) {
    return request.getQueryString();
}

当我们使用curl发送带有多个参数的GET请求到此API时,getQueryString()方法会返回所有'?'之后的字符:

$ curl 'http://127.0.0.1:8080/spring-mvc-basics/api/byGetQueryString?username=bob&roles=admin&roles=stuff'
username=bob&roles=admin&roles=stuff

注意,如果将@GetMapping改为@RequestMapping,当我们使用POST、PUT、PATCH或DELETE等HTTP方法发送请求时,它将返回相同的结果。这意味着HttpServletRequest始终能获取查询字符串,不论HTTP方法是什么。因此,在本文档中,我们将主要关注GET请求。为了简化展示HttpServletRequest提供的方法,我们在后续示例中都会使用相同的请求参数。

2.2. HttpServletRequest#getParameter(String)

为了简化参数解析,HttpServletRequest提供了getParameter方法,通过参数名获取值:

@GetMapping("/api/byGetParameter")
public String byGetParameter(HttpServletRequest request) {
    String username = request.getParameter("username");
    return "username:" + username;
}

当发送包含查询字符串username=bob的GET请求时,调用getParameter("username")将返回bob

$ curl 'http://127.0.0.1:8080/spring-mvc-basics/api/byGetParameter?username=bob&roles=admin&roles=stuff'
username:bob

2.3. HttpServletRequest#getParameterValues(String)

getParameterValues方法与getParameter类似,但返回的是一个String[]而不是String。这是因为HTTP规范允许使用相同名称传递多个参数。

@GetMapping("/api/byGetParameterValues")
public String byGetParameterValues(HttpServletRequest request) {
    String[] roles = request.getParameterValues("roles");
    return "roles:" + Arrays.toString(roles);
}

当传递带有roles参数的值两次时,数组中应有两个值:

$ curl 'http://127.0.0.1:8080/spring-mvc-basics/api/byGetParameterValues?username=bob&roles=admin&roles=stuff'
roles:[admin, stuff]

2.4. HttpServletRequest#getParameterMap()

假设我们有以下UserDtoPOJO作为JSON API示例的一部分:

public class UserDto {
    private String username;
    private List<String> roles;
    // standard getter/setters...
}

可以看到,一个或多个值可以对应多个不同的参数名。为此,HttpServletRequest提供了另一个方法getParameterMap(),它返回一个Map<String, String[]>。这个方法允许我们使用Map获取参数值。

@GetMapping("/api/byGetParameterMap")
public UserDto byGetParameterMap(HttpServletRequest request) {
    Map parameterMap = request.getParameterMap();
    String[] usernames = parameterMap.get("username");
    String[] roles = parameterMap.get("roles");
    UserDto userDto = new UserDto();
    userDto.setUsername(usernames[0]);
    userDto.setRoles(Arrays.asList(roles));
    return userDto;
}

对于这个例子,我们将得到一个JSON响应:

$ curl 'http://127.0.0.1:8080/spring-mvc-basics/api/byGetParameterMap?username=bob&roles=admin&roles=stuff'
{"username":"bob","roles":["admin","stuff"]}

3. 使用Spring MVC获取参数

现在来看看Spring MVC在解析查询字符串时如何改进编码体验。

3.1. 参数名称

在Spring MVC框架中,我们无需直接使用HttpServletRequest手动解析参数。在第一个示例中,我们定义了一个带有查询参数名称usernameroles的方法,并移除了对HttpServletRequest的使用,这由Spring MVC处理。

@GetMapping("/api/byParameterName")
public UserDto byParameterName(String username, String[] roles) {
    UserDto userDto = new UserDto();
    userDto.setUsername(username);
    userDto.setRoles(Arrays.asList(roles));
    return userDto;
}

由于使用了相同的模型,这将返回与上一个例子相同的结果:

$ curl 'http://127.0.0.1:8080/spring-mvc-basics/api/byParameterName?username=bob&roles=admin&roles=stuff'
{"username":"bob","roles":["admin","stuff"]}

3.2. @RequestParam

如果HTTP查询参数名和Java方法参数名不同,或者方法参数名称不会保留在编译后的字节码中,我们可以为这种情况在方法参数上配置@RequestParam注解。

在我们的例子中,我们使用@RequestParam("username")@RequestParam("roles")如下:

@GetMapping("/api/byAnnoRequestParam")
public UserDto byAnnoRequestParam(@RequestParam("username") String var1, @RequestParam("roles") List<String> var2) {
    UserDto userDto = new UserDto();
    userDto.setUsername(var1);
    userDto.setRoles(var2);
    return userDto;
}

然后进行测试:

$ curl 'http://127.0.0.1:8080/spring-mvc-basics/api/byAnnoRequestParam?username=bob&roles=admin&roles=stuff'
{"username":"bob","roles":["admin","stuff"]}

3.3. POJO

更简单的是,我们可以直接使用POJO作为参数类型:

@GetMapping("/api/byPojo")
public UserDto byPojo(UserDto userDto) {
    return userDto;
}

Spring MVC可以自动解析参数、创建POJO实例并填充所需的参数。

$ curl 'http://127.0.0.1:8080/spring-mvc-basics/api/byPojo?username=bob&roles=admin&roles=stuff'
{"username":"bob","roles":["admin","stuff"]}

最后,我们通过单元测试来确保最后四种方法提供完全相同的功能。

@ParameterizedTest
@CsvSource(textBlock = """
    /api/byGetParameterMap
    /api/byParameterName
    /api/byAnnoRequestParam
    /api/byPojo
    """)
public void whenPassParameters_thenReturnResolvedModel(String path) throws Exception {
    this.mockMvc.perform(get(path + "?username=bob&roles=admin&roles=stuff"))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.username").value("bob"))
      .andExpect(jsonPath("$.roles").value(containsInRelativeOrder("admin", "stuff")));
}

4. 结论

在这篇文章中,我们介绍了如何使用Spring MVC从HttpServletRequest获取参数。从这些示例中,我们可以看到,使用Spring MVC解析参数时,可以大大减少代码量。

如往常一样,本文档中的所有代码片段可在GitHub上找到。