对 RestTemplate 上的 URI 变量进行编码

1、概览

我们经常遇到的一个编码问题是,URI 变量中包含一个加号(+)。例如,如果我们的 URI 变量值为 http://localhost:8080/api/v1/plus+sign,那么加号将被编码为空格,这可能会导致意外的服务器响应。

在本教程中,我们将学习如何在 Spring 的 RestTemplate 上对 URI 变量进行编码。

让我们来看看解决这个问题的几种方法。

2、项目设置

创建一个使用 RestTemplate 进行 API 调用的小项目。

2.1、Spring Web 依赖

在 pom.xml 中添加 Spring Web Starter 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

或者,可以使用 Spring Initializr 生成项目并添加依赖。

2.2、RestTemplate Bean

创建一个 RestTemplate Bean:

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

3、 调用 API

创建一个 service 类,调用公共 API http://httpbin.org/get

API 会返回一个包含请求参数的 JSON 响应。例如,如果我们在浏览器上访问 URL https://httpbin.org/get?parameter=springboot,就会得到以下响应:

{
  "args": {
    "parameter": "springboot"
  },
  "headers": {
  },
  "origin": "",
  "url": ""
}

这里的 args 对象包含请求参数。为简洁起见,省略了其他值。

3.1、Service 类

创建一个 service 类,调用 API 并返回 parameter 的值:

@Service
public class HttpBinService {
    private final RestTemplate restTemplate;

    public HttpBinService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public String get(String parameter) {
        String url = "http://httpbin.org/get?parameter={parameter}";
        ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class, parameter);
        Map<String, String> args = (Map<>) response.getBody().get("args");
        return args.get("parameter");
    }
}

get() 方法调用指定的 URL,将响应解析为 Map,并检索、返回其中的 parameter 值。

3.2、测试

使用 2 个不同的参数:springbootspring+boot。来测试 service 类,并检查响应是否符合预期:

@SpringBootTest
class HttpBinServiceTest {
    @Autowired
    private HttpBinService httpBinService;

    @Test
    void givenWithoutPlusSign_whenGet_thenSameValueReturned() throws JsonProcessingException {
        String parameterWithoutPlusSign = "springboot";
        String responseWithoutPlusSign = httpBinService.get(parameterWithoutPlusSign);
        assertEquals(parameterWithoutPlusSign, responseWithoutPlusSign);
    }

    @Test
    void givenWithPlusSign_whenGet_thenSameValueReturned() throws JsonProcessingException {
        String parameterWithPlusSign = "spring+boot";
        String responseWithPlusSign = httpBinService.get(parameterWithPlusSign);
        assertEquals(parameterWithPlusSign, responseWithPlusSign);
    }
}

运行测试,就会发现第二个测试失败了。响应是 spring boot,而不是 spring+boot

4、使用 Interceptor

我们可以使用 interceptor 对 URI 变量进行编码。

创建一个实现 ClientHttpRequestInterceptor 接口的类:

public class UriEncodingInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        HttpRequest encodedRequest = new HttpRequestWrapper(request) {
            @Override
            public URI getURI() {
                URI uri = super.getURI();
                String escapedQuery = uri.getRawQuery().replace("+", "%2B");
                return UriComponentsBuilder.fromUri(uri)
                  .replaceQuery(escapedQuery)
                  .build(true).toUri();
            }
        };
        return execution.execute(encodedRequest, body);
    }
}

实现 intercept() 方法。该方法将在 RestTemplate 发出每个请求之前执行。

代码分析:

  • 我们创建了一个新的 HttpRequest 对象来封装原始 request。
  • 在这个 wrapper 中,我们覆盖 getURI() 方法,对 URI 变量进行编码。在本例中,我们将查询字符串中的加号替换为 %2B
  • 使用 UriComponentsBuilder,我们创建了一个新的 URI,并用编码后的查询字符串替换了查询字符串。
  • 我们从 intercept() 方法中返回编码后的 request,它将取代原始 request。

4.1、添加 Interceptor

接下来,我们需要在 RestTemplate Bean 中添加 interceptor:

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(new UriEncodingInterceptor()));
        return restTemplate;
    }
}

再次运行测试,就会发现测试通过了。

Interceptor 可以灵活地更改请求的任何部分,如添加额外的 Header 或对请求中的字段进行更改。

对于像编码参数这样简单的任务,也可以使用 DefaultUriBuilderFactory 来更改编码。

5、使用 DefaultUriBuilderFactory

对 URI 变量进行编码的另一种方法是更改 RestTemplate 内部使用的 DefaultUriBuilderFactory 对象。

默认情况下,URI builder 首先会对整个 URL 进行编码,然后再分别对值进行编码。我们可以创建一个新的 DefaultUriBuilderFactory 对象,并将编码模式设置为 VALUES_ONLY。这样就仅对值进行编码。

然后,我们可以使用 setUriTemplateHandler() 方法在 RestTemplate Bean 中设置新的 DefaultUriBuilderFactory 对象。

让我们用它来创建一个新的 RestTemplate Bean:

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        DefaultUriBuilderFactory defaultUriBuilderFactory = new DefaultUriBuilderFactory();
        defaultUriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
        restTemplate.setUriTemplateHandler(defaultUriBuilderFactory);
        return restTemplate;
    }
}

这是对 URI 变量进行编码的另一种方式。同样,运行测试,就会发现测试通过了。

6、总结

在本文中,我们了解了如何对 RestTemplate 请求中的 URI 变量进行编码。我们看到了两种方法:使用 Interceptor 和更改 DefaultUriBuilderFactory 对象。


参考:https://www.baeldung.com/spring-resttemplate-uri-variables-encode