2

我遇到了一个奇怪的情况,我在https://github.com/lgueye/uri-parameters-behavior中复制了它

由于我们在方法中请求我们的一个后端时迁移到spring-boot 2 ( spring framework 5GET ),我们遇到了以下情况:所有带有 char 的字段在到达后端时+都被更改为(whitespace) char

以下值已更改:

  • +412386789(电话号码)转入** 412386789**
  • 2019-03-22T17:18:39.621+02:00 (java8 ZonedDateTime) 进入2019-03-22T17:18:39.621 02:00(导致org.springframework.validation.BindException

我在 stackoverflow ( https://github.com/spring-projects/spring-framework/issues/14464#issuecomment-453397378 ) 和 github ( https://github.com/spring-projects/spring -框架/问题/21577

我已经实现了一个 mockMvc 单元测试和一个集成测试

单元测试正常运行集成测试失败(如我们的生产)

任何人都可以帮我解决这个问题吗?我的目标显然是让集成测试通过。

谢谢您的帮助。

路易斯

4

2 回答 2

1

整个错位来自这样一个事实,即如何将空间编码/解码为"+".

可以说,空间可以(正在)编码为"+"or "%20"

例如,谷歌对搜索字符串这样做:

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”。表单数据集在此媒体类型中表示
如下:

  1. 表单字段名称和值被转义:空格 字符替换为 '+'

另一方面rfc3986指出,URL 中的空格必须使用"%20".

这基本上意味着对空格进行编码有不同的标准,具体取决于它们在 URI语法组件中的位置。

     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)。

因此,如果您希望"+"查询参数中的 char 不被解释为空格,则需要将其编码为"%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.

于 2019-03-25T13:16:53.837 回答
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 所需差异:

  • 需要 MultiValueMap 参数模板
  • 地图参数值是必需的
  • 需要编码
  • 需要带有参数值的 buildAndExpand

我有点难过,因为所有这些都非常容易出错且很麻烦(特别是 Map/MultiValueMap 部分)。我很乐意让它们从 Java bean 生成。

这对我们的解决方案有很大影响,但恐怕我们别无选择。我们现在将满足于这个解决方案。

希望这有助于其他人面临这个问题。

最好的,

路易斯

于 2019-03-25T10:35:15.003 回答