При вызове RestTemplate.exchange для выполнения запроса на получение, например:
String foo = "fo+o";
String bar = "ba r";
restTemplate.exchange("http://example.com/?foo = {foo}&bar = {bar}", HttpMethod.GET, null, foo, bar)
что правильно, чтобы переменные URL-адреса правильно экранировались для запроса на получение?
В частности, как мне получить плюсы (+), правильно экранированные, потому что Spring интерпретируется как пробелы, поэтому мне нужно их закодировать.
Я пробовал использовать UriComponentsBuilder вот так:
String foo = "fo+o";
String bar = "ba r";
UriComponentsBuilder ucb = UriComponentsBuilder.fromUriString("http://example.com/?foo = {foo}&bar = {bar}");
System.out.println(ucb.build().expand(foo, bar).toUri());
System.out.println(ucb.build().expand(foo, bar).toString());
System.out.println(ucb.build().expand(foo, bar).toUriString());
System.out.println(ucb.build().expand(foo, bar).encode().toUri());
System.out.println(ucb.build().expand(foo, bar).encode().toString());
System.out.println(ucb.build().expand(foo, bar).encode().toUriString());
System.out.println(ucb.buildAndExpand(foo, bar).toUri());
System.out.println(ucb.buildAndExpand(foo, bar).toString());
System.out.println(ucb.buildAndExpand(foo, bar).toUriString());
System.out.println(ucb.buildAndExpand(foo, bar).encode().toUri());
System.out.println(ucb.buildAndExpand(foo, bar).encode().toString());
System.out.println(ucb.buildAndExpand(foo, bar).encode().toUriString());
и напечатал:
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba r
http://example.com/?foo=fo+o&bar=ba r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba r
http://example.com/?foo=fo+o&bar=ba r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
В некоторых случаях пробел экранируется правильно, но плюс никогда не экранируется.
Я тоже пробовал UriTemplate вот так:
String foo = "fo+o";
String bar = "ba r";
UriTemplate uriTemplate = new UriTemplate("http://example.com/?foo = {foo}&bar = {bar}");
Map<String, String> vars = new HashMap<>();
vars.put("foo", foo);
vars.put("bar", bar);
URI uri = uriTemplate.expand(vars);
System.out.println(uri);
с тем же результатом:
http://example.com/?foo=fo+o&bar=ba%20r
@ShayElkayam: ты в этом уверен?
Вы можете использовать UriComponentsBuilder в Spring (org.springframework.web.util.UriComponentsBuilder)
String url = UriComponentsBuilder
.fromUriString("http://example.com/")
.queryParam("foo", "fo+o")
.queryParam("bar", "ba r")
.build().toUriString();
restTemplate.exchange(url , HttpMethod.GET, httpEntity);
Вы имеете в виду foo вместо "{foo}"?
Вы можете просто использовать переменные foo и bar, как я сделал в своем вопросе. Другое отличие состоит в том, что я использую в своем вопросе шаблон. Не уверен, что это актуально, но использование шаблона с UriCompontentsBuilder, похоже, не позволяет избежать +.
См. Обновленный ответ. UriComponentsBuilder соответствующим образом ускользнет
Я начинаю верить, что это ошибка, и сообщил здесь: https://jira.spring.io/browse/SPR-16860
В настоящее время мой обходной путь таков:
String foo = "fo+o";
String bar = "ba r";
String uri = UriComponentsBuilder.
fromUriString("http://example.com/?foo = {foo}&bar = {bar}").
buildAndExpand(vars).toUriString();
uri = uri.replace("+", "%2B"); // This is the horrible hack.
try {
return new URI(uriString);
} catch (URISyntaxException e) {
throw new RuntimeException("UriComponentsBuilder generated an invalid URI.", e);
}
это ужасный взлом, который может потерпеть неудачу в некоторых ситуациях.
См. Ответ, который я опубликовал, он использует все из URIBuilder и добавляет обходной путь для кодирования и отправки компонентов запроса.
Я думаю, ваша проблема здесь в том, что RFC 3986, на котором основаны UriComponents и расширение UriTemplate, не требует экранирования + в строке запроса.
Взгляд спецификации на это прост:
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / " = "
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
query = *( pchar / "/" / "?" )
URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
Если ваша веб-платформа (например, Spring MVC!) Интерпретирует + как пространство, то это ее решение и не требуется в соответствии со спецификацией URI.
Что касается вышеизложенного, вы также увидите, что !$'()*+,; не экранируются с помощью UriTemplate. = и &находятся экранированы, потому что Spring придерживается «самоуверенного» взгляда на то, как выглядит строка запроса - последовательность пар ключ = значение.
Аналогично, #[] и пробел находятся экранированы, потому что они недопустимы в строке запроса в соответствии со спецификацией.
Конечно, ничто из этого вряд ли станет для вас утешением, если вы вполне разумно хотите, чтобы параметры вашего запроса были экранированы!
Чтобы на самом деле закодировать параметры запроса, чтобы ваша веб-платформа могла их переносить, вы можете использовать что-то вроде org.springframework.web.util.UriUtils.encode(foo, charset).
Проблема в том, что Spring здесь непоследовательна. И если я предварительно закодирую + как% 2B, вызвав UriUtils.encode, тогда% будет дополнительно закодирован с помощью UriTemplate, нарушающего строку.
Не думаю, что это непоследовательно. Если вы уже закодировали свои параметры, Spring не может этого знать. Вы можете сказать, что уже закодировали их, например UriComponentsBuilder.fromUriString("http://example.com/?foo={foo}").queryParams(...).build(true).toUriString()
Для этого я все же предпочел бы, чтобы кодировка решалась с помощью правильного метода, а не с помощью взлома, как вы. Я бы просто использовал что-то вроде ниже
String foo = "fo+o";
String bar = "ba r";
MyUriComponentsBuilder ucb = MyUriComponentsBuilder.fromUriString("http://example.com/?foo = {foo}&bar = {bar}");
UriComponents uriString = ucb.buildAndExpand(foo, bar);
// http://example.com/?foo=fo%252Bo&bar=ba+r
URI x = uriString.toUri();
// http://example.com/?foo=fo%2Bo&bar=ba+r
String y = uriString.toUriString();
// http://example.com/?foo=fo%2Bo&bar=ba+r
String z = uriString.toString();
И, конечно, класс, как показано ниже
class MyUriComponentsBuilder extends UriComponentsBuilder {
protected UriComponentsBuilder originalBuilder;
public MyUriComponentsBuilder(UriComponentsBuilder builder) {
// TODO Auto-generated constructor stub
originalBuilder = builder;
}
public static MyUriComponentsBuilder fromUriString(String uri) {
return new MyUriComponentsBuilder(UriComponentsBuilder.fromUriString(uri));
}
@Override
public UriComponents buildAndExpand(Object... values) {
// TODO Auto-generated method stub
for (int i = 0; i< values.length; i ++) {
try {
values[i] = URLEncoder.encode((String) values[i], StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return originalBuilder.buildAndExpand(values);
}
}
Все еще не самый чистый из возможных способов, но лучше, чем жестко запрограммированный подход к замене.
По-видимому, правильный способ сделать это - определить фабрику и изменить режим кодирования:
String foo = "fo+o";
String bar = "ba r";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
URI uri = factory.uriString("http://example.com/?foo = {foo}&bar = {bar}").build(foo, bar);
System.out.println(uri);
Это распечатывает:
http://example.com/?foo=fo%2Bo&bar=ba%20r
Это задокументировано здесь: https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#web-uri-encoding
Думаю, это решение можно применить и для этот вопрос.
Используйте UriTemplate и вызовите его. Он вернет строку с правильно экранированным URL-адресом.