今天有一个通过Restemplate请求一个天气API,发现其Body数据是乱码。乱码的第一反应是编码格式出了问题。检查头部信息,
content-encoding="gzip"
content-type="application/json;charset=UTF-8"
返回值是UTF-8,Restemplate设置的也是UTF-8。在翻看其他博客,发现问题原因是http存在一个压缩格式:Gzip。
Gzip是一个压缩算法,当请求数据或返回数据体积过大,为减少网络负载压力而使用的压缩算法。通常在服务器端使用,客户端为获得原始数据需通过Gzip解压。
响应头中的gzip
Content-Encoding
是一个实体消息首部,用于对特定媒体类型的数据进行压缩。当这个首部出现的时候,它的值表示消息主体进行了何种方式的内容编码转换。这个消息首部用来告知客户端应该怎样解码才能获取在 Content-Type
中标示的媒体类型内容。
一般建议对数据尽可能地进行压缩,因此才有了这个消息首部的出现。
注:客户端和服务器都可以使用,表示body中的数据采用了什么编码(压缩算法)
Accept-Encoding
HTTP 请求头 Accept-Encoding 会将客户端能够理解的内容编码方式——通常是某种压缩算法——进行通知(给服务端)。通过内容协商的方式,服务端会选择一个客户端提议的方式,使用并在响应头 Content-Encoding 中通知客户端该选择。
注:一般是客户端使用,表示给服务器说明,客户端支持的压缩算法列表。服务从中选择一个对响应体进行压缩。
示例
/**
* @Title: getClimateByRequst
* @Description: ( 通过request 获取天气信息)
* @Author:lijie
* @since 2021/11/9 14:05
* @Version:1.1.0
* @return: climate:封装的天气结果类
*/
public Result<Climate> getClimateByRequst(HttpServletRequest request) {
Result<Climate> result=new Result<>();
try{
//通过request请求获得ip地址
String ip = WebUtils.getIpByRequset(request);
//通过ip获得大致定位
Result<Map<String, Object>> locationByIP = this.getLocationByIP(ip);
if(locationByIP!=null&&locationByIP.isSuccess()){
Map<String, Object> data = locationByIP.getData();
String url=""//请求的url地址
if(MapUtils.isNotEmpty(data)){
String jd = MapUtils.getString(data, "jd");//精度
String wd = MapUtils.getString(data, "wd");//纬度
HttpHeaders httpHeaders = new HttpHeaders();
// Accept 表示客户端支持什么格式的响应体
httpHeaders.set("contentType", "application/json;charset=UTF-8");
// Accept-Encoding 头,表示客户端可以接收gzip格式的压缩
httpHeaders.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
//发送请求
ResponseEntity<byte[]> forEntity = restTemplate.exchange
(url, HttpMethod.GET, new HttpEntity<>(httpHeaders), byte[].class);
if(forEntity.getStatusCode()== HttpStatus.OK){
// 获取服务器响应体编码
String contentEncoding = forEntity.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
if ("gzip".equals(contentEncoding)) { // 是gzip编码
// gzip解压服务器的Body响应体
byte[] weatherData = WebUtils.unGZip(
new ByteArrayInputStream(Objects.requireNonNull(forEntity.getBody())));
String weatherJson = new String(weatherData, StandardCharsets.UTF_8);
Climate climate = JSONObject.parseObject(weatherJson, Climate.class);
if(climate!=null){
result.setSuccess();
result.setData(climate);
}
} else {
// todo 其他的编码
result.setErrored("和风API响应值编码不是Gzip,请联系我,谢谢");
}
}else{
result.setErrored("和风API响应出错了");
}
}
}
}catch (Exception e){
result.setErrored("天气获取出错了");
}
return result;
}
/**
* Gzip解压缩
* @param inputStream
* @return
* @throws IOException
*/
public static byte[] unGZip(InputStream inputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream)) {
byte[] buf = new byte[4096];
int len = -1;
while ((len = gzipInputStream.read(buf, 0, buf.length)) != -1) {
byteArrayOutputStream.write(buf, 0, len);
}
return byteArrayOutputStream.toByteArray();
} finally {
byteArrayOutputStream.close();
}
}
/**
* Gzip压缩数据
* @param data
* @return
* @throws IOException
*/
public static byte[] gZip(byte[] data) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
gzipOutputStream.write(data);
gzipOutputStream.finish();
return byteArrayOutputStream.toByteArray();
}
}
SpringBoot的响应体压缩配置
实际上,并不需要自己手动去写这种响应体的压缩代码。springboot提供了相关的配置。只针对响应压缩
SpringBoot2开启响应压缩
最后
使用RestTemplate
请求文本数据接口,发现解码后的字符串是乱码。此时除了编码格式问题外就可以怀疑是不是服务器响应了压缩后的数据。解决这个问题,先尝试移除Accept-Encoding
请求头,告诉服务器,客户端不需要压缩响应体。如果服务器还是响应压缩后的数据,尝试读取服务器的Content-Encoding
头,根据服务器的压缩编码,自己再进行解压缩。