乐趣区

SpringBoot系列教程web篇之Get请求参数解析姿势汇总

一般在开发 web 应用的时候,如果提供 http 接口,最常见的 http 请求方式为 GET/POST,我们知道这两种请求方式的一个显著区别是 GET 请求的参数在 url 中,而 post 请求可以不在 url 中;那么一个 SpringBoot 搭建的 web 应用可以如何解析发起的 http 请求参数呢?

下面我们将结合实例汇总一下 GET 请求参数的几种常见的解析姿势

原文:190824-SpringBoot 系列教程 web 篇之 Get 请求参数解析姿势汇总
)
<!– more –>

I. 环境搭建

首先得搭建一个 web 应用才有可能继续后续的测试,借助 SpringBoot 搭建一个 web 应用属于比较简单的活;

创建一个 maven 项目,pom 文件如下

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7</version>
    <relativePath/> <!-- lookup parent from update -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    <java.version>1.8</java.version>
</properties>

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

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

添加项目启动类Application.cass

@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class);
    }
}

在演示请求参数的解析实例中,我们使用终端的 curl 命令来发起 http 请求(主要原因是截图上传太麻烦,还是终端的文本输出比较方便;缺点是不太直观)

II. GET 请求参数解析

接下来我们正式进入参数解析的妖娆姿势篇,会介绍一下常见的一些 case(并不能说包含了所有的使用 case)

下面所有的方法都放在 ParamGetRest 这个 Controller 中

@RestController
@RequestMapping(path = "get")
public class ParamGetRest {}

1. HttpServletRequest

直接使用 HttpServletRequest 来获取请求参数,属于比较原始,但是灵活性最高的使用方法了。

常规使用姿势是方法的请求参数中有一个 HttpServletRequest,我们通过ServletRequest#getParameter(参数名) 来获取具体的请求参数,下面演示返回所有请求参数的 case

@GetMapping(path = "req")
public String requestParam(HttpServletRequest httpRequest) {Map<String, String[]> ans = httpRequest.getParameterMap();
    return JSON.toJSONString(ans);
}

测试 case,注意下使用 curl 请求参数中有中文时,进行了 url 编码(后续会针对这个问题进行说明)

➜  ~ curl 'http://127.0.0.1:8080/get/req?name=yihuihiu&age=19'
{"name":["yihuihiu"],"age":["19"]}%                                                                                                                                       ➜  ~ curl 'http://127.0.0.1:8080/get/req?name=%E4%B8%80%E7%81%B0%E7%81%B0&age=19'
{"name":["一灰灰"],"age":["19"]}%

使用 HttpServletRequest 获取请求参数,还有另外一种使用 case,不通过参数传递的方式获取 Request 实例,而是借助RequestContextHolder;这样的一个好处就是,假设我们想写一个 AOP,拦截 GET 请求并输出请求参数时,可以通过下面这种方式来处理

@GetMapping(path = "req2")
public String requestParam2() {
    HttpServletRequest request =
            ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    String name = request.getParameter("name");
    return "param Name=" + name;
}

测试 case

➜  ~ curl 'http://127.0.0.1:8080/get/req2?name=%E4%B8%80%E7%81%B0%E7%81%B0&age=19'
param Name= 一灰灰 %

2. 方法参数

这种解析方式比较厉害了,将 GET 参数与方法的参数根据参数名进行映射,从感官上来看,就像是直接调用这个一样

@GetMapping(path = "arg")
public String argParam(String name, Integer age) {return "name:" + name + "age:" + age;}

针对上面提供的方式,我们的测试自然会区分为下面几种,看下会怎样

  • 正好两个参数,与定义一直
  • 缺少一个请求参数
  • 多一个请求参数
  • 参数类型不一致
