Spring RestTemplate URL编码

27

我尝试使用Spring的RestTemplate进行简单的REST调用:

private void doLogout(String endpointUrl, String sessionId) {
    template.getForObject("http://{enpointUrl}?method=logout&session={sessionId}", Object.class,
            endpointUrl, sessionId);
}

其中 endpointUrl 变量包含类似 service.host.com/api/service.php 的内容。

不幸的是,我的调用结果导致 org.springframework.web.client.ResourceAccessException: I/O error: service.host.com%2Fapi%2Fservice.php

因此,Spring 似乎在创建 URL 时对我的 endpointUrl 字符串进行了编码。有没有简单的方法来防止 Spring 这样做呢?

敬礼


我希望这是更好的解决方案-https://dev59.com/ZmEi5IYBdhLWcg3w2fYm#56027025 它展示了如何避免Spring的RestTemplate URL编码问题。 - Dmitry Trifonov
7个回答

27

这并不容易。URI模板变量通常用于路径元素或查询字符串参数。您试图传递一个主机。理想情况下,您应该寻找更好的解决方案来构建URI。我建议使用Yuci的解决方案

如果您仍想使用Spring工具和模板扩展,则可使用UriTemplate创建包含URI变量的URL,然后对其进行URL解码并将其传递给您的RestTemplate

String url = "http://{enpointUrl}?method=logout&session={sessionId}";
URI expanded = new UriTemplate(url).expand(endpointUrl, sessionId); // this is what RestTemplate uses 
url = URLDecoder.decode(expanded.toString(), "UTF-8"); // java.net class
template.getForObject(url, Object.class);

这解决了我URL中转义斜线的问题。但不幸的是,它并没有解决我的整个问题。在某些情况下,“payload”包含JSON字符串。此JSON字符串在rest模板代码内的某处进行编码。传递给模板getForObject方法的String URL看起来符合预期。但是当spring尝试扩展我的URL的json部分时,我收到了一个异常... - user1145874
@user1145874 你需要提出另一个问题并提供所有细节,但我建议不要将JSON作为查询字符串请求参数。请在POST或PUT的正文中发送它。 - Sotirios Delimanolis
1
你正在做不必要的工作:1)你使用expanded.toString()对uri进行编码,2)然后你将其解码回url,3)之后它将在template.getForObject()中再次被编码。最好使用清晰的方法(使用URI对象而不是字符串)。请参见我在此问题的答案中的解释-https://dev59.com/ZmEi5IYBdhLWcg3w2fYm#56027025 - Dmitry Trifonov
@DmitryTrifonov 我试图澄清这样的转换可以通过模板变量解析用于生成URI(“没有简单的方法可以做到这一点”)。如果他们可以在没有它的情况下正确构建URL,那么那是更好的解决方案。 - Sotirios Delimanolis

17

取决于您使用的Spring版本。例如,如果您的版本太旧,例如版本3.0.6.RELEASE,则您将没有UriComponentsBuilder等工具与您的spring-web jar一起使用。

您需要做的是防止Spring RestTemplate对URL进行编码。您可以执行以下操作:

import java.net.URI;

StringBuilder builder = new StringBuilder("http://");
builder.append(endpointUrl);
builder.append("?method=logout&session=");
builder.append(sessionId);

URI uri = URI.create(builder.toString());
restTemplate.getForObject(uri, Object.class);

我使用Spring版本3.0.6.RELEASE进行了测试,它能正常工作。

简而言之,不要使用restTemplate.getForObject(String url, Object.class),而是使用restTemplate.getForObject(java.net.URI uri, Object.class)

请参见Spring文档中的rest-resttemplate-uri章节


15

看起来我找到了最佳的本地化(最新)解决方案:

  1. 不要将编码后的url字符串作为参数传递给RestTemplate.exchange()
  2. 使用URI对象代替。使用UriComponentsBuilder构建URI。

请参见下面(简化的)示例:

    String instanceUrl = "https://abc.my.salesforce.com"
    HttpEntity<String> entity = new HttpEntity<>(headers);
    UriComponents uriComponents =
            UriComponentsBuilder.fromHttpUrl(instanceUrl)
                    .path("/services/data/v45.0/query/")
                    .queryParam("q", String.format(sqlSelect, id))
                    .build();

    ResponseEntity<OpportunityLineItem> responseEntity =
            restTemplate.exchange(
                    uriComponents.toUri(), HttpMethod.GET,
                    entity, OpportunityLineItem.class);

// Wrong! URI string will be double encoded
/*
ResponseEntity<OpportunityLineItem> responseEntity =
            restTemplate.exchange(
                    uriComponents.toUriString(), HttpMethod.GET,
                    entity, OpportunityLineItem.class);
*/

这样,您就不会遇到双重编码的问题。

在调试基于Spring RestTemplate客户端(包括SOQL查询)的SalesForce REST客户端时,找到了解决方案。


1
如果传递给“getForObject”的URI字符串正确构建,他们将不会有编码问题。因此,他们的问题的要点是如何构建正确的URI。他们在endpointUrlsessionId参数中拥有URI的一部分,并且在传递给getForObject的String字面量中也有一部分。他们需要一个可以协调各个部分的东西。您已经跳过了所有这些并假设存在“instanceUrl”。在他们的问题背景下,这是什么?它的价值从哪里来?您是如何构建它的? - Sotirios Delimanolis
查询参数不是问题所在。问题在于endpointUrl变量中主机service.host.com和路径/api/service.php的解析。 - Sotirios Delimanolis
我再次修改了示例,以使其更清晰易懂。我的意图是展示正确使用 UriComponentsBuilder 以避免 URL 编码问题。 - Dmitry Trifonov

5
你可以使用重载的方法,它接受一个java.net.URI参数: public T getForObject(URI url, Class responseType) throws RestClientException
来自Spring官方文档
UriComponents uriComponents =
    UriComponentsBuilder.fromUriString("http://example.com/hotels/{hotel}/bookings/{booking}").build()
        .expand("42", "21")
        .encode();

URI uri = uriComponents.toUri();

3

显然,调用UriComponentsBuilder类的build(true)会更好:

private void doLogout(String endpointUrl, String sessionId) {
    String url = "http://" + endpointUrl +"?method=logout&session=" + + URLEncoder.encode(sessionId, "UTF-8");
    URI uri = UriComponentsBuilder.fromUriString(url.toString()).build(true).toUri();
    template.getForObject(uri, Object.class,
            endpointUrl, sessionId);
}

通过该方法告诉URIComponentsBuilder在创建URI时不进行编码。


这个解决方案对我来说效果最好,其他所有的解决方案都没有编码或者编码错误。 - Shamshiel

1
完整的示例包含头部、主体,适用于任何HttpMethod和ResponseType,可能如下所示:
String url = "http://google.com/{path}?param1={param1Value}&param2={param2Value}";
Object body = null;
HttpEntity request = new HttpEntity(body, new HttpHeaders());

Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("path", "search");
uriVariables.put("param1Value", "value1");
uriVariables.put("param2Value", "value2");

ResponseEntity<Void> responseEntity = restTemplate.exchange(url, HttpMethod.POST, request, Void.class, uriVariables)
//responseEntity.getBody()

实际上,它将使用相同的UriTemplate和扩展方法。

URI变量将被编码,这与本问题相反。 - Pedro Madrid

0

最好的方法是使用UriComponentsBuilder:

UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
builder.queryParam("some_param", "param with space for encoding");
template.getForObject(builder.encode().build().toUri(), Object.class, headers);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接