正确解析包含“+”字符的字段
Posted
技术标签:
【中文标题】正确解析包含“+”字符的字段【英文标题】:Properly parse field containing '+' char 【发布时间】:2019-08-13 15:59:42 【问题描述】:我遇到了一个奇怪的情况,我在https://github.com/lgueye/uri-parameters-behavior 中转载了
由于我们在 GET
方法中请求我们的后端之一时迁移到了 spring-boot 2(spring 框架 5),因此我们运行了进入以下情况:所有具有 +
字符的字段在到达后端时都被更改为 (空白)字符
以下值已更改:
+412386789(电话号码)转为** 412386789** 2019-03-22T17:18:39.621+02:00 (java8 ZonedDateTime) 变为 2019-03-22T17:18:39.621 02:00(导致 >org.springframework.validation.BindException我在 *** (https://github.com/spring-projects/spring-framework/issues/14464#issuecomment-453397378) 和 github (https://github.com/spring-projects/spring-framework/issues/21577) 上花了很长时间
我已经实现了一个 mockMvc 单元测试和一个集成测试
单元测试正常运行 集成测试失败(就像我们的生产一样)
谁能帮我解决这个问题?我的目标显然是让集成测试通过。
感谢您的帮助。
路易
【问题讨论】:
【参考方案1】:整个错位来自这样一个事实,即如何将空间编码/解码为"+"
的非标准做法。
可以说空间可以(正在)编码为"+"
或"%20"
。
例如 Google 对搜索字符串这样做:
https://www.google.com/search?q=test+my+space+delimited+entry
rfc1866, section-8.2.2
声明 GET 请求的查询部分应编码为 'application/x-www-form-urlencoded'
。
所有表单的默认编码是`application/x-www-form- urlencoded'。表单数据集在此媒体类型中表示为 如下:
表单字段名称和值被转义:空格 字符被替换为“+”。
另一方面,rfc3986
声明 URL 中的空格必须使用 "%20"
进行编码。
这基本上意味着对空格进行编码有不同的标准,具体取决于它们在 URI syntax components 中的位置。
foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/\_________/ \_________/ \__/
| | | | |
scheme authority path query fragment
| _____________________|__
/ \ / \
urn:example:animal:ferret:nose
基于这些评论,我们可以说明在 URI 中的 GET http 调用中:
"?"
之前的空格需要编码为"%20"
查询参数中"?"
后面的空格需要编码为"+"
这意味着"+"
符号需要在查询参数中编码为"%2B"
Spring 实现遵循 rfc 规范,这就是为什么当您在查询参数中发送 "+412386789" 时,"+"
符号被解释为空白字符,并以“412386789”。
看着:
final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost")
.port(port)
.path("/events")
.queryParams(params)
.build()
.toUri();
你会发现:
"foo#bar@quizz+foo-bazz//quir."
编码为"foo%23bar@quizz+foo-bazz//quir."
符合规范 (rfc3986
)。
因此,如果您希望查询参数中的 "+"
字符不被解释为空格,则需要将其编码为 "%2B"
。
您发送到后端的参数应如下所示:
params.add("id", id);
params.add("device", device);
params.add("phoneNumber", "%2B225697845");
params.add("timestamp", "2019-03-25T15%3A09%3A44.703088%2B02%3A00");
params.add("value", "foo%23bar%40quizz%2Bfoo-bazz%2F%2Fquir.");
为此,您可以在将参数传递给地图时使用UrlEncoder
。当心 UriComponentsBuilder 双重编码你的东西!
您可以通过以下方式获得正确的 URL:
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("id", id);
params.add("device", device);
String uft8Charset = StandardCharsets.UTF_8.toString();
params.add("phoneNumber", URLEncoder.encode(phoneNumber, uft8Charset));
params.add("timestamp", URLEncoder.encode(timestamp.toString(), uft8Charset));
params.add("value", URLEncoder.encode(value, uft8Charset));
final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost")
.port(port)
.path("/events")
.queryParams(params)
.build(true)
.toUri();
请注意,将“true”传递给build()
方法会关闭编码,因此这意味着来自URI 部分的方案、主机等不会被UriComponentsBuilder
正确编码。
【讨论】:
【参考方案2】:在与这个问题进行了一番斗争后,我终于让它按照我们期望的方式在我们公司工作。
有问题的组件不是 spring-boot 而是 UriComponentsBuilder
我最初的失败测试如下所示:
@Test
public void get_should_properly_convert_query_parameters()
// Given
final String device = UUID.randomUUID().toString();
final String id = UUID.randomUUID().toString();
final String phoneNumber = "+225697845";
final String value = "foo#bar@quizz+foo-bazz//quir.";
final Instant now = Instant.now();
final ZonedDateTime timestamp = ZonedDateTime.ofInstant(now, ZoneId.of("+02:00"));
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("id", id);
params.add("device", device);
params.add("phoneNumber", phoneNumber);
params.add("timestamp", timestamp.toString());
params.add("value", value);
final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost").port(port).path("/events").queryParams(params).build().toUri();
final Event expected = Event.builder().device(device).id(id).phoneNumber(phoneNumber).value(value).timestamp(timestamp).build();
// When
final Event actual = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), Event.class).getBody();
// Then
assertEquals(expected, actual);
工作版本如下所示:
@Test
public void get_should_properly_convert_query_parameters()
// Given
final String device = UUID.randomUUID().toString();
final String id = UUID.randomUUID().toString();
final String phoneNumber = "+225697845";
final String value = "foo#bar@quizz+foo-bazz//quir.";
final Instant now = Instant.now();
final ZonedDateTime timestamp = ZonedDateTime.ofInstant(now, ZoneId.of("+02:00"));
final Map<String, String> params = new HashMap<>();
params.put("id", id);
params.put("device", device);
params.put("phoneNumber", phoneNumber);
params.put("timestamp", timestamp.toString());
params.put("value", value);
final MultiValueMap<String, String> paramTemplates = new LinkedMultiValueMap<>();
paramTemplates.add("id", "id");
paramTemplates.add("device", "device");
paramTemplates.add("phoneNumber", "phoneNumber");
paramTemplates.add("timestamp", "timestamp");
paramTemplates.add("value", "value");
final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost").port(port).path("/events").queryParams(paramTemplates).encode().buildAndExpand(params).toUri();
final Event expected = Event.builder().device(device).id(id).phoneNumber(phoneNumber).value(value).timestamp(ZonedDateTime.ofInstant(now, ZoneId.of("UTC"))).build();
// When
final Event actual = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), Event.class).getBody();
// Then
assertEquals(expected, actual);
注意 4 所需的差异:
需要 MultiValueMap 参数模板 需要映射参数值 需要编码 需要带有参数值的 buildAndExpand我有点难过,因为所有这些都非常容易出错且很麻烦(特别是 Map/MultiValueMap 部分)。我很乐意让它们从 java bean 生成。
这对我们的解决方案影响很大,但恐怕我们别无选择。我们现在就接受这个解决方案。
希望这可以帮助其他面临此问题的人。
最好的,
路易
【讨论】:
以上是关于正确解析包含“+”字符的字段的主要内容,如果未能解决你的问题,请参考以下文章
为 EXECUTE IMMEDIATE 解析 PL/SQL 语句中的占位符