# 参数解析正常
➜  ~ curl 'http://127.0.0.1:8080/get/arg?name=%E4%B8%80%E7%81%B0%E7%81%B0&age=19'
name: 一灰灰 age: 19%
# 缺少一个参数时,为 null
➜  ~ curl 'http://127.0.0.1:8080/get/arg?name=%E4%B8%80%E7%81%B0%E7%81%B0'
name: 一灰灰 age: null% 
# 多了一个参数,无法被解析
➜  ~ curl 'http://127.0.0.1:8080/get/arg?name=%E4%B8%80%E7%81%B0%E7%81%B0&age=19&id=10'
name: 一灰灰 age: 19%                                                              
# 类型不一致,500 
➜  ~ curl 'http://127.0.0.1:8080/get/arg?name=%E4%B8%80%E7%81%B0%E7%81%B0&age=haha' -i
HTTP/1.1 500
Content-Length: 0
Date: Sat, 24 Aug 2019 01:45:14 GMT
Connection: close

从上面实际的 case 可以看出,利用方法参数解析 GET 传参时,实际效果是:

  • 方法参数与 GET 传参,通过参数签名进行绑定
  • 方法参数类型,需要与接收的 GET 传参类型一致
  • 方法参数非基本类型时,若传参没有,则为 null;(也就是说如果为基本类型,无法转 null,抛异常)
  • 实际的 GET 传参可以多于方法定义的参数

接下来给一个数组传参解析的实例

@GetMapping(path = "arg2")
public String argParam2(String[] names, int size) {return "name:" + (names != null ? Arrays.asList(names) : "null") + "size:" + size;
}

测试 case 如下,传数组时参数值用逗号分隔;基本类型,必须传参,否则解析异常

➜  ~ curl 'http://127.0.0.1:8080/get/arg2?name=yihui,erhui&size=2'
name: null size: 2%                                                                                                                                                       ➜  ~ curl 'http://127.0.0.1:8080/get/arg2?name=yihui,erhui' -i
HTTP/1.1 500
Content-Length: 0
Date: Sat, 24 Aug 2019 01:53:30 GMT
Connection: close

3. RequestParam 注解

这种方式看起来和前面有些相似,但更加灵活,我们先看一下注解

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
  // 指定请求参数名
    String value() default "";
    // 指定请求参数名
    String name() default "";
    // true 表示发起请求时这个参数必须存在
    boolean required() default true;
    String defaultValue() default ValueConstants.DEFAULT_NONE;}

有两个参数需要注意,一个是 name 表示这个参数与 GET 传参的哪个关联;required 表示这个参数是否可选

下面是一个简单的使用方式

@GetMapping(path = "ano")
public String anoParam(@RequestParam(name = "name") String uname,
        @RequestParam(name = "age", required = false) Integer age,
        @RequestParam(name = "uids", required = false) Integer[] uids) {return "name:" + uname + "age:" + age + "uids:" + (uids != null ? Arrays.asList(uids) : "null");
}

测试如下:

# 三个参数全在
➜  ~ curl 'http://localhost:8080/get/ano?name=%E4%B8%80%E7%81%B0%E7%81%B0blog&age=18&uids=1,3,4'
name: 一灰灰 blog age: 18 uids: [1, 3, 4]%
# age 不传
➜  ~ curl 'http://localhost:8080/get/ano?name=%E4%B8%80%E7%81%B0%E7%81%B0blog&uids=1,3,4'
name: 一灰灰 blog age: null uids: [1, 3, 4]% 
# 必选参数 name 不传时
➜  ~ curl 'http://localhost:8080/get/ano?uids=1,3,4' -i
HTTP/1.1 500
Content-Length: 0
Date: Sat, 24 Aug 2019 13:09:07 GMT
Connection: close

使用 RequestParam 注解时,如果指定了name/value,这个参数就与指定的 GETGET 传参关联;如果不指定时,则根据参数签名来关联

下面给出两个更有意思的使用方式,一个是枚举参数解析,一个是 Map 容纳参数,一个是数组参数解析

public enum TYPE {A, B, C;}

@GetMapping(path = "enum")
public String enumParam(TYPE type) {return type.name();
}

@GetMapping(path = "enum2")
public String enumParam2(@RequestParam TYPE type) {return type.name();
}

@GetMapping(path = "mapper")
public String mapperParam(@RequestParam Map<String, Object> params) {return params.toString();
}

