对 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 个不同的参数:springboot
和 spring+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