Каков правильный способ избежать URL-переменных с Spring RestTemplate при вызове Spring RestController?

При вызове 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

Ответ 1

По-видимому, правильный способ сделать это - определить фабрику и изменить режим кодирования:

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

Ответ 2

Я думаю, что ваша проблема здесь в том, что 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).

Ответ 3

Я начинаю верить, что это ошибка, и я сообщил здесь: 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);
}

который является ужасным взломом, который может потерпеть неудачу в некоторых ситуациях.

Ответ 4

Для этого я все же предпочел бы, чтобы кодирование было разрешено с использованием правильного метода вместо использования Hack, как вы. Я бы просто использовал что-то вроде ниже

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);
    }

}

Тем не менее, это не самый чистый способ, но лучше, чем делать жесткую замену

Ответ 5

Вы можете использовать UriComponentsBuilder весной (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);