1. 概述

在这个教程中,我们将学习如何在Spring的*RestTemplate*中编码URI变量。

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

接下来,我们将探讨几种解决这个问题的方法。

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调用

现在,我们创建一个服务类来调用公开API*http://httpbin.org/get*。

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

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

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

3.1. 服务类

现在,我们创建一个服务类,它调用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,然后从args对象内的字段中获取parameter的值。

3.2. 测试

现在,我们测试服务类,分别使用参数springbootspring+boot,检查响应是否符合预期:

@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. 使用RestTemplate拦截器

我们可以使用拦截器来编码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对象,它包装了原始请求。
  • 对于这个包装器,我们重写了getURI()方法来对URI变量进行编码。在这种情况下,我们在查询字符串中将加号替换为%2B
  • 使用UriComponentsBuilder,我们创建一个新的URI,并将编码后的查询字符串替换回原处。
  • intercept()方法返回编码后的请求,这将替换原始请求。

4.1. 添加拦截器

接下来,我们需要将拦截器添加到RestTemplate bean中:

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

再次运行测试,我们会看到它通过了。

拦截器提供了灵活性,可以改变我们想要的任何请求部分。对于复杂的场景,如添加额外的头部或对请求字段进行更改,它们非常有用。

对于像我们示例这样简单的任务,我们也可以使用DefaultUriBuilderFactory来修改编码。下面我们看看如何操作。

5. 使用DefaultUriBuilderFactory

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

默认情况下,URI构建器先对整个URL进行编码,然后再单独对值进行编码。我们将创建一个新的DefaultUriBuilderFactory对象,并设置编码模式为VALUES_ONLY。这仅限于对值进行编码。

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

让我们用这种方式创建一个新的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变量。我们讨论了两种方法:使用拦截器和修改DefaultUriBuilderFactory对象。

如文中所述的代码示例,您可以在GitHub上找到。