我遇到了一种奇怪的情况,我在https://github.com/lgueye/uri-parameters-behavior
中转载了因为当我们以 GET
方法请求后端之一时,我们已迁移到 spring-boot 2 ( spring framework 5 )我们遇到以下情况:当所有具有 +
字符的字段到达后端时,它们都被更改为 (空白)字符>
以下值已更改:
我在stackoverflow(https://github.com/spring-projects/spring-framework/issues/14464#issuecomment-453397378)和github(https://github.com/spring-projects/spring-framework/issues/21577)上花费了很多时间
我已经实现了mockMvc单元测试和集成测试
单元测试行为正常 集成测试失败(例如我们的产品)
有人可以帮助我解决此问题吗?我的目标显然是使集成测试通过。
谢谢您的帮助。
路易
答案 0 :(得分:0)
在解决了这个问题之后,我终于使它按照我们公司的期望工作了。
有问题的组件不是 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的必要区别:
我有点难过,因为所有这些都很容易出错且麻烦(特别是Map / MultiValueMap部分)。我很乐意让它们从Java bean生成。
这对我们的解决方案影响很大,但恐怕我们别无选择。我们将暂时解决此问题。
希望这有助于其他人解决此问题。
最好
路易
答案 1 :(得分:0)
整个失调是由于存在一种非标准的做法,即如何将空间编码/解码为"+"
。
可以说空间可以被编码为"+"
或"%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
指出必须使用"%20"
对URL中的空格进行编码。
这基本上意味着,根据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
正确编码。>