或許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實現類。
找到了原因,我們就需要解決問題,下面使用一個簡單的辦法,即重新設置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幫我們封裝了一部分事情,但是有些事情是他不能確定的,所以要交給子類實現,使用以下方法,同樣可以解決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,只需要給他能處理的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 中的新屬性。
屬性 | 值 | 描述 |
---|---|---|
accept | MIME_type | HTML5 不支持。規定服務器接收到的文件的類型。(文件是通過文件上傳提交的) |
accept-charset | character_set | 規定服務器可處理的表單數據字符集。 |
action | URL | 規定當提交表單時向何處發送表單數據。 |
autocompleteNew | onoff | 規定是否啟用表單的自動完成功能。 |
enctype | application/x-www-form-urlencodedmultipart/form-datatext/plain | 規定在向服務器發送表單數據之前如何對其進行編碼。(適用于 method="post" 的情況) |
method | getpost | 規定用于發送表單數據的 HTTP 方法。 |
name | text | 規定表單的名稱。 |
novalidateNew | novalidate | 如果使用該屬性,則提交表單時不進行驗證。 |
target | _blank_self_parent_top | 規定在何處打開 action URL。 |
全局屬性
<form> 標簽支持 HTML 的全局屬性。
事件屬性
<form> 標簽支持 HTML 的事件屬性。
實例
帶有復選框的表單
此表單包含兩個復選框和一個提交按鈕。
帶有單選按鈕的表單
此表單包含兩個單選框和一個提交按鈕。
如您還有不明白的可以在下面與我留言或是與我探討QQ群308855039,我們一起飛!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。