RestTemplate 实战
1631字约5分钟
2024-06-25
全局配置
虽然我们可以在每次请求的时候手动设置请求头,但是反复设置重复内容造成的代码冗余十分不好,我们可以通过拦截器的方式,定制化自己想要达到的效果。在这里,我们设置通用的请求头内容,以及通过拦截器,我们将日志追踪的功能也一并实现,也不用每次请求的时候手动打印关键信息。基础内容移步 RestTemplate 详解
下图就是实现后的效果截图。
RestTemplate 配置
/**
* @ClassName RestTemplateConfig
* @Desciption RestTemplate 配置
* @Author MaRui
* @Date 2022/9/22 17:29
* @Version 1.0
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate demoRestTemplate(){
RestTemplate restTemplate = new RestTemplate();
// 配置自定义的 interceptor 拦截器
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(new HeadClientHttpRequestInterceptor());
interceptors.add(new TrackLogClientHttpRequestInterceptor());
// RestTemplate 中对字符串使用的是 StringHttpMessageConverter 中默认的编码
// 此编码为(ISO_8859_1)会引起中文乱码,故改用 FastJsonHttpMessageConverter
restTemplate.getMessageConverters().clear();
restTemplate.getMessageConverters().add(new FastJsonHttpMessageConverter());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
http 请求 headers 配置拦截器
/**
* @ClassName HeadClientHttpRequestInterceptor
* @Desciption http 请求 headers 配置拦截器
* @Author MaRui
* @Date 2022/9/22 17:37
* @Version 1.0
*/
public class HeadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
HttpHeaders headers = httpRequest.getHeaders();
// 当前浏览器(客户端)可以接受的文件类型
headers.add("Accept", "application/json");
// 当前浏览器(客户端)可以接受的数据编码。如果服务器吐出的数据不是浏览器可接受的编码,就会产生乱码。
headers.add("Accept-Encoding", "gzip");
// 返回内容的压缩编码类型,如“Content-Encoding :gzip”表示这次回包是以 gzip 格式压缩编码的,这种压缩格式可以减少流量的消耗。
headers.add("Content-Encoding", "UTF-8");
// 表示数据的格式。content-type 为不同的值时,浏览器会做不同的操作,
// 如果 content-type 是 application/octet-stream,表示数据是一个二进制流,此时浏览器会走下载文件的逻辑,而不是打开一个页面。
headers.add("Content-Type", "application/json; charset=UTF-8");
// 保证请求继续被执行
ClientHttpResponse httpResponse = clientHttpRequestExecution.execute(httpRequest, body);
// 获取响应的 headers
HttpHeaders headersResponse = httpResponse.getHeaders();
// 当前浏览器(客户端)可以接受的文件类型
headersResponse.add("Accept", "application/json");
return httpResponse;
}
}
http 请求日志追踪拦截器
/**
* @ClassName TrackLogClientHttpRequestInterceptor
* @Desciption http 请求日志追踪拦截器
* @Author MaRui
* @Date 2022/9/22 21:39
* @Version 1.0
*/
@Slf4j
public class TrackLogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 追踪请求前日志
trackRequest(httpRequest, body);
// 保证请求继续被执行
ClientHttpResponse httpResponse = execution.execute(httpRequest, body);
// 追踪请求后日志
trackResponse(httpResponse);
return httpResponse;
}
/**
* 追踪请求前日志
* @param request
* @param body
* @throws UnsupportedEncodingException
*/
private void trackRequest(HttpRequest request, byte[] body) throws UnsupportedEncodingException {
log.debug("======= request begin ========");
log.info("request uri : {}", request.getURI());
log.debug("method : {}", request.getMethod());
log.debug("headers : {}", request.getHeaders());
log.info("request body : {}", new String(body, "UTF-8"));
log.debug("======== request end =========");
}
/**
* 追踪请求后日志
* @param httpResponse
* @throws IOException
*/
private void trackResponse(ClientHttpResponse httpResponse) throws IOException {
log.debug("============================response begin==========================================");
log.info("Response Status code : {}", httpResponse.getStatusCode());
log.debug("Status text : {}", httpResponse.getStatusText());
log.debug("Headers : {}", httpResponse.getHeaders());
log.debug("============================response end============================================");
}
}
文件上传
提供测试的 controller 代码
/**
* @ClassName FileController
* @Desciption 文件控制层
* @Author MaRui
* @Date 2022/9/23 10:50
* @Version 1.0
*/
@Slf4j
@RestController
@RequestMapping("/file")
public class FileController {
/**
* 上传文件
*/
@PostMapping(value = "/fileUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResultDTO fileUpload(@RequestPart("file") MultipartFile mulFile) throws IOException {
File file = new File("D:\\temp\\a.png");
if(!file.exists()) {
file.mkdirs();
}
mulFile.transferTo(file);
return ResultDTO.ok();
}
}
测试上传代码
需要注意的是,这里使用的
RestTemplate
并不是我们全局配置中的,而是单独创建的一个对象。
/**
* 测试文件上传
*/
@Test
public void testFileUpload() {
// 请求地址
String reqUrl = "http://127.0.0.1:9999/file/fileUpload";
// 上传的文件
File file = new File("D:\\20211027c9025.png");
// 创建一个 RestTemplate 对象
RestTemplate restTemplate = new RestTemplate();
// 设置 headers 中 content-type 类型为 multipart/form-data
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// MultiValueMap 模拟表单提交。在 Postman 工具中,位于 body -》 form-data
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("file", new FileSystemResource(file));
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(form, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
// ==========>请求结果:ResultDTO(code=000000, msg=Success, data=null)
}
MultiValueMap
是一个继承 Map
的接口,一个 Key
可以对应多个 Value
,由 Spring
为我们提供的。
package org.springframework.util;
import java.util.List;
import java.util.Map;
import org.springframework.lang.Nullable;
public interface MultiValueMap<K, V> extends Map<K, List<V>> { // 代码省略 }
采用配置的 RestTemplate 出现的问题
如果采用了全局配置的 RestTemplate
发送文件上传请求,则会出现 the request was rejected because no multipart boundary was found
异常,如下图所示。
看异常信息提示:本次请求拒绝是因为没有 multipart boundary
,那这个 boundary
是什么呢?我们在平时浏览器上上传以及在 Postman
中进行文件上传测试,也从来没有设置过 boundary
呀,那很大可能是浏览器、Postman
中上传文件时候自动给我带上的,那这样猜测的话,那 RestTemplate
那也应该会给我们自动加上才对。
带着我们的猜测,我们需要做下面两件事:
1、找一个文件上传功能,在浏览器上,看一下有没有
boundary
这个东西。2、
RestTemplate
应该会给我们自动加上boundary
才对,Spring
提供这个功能肯定不可能缺少这个东西,是否是我们的全局配置引起的问题。
我们拿 Postman
试试,结果发现他的 Content-Type
并没有 boundary
这个东东,有点失望,那再看看浏览器的再说吧。
在浏览器执行上传请求,很幸运,我们看到了 boundary
,也就是说,我们使用的 RestTemplate
并没有为我们携带上 boundary
这个东东。
按道理来说,那 Postman
的也应该会带上 boundary
才对,我们去代码中打印请求的 headers
瞧一瞧,果不其然,在请求的接口中打印出来,是有 boundary
这个东东,而且每一次请求都不一样。
事情到这里已经变得比较清晰了,那我们直接手动创建一个 RestTemplate
对象去进行文件上传请求,这一次上传成功了。在全局配置中,我们只配置拦截器和 HttpMessageConverter
,首先拦截器肯定是没有问题的,并不影响 RestTemplate
为我们加上 boundary
,那就是因为我们配置的 FastJsonHttpMessageConverter
造成的。
这里提供两种解决方式,一种就是文件上传的就每次手动 new
一个 RestTemplate
,另外一种就是,再写一个配置,不用替换掉原有的 HttpMessageConverter
。