1. 程式人生 > >restTemplate的介紹和使用

restTemplate的介紹和使用

1、背景介紹

Spring RestTemplate 是 Spring 提供的用於訪問 Rest 服務的客戶端,RestTemplate 提供了多種便捷訪問遠端Http服務的方法,能夠大大提高客戶端的編寫效率,所以很多客戶端比如 Android或者第三方服務商都是使用 RestTemplate 請求 restful 服務。

 

2、知識剖析

呼叫 RestTemplate 的預設建構函式,RestTemplate 物件在底層通過使用 java.net 包下的實現建立 HTTP 請求,可以通過使用 ClientHttpRequestFactory 指定不同的HTTP請求方式。預設使用 SimpleClientHttpRequestFactory,是 ClientHttpRequestFactory 實現類。如下流程:

1)使用預設構造方法new一個例項

RestTemplate template = new RestTemplate();

2)RestTemplate 內部通過呼叫 doExecute 方法,首先就是獲取 ClientHttpRequest

 

3)RestTemplate 實現了抽象類 HttpAccessor ,所以可以呼叫父類的 createRequest

private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

public ClientHttpRequestFactory getRequestFactory() {

return this.requestFactory;

}

protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {

ClientHttpRequest request = getRequestFactory().createRequest(url, method);

if (logger.isDebugEnabled()) {

logger.debug("Created " + method.name() + " request for \"" + url + "\"");

}

return request;

}

 

4)SimpleClientHttpRequestFactory 實現了 ClientHttpRequest,同時實現方法

注意 bufferRequestBody 是可以在 RestTemplate 設定,是標誌是否使用快取流的形式,預設是 true,缺點是當傳送大量資料時,比如put/post的儲存和修改,那麼可能記憶體消耗嚴重。所以這時候可以設定 RestTemplate.setBufferRequestBody(false);

即使用 SimpleStreamingClientHttpRequest 來實現。

 

5)openConnection 沒什麼文章,而是 prepareConnection 則是大有文章,這裡我們分兩個版本來說,因為我們一開始使用 4.1.1 的時候不能使用帶請求體的delete,可是在 4.3.2 版本則可以使用,所以特別區分了這兩個版本的程式碼,如下:

SimpleClientHttpRequestFactory -- 4.1.1 版本的程式碼預設

delete connection.setDoOutput = fase

如果設定false,然後後面又去獲取輸出流時,會發生如下錯誤 sun 包的 HttpURLConnection

if(!this.doOutput) {

throw new ProtocolException(

"cannot write to a URLConnection if doOutput=false - call setDoOutput(true)"

);

}

 

SimpleClientHttpRequestFactory -- 4.3.2 版本的程式碼預設

delete connection.setDoOutput = fase

DoOutput 的屬性作用是可以使用 conn.getOutputStream().write() ,這樣就能傳送請求體了

 

6)接著執行 requestCallback.doWithRequest(request);

RequestCallback 封裝了請求體和請求頭物件,也就是說在該物件裡面可以拿到我們需要的請求引數,在執行 doWithRequest 時,有一個非常重要的步驟,他和前面Connection傳送請求體有著密切關係,我們知道請求頭就是 SimpleBufferingClientHttpRequest.addHeaders 方法,那麼請求體 bufferedOutput 是如何賦值的呢?就是在 doWithRequest 裡面,如下 StringHttpMessageConverter (其他 MessageConvert 也一樣,這裡也是經常亂碼的原因)

其中 s 就是請求體,HttpOutputMessage 物件就是我們準備的 ClientHttpRequest 物件,也就是上面的 SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttpRequest

這樣,先呼叫父類的流方法,把內容寫入流中,然後呼叫父類的 executeInternal方法在呼叫自身的該方法 executeInternal ,如下一步

 

7)接著執行 response = request.execute();

然後使用例項 SimpleBufferingClientHttpRequest 封裝請求體和請求頭

SimpleBufferingClientHttpRequest -- 4.1.1 版本的程式碼預設

delete 時通過前面設定的 DoOutput 引數和是否可以設定輸出流來判斷是否需要傳送請求體

如果是 delete 請求,那麼很明顯 DoOutput = false,所以不會有封裝請求體的過程,即不執行

FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());

所以服務端無法獲取到請求體,會出現 HttpMessageNotReadableException: Required request body is missing

 

SimpleBufferingClientHttpRequest -- 4.3.2 版本的程式碼預設

delete 時通過請求方式和是否有請求體物件來判斷是否需要傳送請求體

如果是delete請求,首先設定 DoOutput = true,然後根據是否有請求體資料,然後封裝請求體

FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());

 

8)最後解析response

接著就是 response 的解析了,主要還是 Error 的解析。

handleResponseError(method, url, response);

 

3、RestTemplate 的配置項

1)setBufferRequestBody 是否是否緩衝流來儲存請求體,預設true

2)setProxy 設定代理物件

3)setChunkSize 設定每次傳輸位元組長度,與 setBufferRequestBody(false) 結合使用

4)setConnectTimeout 設定連線超時時間,預設 -1

5)setReadTimeout 設定讀取內容超時時間,預設 -1

