6.6.5、生產者、消費者限定
6.6.5.1、基本概念
首先讓我們看一下通過HTTP協議傳輸的媒體類型及如何表示媒體類型:
一、Media Type:
互聯網媒體類型,一般就是我們所說的MIME類型,用來確定請求的內容類型或響應的內容類型。
type主類型,任意的字符串,如text,如果是*號代表所有;
subtype 子類型,任意的字符串,如html,如果是*號代表所有;
parameter 可選,一些參數,如Accept請求頭的q參數, Content-Type的 charset參數。
詳見http://tools.ietf.org/html/rfc2616#section-3.7
常見媒體類型:
text/html : HTML格式 text/plain :純文本格式 text/xml :XML格式
image/gif :gif圖片格式 image/jpeg :jpg圖片格式 image/png:png圖片格式
application/x-www-form-urlencoded : <form encType=””>中默認的encType,form表單數據被編碼爲key/value格式發送到服務器(表單默認的提交數據的格式)。
multipart/form-data : 當你需要在表單中進行文件上傳時,就需要使用該格式;
application/xhtml+xml :XHTML格式 application/xml : XML數據格式
application/atom+xml :Atom XML聚合格式 application/json : JSON數據格式
application/pdf :pdf格式 application/msword : Word文檔格式
application/octet-stream : 二進制流數據(如常見的文件下載)。
在如tomcat服務器的 “conf/web.xml”中指定了擴展名到媒體類型的映射,在此我們可以看到服務器支持的媒體類型。
二、Content-Type:內容類型,即請求/響應的內容區數據的媒體類型;
2.1、請求頭的內容類型,表示發送到服務器的內容數據的媒體類型;
request中設置請求頭“Content-Type: application/x-www-form-urlencoded”表示請求的數據爲key/value數據;
(1、控制器cn.javass.chapter6.web.controller.consumesproduces.contenttype.RequestContentTypeController
@RequestMapping(value = "/ContentType", method = RequestMethod.GET)
public String showForm() throws IOException {
//form表單,使用application/x-www-form-urlencoded編碼方式提交表單
return "consumesproduces/Content-Type";
}
@RequestMapping(value = "/ContentType", method = RequestMethod.POST,
headers = "Content-Type=application/x-www-form-urlencoded")
public String request1(HttpServletRequest request) throws IOException {
//①得到請求的內容區數據的類型
String contentType = request.getContentType();
System.out.println("========ContentType:" + contentType);
//②得到請求的內容區數據的編碼方式,如果請求中沒有指定則爲null
//注意,我們的CharacterEncodingFilter這個過濾器設置了編碼(UTF-8)
//編碼只能被指定一次,即如果客戶端設置了編碼,則過濾器不會再設置
String characterEncoding = request.getCharacterEncoding();
System.out.println("========CharacterEncoding:" + characterEncoding);
//③表示請求的內容區數據爲form表單提交的參數,此時我們可以通過request.getParameter得到數據(key=value)
System.out.println(request.getParameter("realname"));
System.out.println(request.getParameter("username"));
return "success";
}
showForm功能處理方式:展示表單,且form的enctype="application/x-www-form-urlencoded",在提交時請求的內容類型頭爲“Content-Type:application/x-www-form-urlencoded”;
request1功能處理方法:只對請求頭爲“Content-Type:application/x-www-form-urlencoded”的請求進行處理(即消費請求內容區數據);
request.getContentType():可以得到請求頭的內容區數據類型(即Content-Type頭的值)
request.getCharacterEncoding():如“Content-Type:application/json;charset=GBK”,則得到的編碼爲“GBK”,否則如果你設置過濾器(CharacterEncodingFilter)則得到它設置的編碼,否則返回null。
request.getParameter():因爲請求的內容區數據爲application/x-www-form-urlencoded格式的數據,因此我們可以通過request.getParameter()得到相應參數數據。
request中設置請求頭“Content-Type:application/json;charset=GBK”表示請求的內容區數據爲json類型數據,且內容區的數據以GBK進行編碼;
(1、控制器cn.javass.chapter6.web.controller.consumesproduces.contenttype.RequestContentTypeController
@RequestMapping(value = "/request/ContentType", method = RequestMethod.POST,
headers = "Content-Type=application/json")
public String request2(HttpServletRequest request) throws IOException {
//①表示請求的內容區數據爲json數據
InputStream is = request.getInputStream();
byte bytes[] = new byte[request.getContentLength()];
is.read(bytes);
//②得到請求中的內容區數據(以CharacterEncoding解碼)
//此處得到數據後你可以通過如json-lib轉換爲其他對象
String jsonStr = new String(bytes, request.getCharacterEncoding());
System.out.println("json data:" + jsonStr);
return "success";
}
request2功能處理方法:只對請求頭爲“Content-Type:application/json”的進行請求處理(即消費請求內容區數據);
request.getContentLength():可以得到請求頭的內容區數據的長度;
request.getCharacterEncoding():如“Content-Type:application/json;charset=GBK”,則得到的編碼爲“GBK”,否則如果你設置過濾器(CharacterEncodingFilter)則得到它設置的編碼,否則返回null。
我們得到json的字符串形式後就能很簡單的轉換爲JSON相關的對象。
(2、客戶端發送json數據請求
//請求的地址
String url = "http://localhost:9080/springmvc-chapter6/request/ContentType";
//①創建Http Request(內部使用HttpURLConnection)
ClientHttpRequest request =
new SimpleClientHttpRequestFactory().
createRequest(new URI(url), HttpMethod.POST);
//②設置請求頭的內容類型頭和內容編碼(GBK)
request.getHeaders().set("Content-Type", "application/json;charset=gbk");
//③以GBK編碼寫出請求內容體
String jsonData = "{\"username\":\"zhang\", \"password\":\"123\"}";
request.getBody().write(jsonData.getBytes("gbk"));
//④發送請求並得到響應
ClientHttpResponse response = request.execute();
System.out.println(response.getStatusCode());
此處我們使用Spring提供的Http客戶端API SimpleClientHttpRequestFactory創建了請求並設置了請求的Content-Type和編碼並在響應體中寫回了json數據(即生產json類型的數據),此處是硬編碼,實際工作可以使用json-lib等工具進行轉換。
具體代碼在cn.javass.chapter6.web.controller.consumesproduces.contenttype.RequestContentTypeClient。
2.2、響應頭的內容類型,表示發送到客戶端的內容數據類型,和請求頭的內容類型類似,只是方向相反。
@RequestMapping("/response/ContentType")
public void response1(HttpServletResponse response) throws IOException {
//①表示響應的內容區數據的媒體類型爲html格式,且編碼爲utf-8(客戶端應該以utf-8解碼)
response.setContentType("text/html;charset=utf-8");
//②寫出響應體內容
response.getWriter().write("<font style='color:red'>hello</font>");
}
<!--[endif]-->
如上所示,通過response.setContentType("text/html;charset=utf-8") 告訴客戶端響應體媒體類型爲html,編碼爲utf-8,大家可以通過chrome工具查看響應頭爲“Content-Type:text/html;charset=utf-8”,還一個“Content-Length:36”表示響應體大小。
代碼在cn.javass.chapter6.web.controller.consumesproduces.contenttype.ResponseContentTypeController。
如上代碼可以看出Content-Type可以指定請求/響應的內容體的媒體格式和可選的編碼方式。如圖6-9
①客戶端—發送請求—服務器:客戶端通過請求頭Content-Type指定內容體的媒體類型(即客戶端此時是生產者),服務器根據Content-Type消費內容體數據(即服務器此時是消費者);
②服務器—發送請求—客戶端:服務器生產響應頭Content-Type指定的響應體數據(即服務器此時是生產者),客戶端根據Content-Type消費內容體數據(即客戶端此時是消費者)。
問題:
①服務器端可以通過指定【headers = "Content-Type=application/json"】來聲明可處理(可消費)的媒體類型,即只消費Content-Type指定的請求內容體數據;
②客戶端如何告訴服務器端它只消費什麼媒體類型的數據呢?即客戶端接受(需要)什麼類型的數據呢?服務器應該生產什麼類型的數據?此時我們可以請求的Accept請求頭來實現這個功能。
三、Accept:用來指定什麼媒體類型的響應是可接受的,即告訴服務器我需要什麼媒體類型的數據,此時服務器應該根據Accept請求頭生產指定媒體類型的數據。
2.1、json數據
(1、服務器端控制器
@RequestMapping(value = "/response/ContentType", headers = "Accept=application/json")
public void response2(HttpServletResponse response) throws IOException {
//①表示響應的內容區數據的媒體類型爲json格式,且編碼爲utf-8(客戶端應該以utf-8解碼)
response.setContentType("application/json;charset=utf-8");
//②寫出響應體內容
String jsonData = "{\"username\":\"zhang\", \"password\":\"123\"}";
response.getWriter().write(jsonData);
}
服務器根據請求頭“Accept=application/json”生產json數據。
(2、客戶端端接收服務器端json數據響應
使用瀏覽器測試(Ajax場景使用該方式)
請求地址爲:http://localhost:9080/springmvc-chapter6/response/ContentType,且把修改請求頭Accept改爲“Accept=application/json”:
大家可以下載chrome的JSONView插件來以更好看的方式查看json數據,安裝地址:https://chrome.google.com/webstore/detail/chklaanhfefbnpoihckbnefhakgolnmc
使用普通客戶端測試(服務器之間通信可使用該方式)
private static void jsonRequest() throws IOException, URISyntaxException {
//請求的地址
String url = "http://localhost:9080/springmvc-chapter6/response/ContentType";
//①創建Http Request(內部使用HttpURLConnection)
ClientHttpRequest request =
new SimpleClientHttpRequestFactory().
createRequest(new URI(url), HttpMethod.POST);
//②設置客戶端可接受的媒體類型(即需要什麼類型的響應體數據)
request.getHeaders().set("Accept", "application/json");
//③發送請求並得到響應
ClientHttpResponse response = request.execute();
//④得到響應體的編碼方式
Charset charset = response.getHeaders().getContentType().getCharSet();
//⑤得到響應體的內容
InputStream is = response.getBody();
byte bytes[] = new byte[(int)response.getHeaders().getContentLength()];
is.read(bytes);
String jsonData = new String(bytes, charset);
System.out.println("charset : " + charset + ", json data : " + jsonData);
}
request.getHeaders().set("Accept", "application/json"):表示客戶端只接受(即只消費)json格式的響應數據;
response.getHeaders():可以得到響應頭,從而可以得到響應體的內容類型和編碼、內容長度。
2.2、xml數據
(1、服務器端控制器
@RequestMapping(value = "/response/ContentType", headers = "Accept=application/xml")
public void response3(HttpServletResponse response) throws IOException {
//①表示響應的內容區數據的媒體類型爲xml格式,且編碼爲utf-8(客戶端應該以utf-8解碼)
response.setContentType("application/xml;charset=utf-8");
//②寫出響應體內容
String xmlData = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
xmlData += "<user><username>zhang</username><password>123</password></user>";
response.getWriter().write(xmlData);
}
和生產json數據唯一不同的兩點:請求頭爲“Accept=application/xml”,響應體數據爲xml。
(2、客戶端端接收服務器端xml數據響應
使用瀏覽器測試(Ajax場景使用該方式)
請求地址爲:http://localhost:9080/springmvc-chapter6/response/ContentType,且把修改請求頭Accept改爲“Accept=application/xml”,和json方式類似,此處不再重複。
使用普通客戶端測試(服務器之間通信可使用該方式)
private static void xmlRequest() throws IOException, URISyntaxException {
//請求的地址
String url = "http://localhost:9080/springmvc-chapter6/response/ContentType";
//①創建Http Request(內部使用HttpURLConnection)
ClientHttpRequest request =
new SimpleClientHttpRequestFactory().
createRequest(new URI(url), HttpMethod.POST);
//②設置客戶端可接受的媒體類型(即需要什麼類型的響應體數據)
request.getHeaders().set("Accept", "application/xml");
//③發送請求並得到響應
ClientHttpResponse response = request.execute();
//④得到響應體的編碼方式
Charset charset = response.getHeaders().getContentType().getCharSet();
//⑤得到響應體的內容
InputStream is = response.getBody();
byte bytes[] = new byte[(int)response.getHeaders().getContentLength()];
is.read(bytes);
String xmlData = new String(bytes, charset);
System.out.println("charset : " + charset + ", xml data : " + xmlData);
}
request.getHeaders().set("Accept", "application/xml"):表示客戶端只接受(即只消費)xml格式的響應數據;
response.getHeaders():可以得到響應頭,從而可以得到響應體的內容類型和編碼、內容長度。
許多開放平臺,都提供了同一種數據的多種不同的表現形式,此時我們可以根據Accept請求頭告訴它們我們需要什麼類型的數據,他們根據我們的Accept來判斷需要返回什麼類型的數據。
實際項目使用Accept請求頭是比較麻煩的,現在大多數開放平臺(國內的新浪微博、淘寶、騰訊等開放平臺)使用如下兩種方式:
擴展名:如response/ContentType.json response/ContentType.xml方式,使用擴展名錶示需要什麼類型的數據;
參數:如response/ContentType?format=json response/ContentType?format=xml,使用參數表示需要什麼類型的數據;
也就是說,目前我們可以使用如上三種方式實現來告訴服務器我們需要什麼類型的數據,但麻煩的是現在有三種實現方式,難道我們爲了支持三種類型的數據就要分別進行三種實現嗎?當然不要這麼麻煩,後續我們會學ContentNegotiatingViewResolver,它能幫助我們做到這一點。
6.6.5.2、生產者消費者流程圖
生產者消費者流程,如圖6-10:
從圖6-10可以看出:
請求階段:客戶端是生產者【生產Content-Type媒體類型的請求內容區數據】,服務器是消費者【消費客戶端生產的Content-Type媒體類型的請求內容區數據】;
響應階段:服務器是生產者【生產客戶端請求頭參數Accept指定的響應體數據】,客戶端是消費者【消費服務器根據Accept請求頭生產的響應體數據】。
如上生產者/消費者寫法無法很好的體現我們分析的生產者/消費者模式,Spring3.1爲生產者/消費者模式提供了簡化支持,接下來我們學習一下如何在Spring3.1中來實現生產者/消費者模式吧。
6.6.5.3、生產者、消費者限定
Spring3.1開始支持消費者、生產者限定,而且必須使用如下HandlerMapping和HandlerAdapter才支持:
<!--Spring3.1開始的註解 HandlerMapping -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<!--Spring3.1開始的註解 HandlerAdapter -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
一、功能處理方法是消費者
@RequestMapping(value = "/consumes", consumes = {"application/json"}):此處使用consumes來指定功能處理方法能消費的媒體類型,其通過請求頭的“Content-Type”來判斷。
此種方式相對使用@RequestMapping的“headers = "Content-Type=application/json"”更能表明你的目的。
服務器控制器代碼詳解cn.javass.chapter6.web.controller.consumesproduces.ConsumesController;
客戶端代碼類似於之前的Content-Type中的客戶端,詳見ConsumesClient.java代碼。
二、功能處理方法是生產者
@RequestMapping(value = "/produces", produces = "application/json"):表示將功能處理方法將生產json格式的數據,此時根據請求頭中的Accept進行匹配,如請求頭“Accept:application/json”時即可匹配;
@RequestMapping(value = "/produces", produces = "application/xml"):表示將功能處理方法將生產xml格式的數據,此時根據請求頭中的Accept進行匹配,如請求頭“Accept:application/xml”時即可匹配。
此種方式相對使用@RequestMapping的“headers = "Accept=application/json"”更能表明你的目的。
服務器控制器代碼詳解cn.javass.chapter6.web.controller.consumesproduces.ProducesController;
客戶端代碼類似於之前的Content-Type中的客戶端,詳見ProducesController.java代碼。
當你有如下Accept頭:
①Accept:text/html,application/xml,application/json
將按照如下順序進行produces的匹配 ①text/html ②application/xml ③application/json
②Accept:application/xml;q=0.5,application/json;q=0.9,text/html
將按照如下順序進行produces的匹配 ①text/html ②application/json ③application/xml
q參數爲媒體類型的質量因子,越大則優先權越高(從0到1)
③Accept:*/*,text/*,text/html
將按照如下順序進行produces的匹配 ①text/html ②text/* ③*/*
即匹配規則爲:最明確的優先匹配。
代碼詳見ProducesPrecedenceController1、ProducesPrecedenceController2、ProducesPrecedenceController3。
Accept詳細信息,請參考http://tools.ietf.org/html/rfc2616#section-14.1。
三、窄化時是覆蓋 而 非繼承
如類級別的映射爲 @RequestMapping(value="/narrow", produces="text/html"),方法級別的爲@RequestMapping(produces="application/xml"),此時方法級別的映射將覆蓋類級別的,因此請求頭“Accept:application/xml”是成功的,而“text/html”將報406錯誤碼,表示不支持的請求媒體類型。
詳見cn.javass.chapter6.web.controller.consumesproduces.NarrowController。
只有生產者/消費者 模式 是 覆蓋,其他的使用方法是繼承,如headers、params等都是繼承。
四、組合使用是“或”的關係
@RequestMapping(produces={"text/html", "application/json"}) :將匹配“Accept:text/html”或“Accept:application/json”。
五、問題
消費的數據,如JSON數據、XML數據都是由我們讀取請求的InputStream並根據需要自己轉換爲相應的模型數據,比較麻煩;
生產的數據,如JSON數據、XML數據都是由我們自己先把模型數據轉換爲json/xml等數據,然後輸出響應流,也是比較麻煩的。
Spring提供了一組註解(@RequestBody、@ResponseBody
)和一組轉換類(HttpMessageConverter
)來完成我們遇到的問題,詳見6.6.8節。