// 注意下面这个写法,无法正常获取请求参数,这里用来对比列出
@GetMapping(path = "mapper2")
public String mapperParam2(Map<String, Object> params) {return params.toString();
}


@GetMapping(path = "ano1")
public String anoParam1(@RequestParam(name = "names") List<String> names) {return "name:" + names;}

// 注意下面这个写法无法正常解析数组
@GetMapping(path = "arg3")
public String anoParam2(List<String> names) {return "names:" + names;}

测试 case 如下

➜  ~ curl 'http://localhost:8080/get/enum?type=A'
A%
➜  ~ curl 'http://localhost:8080/get/enum2?type=A'
A%
➜  ~ curl 'http://localhost:8080/get/mapper?type=A&age=3'
{type=A, age=3}%
➜  ~ curl 'http://localhost:8080/get/mapper2?type=A&age=3'
{}%
➜  ~ curl 'http://localhost:8080/get/ano1?names=yi,hui,ha'
name: [yi, hui, ha]%
➜  ~ curl 'http://localhost:8080/get/arg3?names=yi,hui,ha' -i
HTTP/1.1 500
Content-Length: 0
Date: Sat, 24 Aug 2019 13:50:55 GMT
Connection: close

从测试结果可以知道:

  • GET 传参映射到枚举时,根据 enum.valueOf() 来实例的
  • 如果希望使用 Map 来容纳所有的传参,需要加上注解@RequestParam
  • 如果参数为 List 类型,必须添加注解@RequestParam;否则用数组来接收

4. PathVariable

从请求的 url 路径中解析参数,使用方法和前面的差别不大

@GetMapping(path = "url/{name}/{index}")
public String urlParam(@PathVariable(name = "name") String name,
        @PathVariable(name = "index", required = false) Integer index) {return "name:" + name + "index:" + index;}

上面是一个常见的使用方式,对此我们带着几个疑问设计 case

  • 只有 name 没有 index,会怎样?
  • 有 name,有 index,后面还有路径,会怎样?
➜  ~ curl http://127.0.0.1:8080/get/url/yihhuihui/1
name: yihhuihui index: 1%

➜  ~ curl 'http://127.0.0.1:8080/get/url/yihhuihui' -i
HTTP/1.1 500
Content-Length: 0
Date: Sat, 24 Aug 2019 13:27:08 GMT
Connection: close

➜  ~ curl 'http://127.0.0.1:8080/get/url/yihhuihui/1/test' -i
HTTP/1.1 500
Content-Length: 0
Date: Sat, 24 Aug 2019 13:27:12 GMT
Connection: close

从 path 中获取参数时,对 url 有相对严格的要求,注意使用


5. POJO

这种 case,我个人用得比较多,特别是基于 SpringCloud 的生态下,借助 Feign 来调用第三方微服务,可以说是很舒爽了;下面看一下这种方式的使用姿势

首先定义一个 POJO

@Data
public class BaseReqDO implements Serializable {
    private static final long serialVersionUID = 8706843673978981262L;

    private String name;

    private Integer age;

    private List<Integer> uIds;
}

提供一个服务

@GetMapping(path = "bean")
public String beanParam(BaseReqDO req) {return req.toString();
}

POJO 中定义了三个参数,我们再测试的时候,看一下这些参数是否必选

# GET 传参与 POJO 中成员名进行关联
➜  ~ curl 'http://127.0.0.1:8080/get/bean?name=yihuihui&age=18&uIds=1,3,4'
BaseReqDO(name=yihuihui, age=18, uIds=[1, 3, 4])%
# 没有传参的属性为 null;因此如果 POJO 中成员为基本类型,则参数必传
➜  ~ curl 'http://127.0.0.1:8080/get/bean?name=yihuihui&age=18'
BaseReqDO(name=yihuihui, age=18, uIds=null)%

II. 其他

0. 项目

  • 工程:https://github.com/liuyueyi/spring-boot-demo
  • 项目: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/202-web-params

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰 Blog 个人博客 https://blog.hhui.top
  • 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top

退出移动版