6)setOutputStreaming 設定Connection是否設定輸出流程

7)setTaskExecutor 設定非同步回撥執行器

 

4、RestTemplate 設定 RequestFactory

其實任何有連線的地方都會有連線池的概念,比如資料庫連線等,這裡也不例外,肯定也會有,RestTemplate 預設有兩種工廠物件實現方式,都是 ClientHttpRequestFactory 的子類。如下

1)SimpleClientHttpRequestFactory 底層使用 java.net.HttpUrlConnection,可配置證書

2)HttpComponentsClientHttpRequestFactory 底層使用Apache HttpClient訪問遠端的Http服務,使用HttpClient同樣可以配置連線池和證書等資訊,而且功能更強大,配置項更多。

 

5、RequestFactory 的配置方式

1)使用XML配置,就是配置JavaBean

2)使用程式碼配置,就是初始化這個物件

無論上面那種方式配置,都是配置外殼 RestTemplate,真正傳送請求的 request 物件其實都是由工廠管理的,所以我們不關心連線池的管理,只是配置連線池初始化的一些引數而已。

這個可以參考:

http://www.open-open.com/lib/view/open1436018677419.html

 

6、請求引數的傳遞

 

7、關於網上說的無法傳送delete請求體

HttpMessageNotReadableException: Required request body is missing

Spring MVC 的 @RequestBody 只支援RestTemplate 的 POST 和 PUT

但是 RestTemplate 的 delete 方法並不支援傳入請求體(Request Body)。經測試,通過呼叫 RestTemplate 類的exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<ResponseResult> responseType, Object... uriVariables)  方法,將 method 指定為 org.springframework.http.HttpMethod.DELETE,並傳入 requestEntity(請求體) 物件時,在服務端得到的 Request Body 仍然為 null。可見 RestTemplate 預設並不支援對 DELETE 方法使用請求體。

    通過查閱資料發現 RestTemplate 預設是使用 spring 自身的 SimpleClientHttpRequestFactory 建立請求物件和對其進行相關設定(如請求頭、請求體等),它只支援 PUT 和 POST 方法帶請求體,RestTemplate 的 DELETE 方法不支援傳入請求體是因為 JDK 中 HttpURLConnection 物件的 delete 方法不支援傳入請求體(如果對 HttpURLConnection 物件的 delete 方法傳入請求體,在執行時會丟擲 IOException)。

從程式碼中也看到了 Spring 對 delete 做了判斷,如果是 4.1.1 及以前的版本,確實是會出現上面的問題,但是當我使用 4.3.2 之後的版本,發現完全可以傳送請求體,這裡面的變化就是前者在程式碼中把請求體過濾掉了,後者把請求體加上了。至於更細的細節,希望有人能夠繼續深究下去。

3、常見問題

4、解決方案

5、編碼實戰

配置快取管理器

@Configuration//相當於beans標籤
@EnableCaching//註解驅動的快取管理器
public class RedisConfiguration extends CachingConfigurerSupport {
/*    @Autowired
    private RedisConnectionFactory connectionFactory;*/
    /**
     * @Description: 指定redis主鍵生成規則:包名+方法名+引數名列表(原有:引數組合)
     * @return: org.springframework.cache.interceptor.KeyGenerator
     * @Date: 2018/6/28 17:11
     */
/*    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object o, Method method, Object... objects) {
                StringBuffer stringBuffer = new StringBuffer();
                stringBuffer.append(o.getClass().getName());
                stringBuffer.append("::" + method.getName() + ":");
                for (Object object : objects)
                    stringBuffer.append(object.toString());
                return stringBuffer.toString();
            }

        };
    }*/

    /**
     * @Description: 快取管理器
     * @return: org.springframework.cache.CacheManager
     * @Date: 2018/6/28 17:12
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        //通過連線工廠初始化RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
        ClassLoader loader = this.getClass().getClassLoader();
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer =
                new GenericJackson2JsonRedisSerializer(om);
        RedisSerializationContext.SerializationPair<Object> rs =
                RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer);
        //設定value序列化方式,如果自帶value序列化方式是jdkSerializer,存在redis之中會新增一些東西,人還看不懂
        //GenericJackson2JsonRedisSerializer序列化方法儲存的大小是jdkSerializer的五分之一,並且是人能夠讀懂的值
        RedisCacheConfiguration redisCacheConfiguration =
                RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(rs);
        //設定預設過期時間
        redisCacheConfiguration.entryTtl(Duration.ofDays(1));
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }

    /**
     * @Description: 設定RedisTemplate的序列化方式
     * @return: org.springframework.data.redis.core.RedisTemplate<java.lang.String , java.lang.Object>
     * @Date: 2018/6/28 17:13
     */
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer =
                new GenericJackson2JsonRedisSerializer(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(genericJackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        return template;
    }


}

6、擴充套件思考

7、參考文獻

https://blog.csdn.net/guokezhongdeyuzhou/article/details/79789629

https://blog.csdn.net/u011851478/article/details/70239722

https://www.cnblogs.com/fashflying/p/6908028.html

8、更多討論

--------------------- 本文來自 weixin_41261521 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/weixin_41261521/article/details/81293265?utm_source=copy