Spring RestTemplate 調用天氣預報接口亂碼的解決 原 薦

Spring RestTemplate 調用天氣預報接口可能遇到中文亂碼的問題,解決思路如下。

問題出現

我們在網上找了一個免費的天氣預報接口 http://wthrcdn.etouch.cn/weather_mini?citykey=101280601。我們希望調用該接口,並將返回的數據解析爲 JSON 格式。

核心業務邏輯如下:

private WeatherResponse doGetWeatherData(String uri) {

    ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class);
    
    String strBody = null;

    if (response.getStatusCodeValue() == 200) {
        strBody = response.getBody();
    }

    ObjectMapper mapper = new ObjectMapper();
    WeatherResponse weather = null;

    try {
        weather = mapper.readValue(strBody, WeatherResponse.class);
    } catch (IOException e) {
        e.printStackTrace();
    }

    return weather;
}

在瀏覽器裏面訪問該接口都挺正常。如下圖所示:

在瀏覽器裏面訪問該接口都挺正常

但在純 Spring 應用裏面,嘗試使用 RestTemplate 來調用,結果解析數據爲 JSON 失敗,因爲數據有亂碼。如下圖所示:

RestTemplate 數據有亂碼

嘗試進行編碼轉換

一開始,我們認爲這可能是對方轉過來的數據不是 UTF-8 導致的,所以,嘗試加入了消息轉換器。

@Configuration
public class RestConfiguration {

    @Bean  
    public RestTemplate restTemplate() { 
    	RestTemplate restTemplate = new RestTemplate(); 
    	restTemplate.getMessageConverters().set(1, 
    			new StringHttpMessageConverter(StandardCharsets.UTF_8)); // 支持中文編碼
        return restTemplate;
    }

}

StringHttpMessageConverter 默認是 ISO_8859_1,所以我們設置爲了 UTF_8。

再次執行,發現仍然是亂碼。

找到問題的根源

這一次我沒有再瞎猜了,而是仔細觀察了 HTTP 的請求協議。發現消息頭裏面的蛛絲馬跡:

HTTP 的請求協議

原來,數據是經過 GZIP 壓縮過的。默認情況下, RestTemplate 使用的是 JDK 的 HTTP 調用器,並不支持 GZIP 解壓,難怪解析不了。

解決方案

既然找到了問題所在,解決起來就簡單了。主要考慮了以下幾種方案。

1. 編寫 GIZP 工具類

處理 Gizp 壓縮的數據的工具類如下:

/**
 * Welcome to https://waylau.com
 */
package com.waylau.spring.mvc.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;

/**
 * String Util.
 * 
 * @since 1.0.0 2018年3月27日
 * @author <a href="https://waylau.com">Way Lau</a>
 */
public class StringUtil {

	/**
	 * 處理 Gizp 壓縮的數據.
	 * 
	 * @param str
	 * @return
	 * @throws IOException
	 */
	public static String conventFromGzip(String str) throws IOException {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		ByteArrayInputStream in;
		GZIPInputStream gunzip = null;

		in = new ByteArrayInputStream(str.getBytes("ISO-8859-1"));
		gunzip = new GZIPInputStream(in);
		byte[] buffer = new byte[256];
		int n;
		while ((n = gunzip.read(buffer)) >= 0) {
			out.write(buffer, 0, n);
		}

		return out.toString();
	}
}

核心業務邏輯如下:

private WeatherResponse doGetWeatherData(String uri) {

    ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class);
    String strBody = null;

    if (response.getStatusCodeValue() == 200) {
        try {
            strBody = StringUtil.conventFromGzip(response.getBody());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    ObjectMapper mapper = new ObjectMapper();
    WeatherResponse weather = null;

    try {
        weather = mapper.readValue(strBody, WeatherResponse.class);
    } catch (IOException e) {
        e.printStackTrace();
    }

    return weather;
}

2. 使用 Apache HttpClient

使用 Apache HttpClient 作爲 REST 客戶端。Apache HttpClient 內置了對於 GZIP 的支持

@Configuration
public class RestConfiguration {

    @Bean  
    public RestTemplate restTemplate() { 
    	RestTemplate restTemplate = new RestTemplate(
    			new HttpComponentsClientHttpRequestFactory()); // 使用HttpClient,支持GZIP
    	restTemplate.getMessageConverters().set(1, 
    			new StringHttpMessageConverter(StandardCharsets.UTF_8)); // 支持中文編碼
        return restTemplate;
    }

}

核心業務邏輯如下:

private WeatherResponse doGetWeatherData(String uri) {

    ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class);
    
    String strBody = null;

    if (response.getStatusCodeValue() == 200) {
        strBody = response.getBody();
    }

    ObjectMapper mapper = new ObjectMapper();
    WeatherResponse weather = null;

    try {
        weather = mapper.readValue(strBody, WeatherResponse.class);
    } catch (IOException e) {
        e.printStackTrace();
    }

    return weather;
}

當然,使用該方案,需要引入 Apache HttpClient 的依賴。

最終效果,完美!

最終效果

在 Spring Boot 中所使用的差異

也有學員問到,爲啥我在“基於Spring Cloud的微服務實戰”課程中,沒有同樣也是使用 RestTemplate, 調用同樣的接口,爲啥沒有出現亂碼的問題?

其實,細心的學員應該發現,在課程中,我們同樣也是使用了 Apache HttpClient,由於 Spring Cloud 本身也是基於 Spring Boot 來構建的,所以屏蔽了很多消息轉換的細節而言。

以下是 Spring Boot 中通過 RestTemplateBuilder 來構建 RestTemplate 的方式:

@Configuration
public class RestConfiguration {
	
	@Autowired
	private RestTemplateBuilder builder;

	@Bean
	public RestTemplate restTemplate() {
		return builder.build();
	}
	
}

所以學習編碼,知其然要知其所以然!

源碼

參考引用:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章