整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          被坑過后才知道HttpMessageConverte

          被坑過后才知道HttpMessageConverter多重要

          、HttpMessageConverter簡介

          或許HttpMessageConverter沒聽過,但是@RequestBody和@ResponseBody這兩個注解不會不知道吧,深入研究數據轉換時,就會發現HttpMessageConverter這個接口,簡單說就是HTTP的request和response的轉換器,在遇到@RequestBody時候SpringBoot會選擇一個合適的HttpMessageConverter實現類來進行轉換,內部有很多實現類,也可以自己實現,如果這個實現類能處理這個數據,那么它的canRead()方法會返回true,SpringBoot會調用它的read()方法從請求中讀出并轉換成實體類,同樣canWrite也是。

          但是我并不是從這里認識到HttpMessageConverter的,而是從RestTemplate,RestTemplate是一個使用同步方式執行HTTP請求的類,因此不需要加入OkHttp或者其他HTTP客戶端的依賴,使用它就可以和其他服務進行通信,但是容易出現轉換問題,如果對微信接口或者qq接口有所了解的話,那么在使用RestTemplate調用他們服務的時候,必定會報一個錯誤。

          如下面在調用qq互聯獲取用戶信息的接口時,報的錯誤。

          org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class xxx.xxx.xxxxx] and content type [text/html;charset=utf-8]
          復制代碼

          錯誤信息是未知的ContentType,這個ContentType就是第三方接口返回時候在HTTP頭中的Content-Type,如果通過其他工具查看這個接口返回的HTTP頭,會發現它的值是text/html,通常我們見的都是application/json類型。(微信接口返回的是text/plain),由于內部沒有HttpMessageConverter能處理text/html的數據,沒有一個實現類的canRead()返回true,所以最后報錯。


          通常使用OkHttp或者其他框架時不會遇到這個錯誤。

          二、深入報錯源碼

          只有了解了報錯原因以及源碼,才能更好的解決問題,所以,我們根據報錯源碼的行數,定位到HttpMessageConverterExtractor下的extractData方法,從這個結構一眼就能看出大概邏輯:循環找出能處理這個contentType的HttpMessageConverter,然后調用這個HttpMessageConverter的read()并返回。

           public T extractData(ClientHttpResponse response) throws IOException {
               MessageBodyClientHttpResponseWrapper responseWrapper=new MessageBodyClientHttpResponseWrapper(response);
               if (responseWrapper.hasMessageBody() && !responseWrapper.hasEmptyMessageBody()) {
                   MediaType contentType=this.getContentType(responseWrapper);
                   try {
                      //拿到messageConverters的迭代器
                       Iterator var4=this.messageConverters.iterator();
                       while(var4.hasNext()) {
                          //下一個HttpMessageConverter
                           HttpMessageConverter<?> messageConverter=(HttpMessageConverter)var4.next();
                           //如果是GenericHttpMessageConverter接口的實例,繼承AbstractHttpMessageConverter會走這個if。
                           if (messageConverter instanceof GenericHttpMessageConverter) {
                               GenericHttpMessageConverter<?> genericMessageConverter=(GenericHttpMessageConverter)messageConverter;
                               //判斷這個轉換器是不能能轉換這個類型
                               if (genericMessageConverter.canRead(this.responseType, (Class)null, contentType)) {
                                   if (this.logger.isDebugEnabled()) {
                                       ResolvableType resolvableType=ResolvableType.forType(this.responseType);
                                       this.logger.debug("Reading to [" + resolvableType + "]");
                                   }
                                   //走到這代表當前的HttpMessageConverter能進行轉換,則調用read并返回
                                   return genericMessageConverter.read(this.responseType, (Class)null, responseWrapper);
                               }
                           }
                           //還是判斷這個轉換器能不能進行轉換
                           if (this.responseClass !=null && messageConverter.canRead(this.responseClass, contentType)) {
                               if (this.logger.isDebugEnabled()) {
                                   String className=this.responseClass.getName();
                                   this.logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
                               }
                               ////走到這代表當前的HttpMessageConverter能進行轉換,則調用read并返回
                               return messageConverter.read(this.responseClass, responseWrapper);
                           }
                       }
                   } catch (HttpMessageNotReadableException | IOException var8) {
                       throw new RestClientException("Error while extracting response for type [" + this.responseType + "] and content type [" + contentType + "]", var8);
                   }
                   //走到這拋出異常,所有的消息轉換器都不能進行處理。
                   throw new UnknownContentTypeException(this.responseType, contentType, response.getRawStatusCode(), response.getStatusText(), response.getHeaders(), getResponseBody(response));
               } else {
                   return null;
               }
           }
          復制代碼

          messageConverters集合中就保存著在RestTemplate構造方法中添加的HttpMessageConverter實現類。

          三、自定義HttpMessageConverter

          找到了原因,我們就需要解決問題,下面使用一個簡單的辦法,即重新設置MappingJackson2HttpMessageConverter能處理的MediaType。

              @Bean
              public RestTemplate restTemplate(){
                  RestTemplate restTemplate=new RestTemplate();
                  MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter=new MappingJackson2HttpMessageConverter();
                  mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML));
                  restTemplate.getMessageConverters().add(mappingJackson2HttpMessageConverter);
                  return  restTemplate;
              }
          復制代碼

          對,沒錯,這就解決了,MappingJackson2HttpMessageConverter也是一個HttpMessageConverter轉換類,但是他不能處理text/html的數據,原因是他的父類AbstractHttpMessageConverter中的supportedMediaTypes集合中沒有text/html類型,如果有的話就能處理了,通過setSupportedMediaTypes可以給他指定一個新的MediaType集合,上面的寫法會導致MappingJackson2HttpMessageConverter只能處理text/html類型的數據。

          但是,為了更深的研究,我們要直接繼承HttpMessageConverter(當然更推薦的是繼承AbstractHttpMessageConverter)來實現,在此之前,先看這幾個方法具體代表什么意思,才能繼續往下寫。

          public interface HttpMessageConverter<T> {
              /**
               * 根據mediaType判斷clazz是否可讀
               */
              boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
          
              /**
               * 根據mediaType判斷clazz是否可寫
               */
              boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
          
              /**
               * 獲取支持的mediaType
               */
              List<MediaType> getSupportedMediaTypes();
          
              /**
               * 將HttpInputMessage流中的數據綁定到clazz中
               */
              T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
                      throws IOException, HttpMessageNotReadableException;
          
              /**
               * 將t對象寫入到HttpOutputMessage流中
               */
              void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
                      throws IOException, HttpMessageNotWritableException;
          }
          復制代碼

          對于解決這個問題,canWrite,write方式是不需要處理的,只管canRead和read就行,在canRead方法中判斷了是不是text/html類型,是的話就會返回true,Spring就會調用read,用來將字節流中的數據轉換成具體實體,aClass就是我們最終想要得到的實例對象的Class,StreamUtils這個工具類是SpringBoot自帶的一個,用來讀取InputStream中的數據并返回String字符串,SpringBoott內部很多地方都用到了這個工具類,所以這里來借用一下,現在拿到了String型的數據后,就需要將String轉換成對應的對象,這里可能想到了Gson、Fastjson,使用他們也可以完成,但是還需要額外的加入jar包,SpringBoot自身已經集成了ObjectMapper,所以在來借用一下。

          package com.hxl.vote.config;
          
          import com.fasterxml.jackson.databind.DeserializationFeature;
          import com.fasterxml.jackson.databind.ObjectMapper;
          import org.springframework.http.HttpInputMessage;
          import org.springframework.http.HttpOutputMessage;
          import org.springframework.http.MediaType;
          import org.springframework.http.converter.HttpMessageConverter;
          import org.springframework.http.converter.HttpMessageNotReadableException;
          import org.springframework.http.converter.HttpMessageNotWritableException;
          import org.springframework.util.StreamUtils;
          
          import java.io.IOException;
          import java.nio.charset.Charset;
          import java.util.Arrays;
          import java.util.List;
          
          public class QQHttpMessageConverter implements HttpMessageConverter<Object> {
              @Override
              public boolean canRead(Class<?> aClass, MediaType mediaType) {
                  if (mediaType !=null) {
                      return mediaType.isCompatibleWith(MediaType.TEXT_HTML);
                  }
                  return false;
              }
          
              @Override
              public boolean canWrite(Class<?> aClass, MediaType mediaType) {
                  return false;
              }
          
              @Override
              public List<MediaType> getSupportedMediaTypes() {
                  return Arrays.asList(MediaType.TEXT_HTML);
              }
          
              @Override
              public Object read(Class<?> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
                  String json=StreamUtils.copyToString(httpInputMessage.getBody(), Charset.forName("UTF-8"));
                  ObjectMapper objectMapper=new ObjectMapper();
                  objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                  return objectMapper.readValue(json, aClass);
              }
          
              @Override
              public void write(Object o, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
          
              }
          }
          
          復制代碼

          最后需要要進行配置,getMessageConverters()會返回現有的HttpMessageConverter集合,我們在這個基礎上加入我們自定義的HttpMessageConverter即可,這回就不報錯了。

          @Bean
          public RestTemplate restTemplate(){
              RestTemplate restTemplate=new RestTemplate();
              restTemplate.getMessageConverters().add(new QQHttpMessageConverter());
              return  restTemplate;
          }
          復制代碼

          繼承AbstractHttpMessageConverter

          AbstractHttpMessageConverter幫我們封裝了一部分事情,但是有些事情是他不能確定的,所以要交給子類實現,使用以下方法,同樣可以解決text/html的問題。

          public class QQHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
              public QQHttpMessageConverter() {
                  super(MediaType.TEXT_HTML);
              }
              @Override
              protected boolean supports(Class<?> aClass) {
                  return true;
              }
              @Override
              protected Object readInternal(Class<?> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
                  String json=StreamUtils.copyToString(httpInputMessage.getBody(), Charset.forName("UTF-8"));
                  ObjectMapper objectMapper=new ObjectMapper();
                  objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                  return objectMapper.readValue(json, aClass);
              }
              @Override
              protected void writeInternal(Object o, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
              }
          }
          復制代碼

          繼承MappingJackson2HttpMessageConverter

          好吧,使用MappingJackson2HttpMessageConverter,只需要給他能處理的MediaType即可,更簡單。

          public class QQHttpMessageConverter extends MappingJackson2HttpMessageConverter {
              public QQHttpMessageConverter() {
                  setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML));
              }
          復


          作者:i聽風逝夜
          鏈接:https://juejin.im/post/6886733763020062733
          來源:掘金
          著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

          頁可見區域寬:document.body.clientWidth

          網頁可見區域高:document.body.clientHeight

          網頁可見區域寬:document.body.offsetWidth (包括邊線的寬)

          網頁可見區域高:document.body.offsetHeight (包括邊線的寬)

          網頁正文全文寬:document.body.scrollWidth

          網頁正文全文高:document.body.scrollHeight

          網頁被卷去的高:document.body.scrollTop

          網頁被卷去的左:document.body.scrollLeft

          網頁正文部分上:window.screenTop

          網頁正文部分左:window.screenLeft

          屏幕分辨率的高:window.screen.height

          屏幕分辨率的寬:window.screen.width

          屏幕可用工作區高度:window.screen.availHeight

          屏幕可用工作區寬度:window.screen.availWidth

          HTML精確定位:scrollLeft,scrollWidth,clientWidth,offsetWidth

          scrollHeight: 獲取對象的滾動高度。

          scrollLeft:設置或獲取位于對象左邊界和窗口中目前可見內容的最左端之間的距離

          scrollTop:設置或獲取位于對象最頂端和窗口中可見內容的最頂端之間的距離

          scrollWidth:獲取對象的滾動寬度

          offsetHeight:獲取對象相對于版面或由父坐標 offsetParent 屬性指定的父坐標的高度

          offsetLeft:獲取對象相對于版面或由 offsetParent 屬性指定的父坐標的計算左側位置

          offsetTop:獲取對象相對于版面或由 offsetTop 屬性指定的父坐標的計算頂端位置

          event.clientX 相對文檔的水平座標

          event.clientY 相對文檔的垂直座標

          event.offsetX 相對容器的水平坐標

          event.offsetY 相對容器的垂直坐標

          document.documentElement.scrollTop 垂直方向滾動的值

          event.clientX+document.documentElement.scrollTop 相對文檔的水平座標+垂直方向滾動的量

          IE,FireFox 差異如下:

          IE6.0、FF1.06+:

          clientWidth=width + padding

          clientHeight=height + padding

          offsetWidth=width + padding + border

          offsetHeight=height + padding + border

          IE5.0/5.5:

          clientWidth=width - border

          clientHeight=height - border

          offsetWidth=width

          offsetHeight=height

          (需要提一下:CSS中的margin屬性,與clientWidth、offsetWidth、clientHeight、offsetHeight均無關)

          網頁可見區域寬: document.body.clientWidth

          網頁可見區域高: document.body.clientHeight

          網頁可見區域寬: document.body.offsetWidth (包括邊線的寬)

          網頁可見區域高: document.body.offsetHeight (包括邊線的高)

          網頁正文全文寬: document.body.scrollWidth

          網頁正文全文高: document.body.scrollHeight

          網頁被卷去的高: document.body.scrollTop

          網頁被卷去的左: document.body.scrollLeft

          網頁正文部分上: window.screenTop

          網頁正文部分左: window.screenLeft

          屏幕分辨率的高: window.screen.height

          屏幕分辨率的寬: window.screen.width

          屏幕可用工作區高度: window.screen.availHeight

          屏幕可用工作區寬度: window.screen.availWidth

          -------------------

          技術要點

          本節代碼主要使用了Document對象關于窗口的一些屬性,這些屬性的主要功能和用法如下。

          要得到窗口的尺寸,對于不同的瀏覽器,需要使用不同的屬性和方法:若要檢測窗口的真實尺寸,在Netscape下需要使用Window的屬性;在IE下需要 深入Document內部對body進行檢測;在DOM環境下,若要得到窗口的尺寸,需要注意根元素的尺寸,而不是元素。

          Window對象的innerWidth屬性包含當前窗口的內部寬度。Window對象的innerHeight屬性包含當前窗口的內部高度。

          Document對象的body屬性對應HTML文檔的標簽。Document對象的documentElement屬性則表示HTML文檔的根節點。

          document.body.clientHeight表示HTML文檔所在窗口的當前高度。document.body. clientWidth表示HTML文檔所在窗口的當前寬度。

          實現代碼

          <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

          <html xmlns="http://www.w3.org/1999/xhtml">

          <head>

          <title>請調整瀏覽器窗口</title>

          <meta http-equiv="content-type" content="text/html; charset=gb2312">

          </head>

          <body>

          <h2 align="center">請調整瀏覽器窗口大小</h2><hr>

          <form action="#" method="get" name="form1" id="form1">

          <!--顯示瀏覽器窗口的實際尺寸-->

          瀏覽器窗口 的 實際高度: <input type="text" name="availHeight" size="4"><br>

          瀏覽器窗口 的 實際寬度: <input type="text" name="availWidth" size="4"><br>

          </form>

          <script type="text/javascript">

          <!--

          var winWidth=0;

          var winHeight=0;

          function findDimensions() //函數:獲取尺寸

          {

          //獲取窗口寬度

          if (window.innerWidth)

          winWidth=window.innerWidth;

          else if ((document.body) && (document.body.clientWidth))

          winWidth=document.body.clientWidth;

          //獲取窗口高度

          if (window.innerHeight)

          winHeight=window.innerHeight;

          else if ((document.body) && (document.body.clientHeight))

          winHeight=document.body.clientHeight;

          //通過深入Document內部對body進行檢測,獲取窗口大小

          if (document.documentElement && document.documentElement.clientHeight && document.documentElement.clientWidth)

          {

          winHeight=document.documentElement.clientHeight;

          winWidth=document.documentElement.clientWidth;

          }

          //結果輸出至兩個文本框

          document.form1.availHeight.value=winHeight;

          document.form1.availWidth.value=winWidth;

          }

          findDimensions();

          //調用函數,獲取數值

          window.onresize=findDimensions;

          //-->

          </script>

          </body>

          </html>

          源程序解讀

          (1)程序首先建立一個表單,包含兩個文本框,用于顯示窗口當前的寬度和高度,并且,其數值會隨窗口大小的改變而變化。

          (2)在隨后的JavaScript代碼中,首先定義了兩個變量winWidth和winHeight,用于保存窗口的高度值和寬度值。

          (3)然后,在函數findDimensions ( )中,使用window.innerHeight和window.innerWidth得到窗口的高度和寬度,并將二者保存在前述兩個變量中。

          (4)再通過深入Document內部對body進行檢測,獲取窗口大小,并存儲在前述兩個變量中。

          (5)在函數的最后,通過按名稱訪問表單元素,結果輸出至兩個文本框。

          (6)在JavaScript代碼的最后,通過調用findDimensions ( )函數,完成整個操作。

          帶有兩個輸入字段和一個提交按鈕的 HTML 表單:

          <form action="demo_form.php" method="get">

          First name: <input type="text" name="fname"><br>

          Last name: <input type="text" name="lname"><br>

          <input type="submit" value="提交">

          </form>

          (更多實例見頁面底部)


          瀏覽器支持

          所有主流瀏覽器都支持 <form> 標簽。


          標簽定義及使用說明

          <form> 標簽用于創建供用戶輸入的 HTML 表單。

          <form> 元素包含一個或多個如下的表單元素:

          • <input>

          • <textarea>

          • <button>

          • <select>

          • <option>

          • <optgroup>

          • <fieldset>

          • <label>


          HTML 4.01 與 HTML5之間的差異

          HTML5 新增了兩個新的屬性:autocomplete 和 novalidate,同時不再支持 HTML 4.01 中的某些屬性。


          HTML 與 XHTML 之間的差異

          在 XHTML 中,name 屬性已被廢棄。使用全局 id 屬性代替。


          屬性

          New :HTML5 中的新屬性。

          屬性描述
          acceptMIME_typeHTML5 不支持。規定服務器接收到的文件的類型。(文件是通過文件上傳提交的)
          accept-charsetcharacter_set規定服務器可處理的表單數據字符集。
          actionURL規定當提交表單時向何處發送表單數據。
          autocompleteNewonoff規定是否啟用表單的自動完成功能。
          enctypeapplication/x-www-form-urlencodedmultipart/form-datatext/plain規定在向服務器發送表單數據之前如何對其進行編碼。(適用于 method="post" 的情況)
          methodgetpost規定用于發送表單數據的 HTTP 方法。
          nametext規定表單的名稱。
          novalidateNewnovalidate如果使用該屬性,則提交表單時不進行驗證。
          target_blank_self_parent_top規定在何處打開 action URL。

          全局屬性

          <form> 標簽支持 HTML 的全局屬性。


          事件屬性

          <form> 標簽支持 HTML 的事件屬性。

          實例

          帶有復選框的表單

          此表單包含兩個復選框和一個提交按鈕。

          帶有單選按鈕的表單

          此表單包含兩個單選框和一個提交按鈕。

          如您還有不明白的可以在下面與我留言或是與我探討QQ群308855039,我們一起飛!


          主站蜘蛛池模板: 日韩一区二区在线视频| 成人区人妻精品一区二区不卡| 亚洲一区二区三区高清| 日产亚洲一区二区三区| 人妻少妇精品视频三区二区一区 | 伦精品一区二区三区视频| 亚洲日韩中文字幕一区| 国产aⅴ一区二区三区| 丰满少妇内射一区| 亚洲AV日韩综合一区| 一区二区三区精品视频| 人妻AV中文字幕一区二区三区| 亚洲一本一道一区二区三区 | 午夜福利一区二区三区高清视频 | 亚洲av成人一区二区三区在线观看 | 国产99久久精品一区二区| 国产免费私拍一区二区三区| 国产av天堂一区二区三区| 午夜视频在线观看一区| 亚洲线精品一区二区三区影音先锋| 亚洲一区二区三区在线网站 | 亚欧在线精品免费观看一区| 精品一区二区三区电影| 午夜福利一区二区三区在线观看 | 亚洲AV美女一区二区三区| 国语对白一区二区三区| 久久一区二区三区免费播放| 国产中的精品一区的| 亚洲狠狠狠一区二区三区| 久久国产精品免费一区| 亚洲AV成人一区二区三区观看| 国产日韩一区二区三免费高清| 国产乱码精品一区二区三区麻豆| av无码人妻一区二区三区牛牛| 鲁丝丝国产一区二区| 日韩人妻精品无码一区二区三区 | 激情亚洲一区国产精品| 国产av熟女一区二区三区| 精品一区二区三区在线播放| 亚洲一区二区三区在线| 国产一区麻豆剧传媒果冻精品 |