本文介紹Spring MVC中的一個極其重要的組件:HttpMessageConverter消息轉換器。
有一副非常著名的圖,來形容Spring MVC對一個請求的處理:
從圖中可見HttpMessageConverter對Spring MVC的重要性。它對請求、響應都起到了非常關鍵的作用~
為何需要消息轉換器
HttpMessageConverter是用來處理request和response里的數據的。
請求和響應都有對應的body,而這個body就是需要關注的主要數據。
請求體的表述一般就是一段字符串,當然也可以是二進制數據(比如上傳~)。
響應體則是瀏覽器渲染頁面的依據,對於一個普通html頁面得響應,響應體就是這個html頁面的源代碼。
請求體和響應體都是需要配合Content-Type頭部使用的,這個頭部主要用於說明body中得字符串是什么格式的,比如:text,json,xml等。對於請求報文,只有通過此頭部,服務器才能知道怎么解析請求體中的字符串,對於響應報文,瀏覽器通過此頭部才知道應該怎么渲染響應結果,是直接打印字符串還是根據代碼渲染為一個網頁。
對於HttpServletRequest和HttpServletResponse,可以分別調用getInputStream和getOutputStream來直接獲取body。但是獲取到的僅僅只是一段字符串。
而對於java來說,處理一個對象肯定比處理一個字符串要方便得多,也好理解得多。所以根據Content-Type頭部,將body字符串轉換為java對象是常有的事。反過來,根據Accept頭部,將java對象轉換為客戶端期望格式的字符串也是必不可少的工作。
消息轉換器它能屏蔽你對底層轉換的實現,分離你的關注點,讓你專心操作java對象,其余的事情你就交給我Spring MVC~大大提高你的編碼效率(可議說比原生Servlet開發高級太多了)。
Spring內置了很多HttpMessageConverter,比如MappingJackson2HttpMessageConverter,StringHttpMessageConverter,甚至還有FastJsonHttpMessageConverter(需導包和自己配置)。
HttpMessageConverter
在具體講解之前,先對所有的轉換器來個概述:
名稱 | 作用 | 讀支持MediaType | 寫支持MediaType | 備注 |
FormHttpMessageConverter | 表單與MultiValueMap的相互轉換 | application/x-www-form-urlencoded | application/x-www-form-urlencoded和multipart/form-data | 可用於處理下載 |
XmlAwareFormHttpMessageConverter | Spring3.2后已過期,使用下面AllEnc…代替 | 略 | 略 | |
AllEncompassingFormHttpMessageConverter | 對FormHttp…的擴展,提供了對xml和json的支持 | 同上 | 同上 | |
SourceHttpMessageConverter | 數據與javax.xml.transform.Source的相互轉換 | application/xml和text/xml和application/*+xml | 同read | 和Sax/Dom等有關 |
ResourceHttpMessageConverter | 數據與org.springframework.core.io.Resource | */* | */* | |
ByteArrayHttpMessageConverter | 數據與字節數組的相互轉換 | */* | application/octet-stream | |
ObjectToStringHttpMessageConverter | 內部持有一個StringHttpMessageConverter和ConversionService | 它倆的&& | 它倆的&& | |
RssChannelHttpMessageConverter | 處理RSS <channel> 元素 | application/rss+xml | application/rss+xml | 很少接觸 |
MappingJackson2HttpMessageConverter | 使用Jackson的ObjectMapper轉換Json數據 | application/json和application/*+json | application/json和application/*+json | 默認編碼UTF-8 |
MappingJackson2XmlHttpMessageConverter | 使用Jackson的XmlMapper轉換XML數據 | application/xml和text/xml | application/xml和text/xml | 需要額外導包Jackson-dataformat-XML才能生效。從Spring4.1后才有 |
GsonHttpMessageConverter | 使用Gson處理Json數據 | application/json | application/json | 默認編碼UTF-8 |
ResourceRegionHttpMessageConverter | 數據和org.springframework.core.io.support.ResourceRegion的轉換 | application/octet-stream | application/octet-stream | Spring4.3才提供此類 |
ProtobufHttpMessageConverter | 轉換com.google.protobuf.Message數據 | application/x-protobuf和text/plain和application/json和application/xml | 同read | @since 4.1 |
StringHttpMessageConverter | 數據與String類型的相互轉換 | */* | */* | 轉成字符串的默認編碼為ISO-8859-1 |
BufferedImageHttpMessageConverter | 數據與java.awt.image.BufferedImage的相互轉換 | Java I/O API支持的所有類型 | Java I/O API支持的所有類型 | |
FastJsonHttpMessageConverter | 使用FastJson處理Json數據 | */* | */* | 需要導入Jar包和自己配置,Spring並不默認內置 |
需要知道的是:上面說的支持都說的是默認支持,當然你是可以自定義讓他們更強大的。比如:我們可以自己配置StringHttpMessageConverter,改變(增強)它的默認行為:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
既然它是HttpMessageConverter,所以鐵定和HttpMessage有關,因為此接口涉及的內容相對來說比較偏底層,因此本文只在接口層面做簡要的一個說明。
HttpMessage
它是Spring 3.0后增加一個非常抽象的接口。表示:表示HTTP請求和響應消息的基本接口。
public interface HttpMessage {
// Return the headers of this message
HttpHeaders getHeaders();
}
看看它的繼承樹:
HttpInputMessage和HttpOutputMessage
這就是目前都在使用的接口,表示輸入、輸出信息~
public interface HttpInputMessage extends HttpMessage {
InputStream getBody() throws IOException;
}
public interface HttpOutputMessage extends HttpMessage {
OutputStream getBody() throws IOException;
}
HttpRequest
代表着一個Http請求信息,提供了多的幾個API,是對HttpMessage的一個補充。Spring3.1新增的
public interface HttpRequest extends HttpMessage {
@Nullable
default HttpMethod getMethod() {
// 可議根據String類型的值 返回一個枚舉
return HttpMethod.resolve(getMethodValue());
}
String getMethodValue();
// 可以從請求消息里 拿到URL
URI getURI();
}
ReactiveHttpInputMessage和ReactiveHttpOutputMessage
顯然,是Spring5.0新增的接口,也是Spring5.0最重磅的升級之一。自此Spring容器就不用強依賴於Servlet容器了。它還可以選擇依賴於reactor這個框架。
比如這個類:reactor.core.publisher.Mono就是Reactive的核心類之一~
因為屬於Spring5.0的最重要的新特性之一,所以此處也不再過多介紹了。
HttpMessageConverter接口是Spring3.0之后新增的一個接口,它負責將請求信息轉換為一個對象(類型為T),並將對象(類型為T)綁定到請求方法的參數中或輸出為響應信息
// @since 3.0 Spring3.0后推出的 是個泛型接口
// 策略接口,指定可以從HTTP請求和響應轉換為HTTP請求和響應的轉換器
public interface HttpMessageConverter<T> {
// 指定轉換器可以讀取的對象類型,即轉換器可將請求信息轉換為clazz類型的對象
// 同時支持指定的MIME類型(text/html、application/json等)
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
// 指定轉換器可以將clazz類型的對象寫到響應流當中,響應流支持的媒體類型在mediaType中定義
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
// 返回當前轉換器支持的媒體類型~~
List<MediaType> getSupportedMediaTypes();
// 將請求信息轉換為T類型的對象 流對象為:HttpInputMessage
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
// 將T類型的對象寫到響應流當中,同事指定響應的媒體類型為contentType 輸出流為:HttpOutputMessage
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}
看看它的繼承樹:
按照層級划分,它的直接子類是如下四個:
FormHttpMessageConverter、AbstractHttpMessageConverter、BufferedImageHttpMessageConverter、GenericHttpMessageConverter(Spring3.2出來的,支持到了泛型)。
FormHttpMessageConverter:form表單提交/文件下載
從名字知道,它和Form表單有關。瀏覽器原生表單默認的提交數據的方式(就是沒有設置enctype屬性),它默認是這個:Content-Type: application/x-www-form-urlencoded;charset=utf-8
從請求和響應讀取/編寫表單數據。默認情況下,它讀取媒體類型 application/x-www-form-urlencoded 並將數據寫入 MultiValueMap<String,String>。因為它獨立的存在,所以可以看看源碼內容:
// @since 3.0
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
// 默認UTF-8編碼 MediaType為:application/x-www-form-urlencoded
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE = new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET);
// 緩存下它所支持的MediaType們
private List<MediaType> supportedMediaTypes = new ArrayList<>();
// 用於二進制內容的消息轉換器們~~~ 畢竟此轉換器還支持`multipart/form-data`這種 可以進行文件下載~~~~~
private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();
private Charset charset = DEFAULT_CHARSET;
@Nullable
private Charset multipartCharset;
// 唯一的一個構造函數~
public FormHttpMessageConverter() {
// 默認支持處理兩種MediaType:application/x-www-form-urlencoded和multipart/form-data
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
// === 它自己不僅是個轉換器,還內置了這三個轉換器 至於他們具體處理那種消息,請看下面 都有詳細說明 ==
// 注意:這些消息轉換器都是去支持part的,支持文件下載
this.partConverters.add(new ByteArrayHttpMessageConverter());
this.partConverters.add(stringHttpMessageConverter);
this.partConverters.add(new ResourceHttpMessageConverter());
// 這是為partConverters設置默認的編碼~~~
applyDefaultCharset();
}
// 省略屬性額get/set方法
// 從這可以發現,只有Handler的入參類型是是MultiValueMap它才會去處理~~~~
@Override
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
return false;
}
// 若沒指定MedieType 會認為是可讀的~
if (mediaType == null) {
return true;
}
// 顯然,只有我們Supported的MediaType才會是true(當然multipart/form-data例外,此處是不可讀的)
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
// We can't read multipart....
if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {
return true;
}
}
return false;
}
// 注意和canRead的區別,有點對着干的意思~~~
@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
return false;
}
// 如果是ALL 說明支持所有的類型 那就恆返回true 當然null也是的
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
// isCompatibleWith是否是兼容的
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
}
// 把輸入信息讀進來,成為一個 MultiValueMap<String, String>
// 注意:此處發現class這個變量並沒有使用~
@Override
public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
// 拿到請求的ContentType請求頭~~~~
MediaType contentType = inputMessage.getHeaders().getContentType();
// 這里面 編碼放在contentType里面 若沒有指定 走默認的編碼
// 類似這種形式就是我們自己指定了編碼:application/json;charset=UTF-8
Charset charset = (contentType != null && contentType.getCharset() != null ? contentType.getCharset() : this.charset);
// 把body的內容讀成字符串~
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
// 用"&"分隔 因為此處body一般都是hello=world&fang=shi這樣傳進來的
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
// 這個就不說了,就是把鍵值對保存在map里面。注意:此處為何用多值Map呢?因為一個key可能是會有多個value的
for (String pair : pairs) {
int idx = pair.indexOf('=');
if (idx == -1) {
result.add(URLDecoder.decode(pair, charset.name()), null);
}
else {
String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
result.add(name, value);
}
}
return result;
}
}
AbstractHttpMessageConverter
一個基礎抽象實現,它是個泛型類。對於泛型的控制,有如下特點:
- 最廣的可以選擇Object,不過Object並不都是可以序列化的,但是子類可以在覆蓋supports方法中進一步控制,因此選擇Object是可以的
- 最符合的是Serializable,既完美滿足泛型定義,本身也是個Java序列化/反序列化的充要條件
- 自定義的基類Bean,有些技術規范要求自己代碼中的所有bean都繼承自同一個自定義的基類BaseBean,這樣可以在Serializable的基礎上再進一步控制,滿足自己的業務要求
若我們自己需要自定義一個消息轉換器,大多數情況下也是繼承抽象類再具體實現。比如我們最熟悉的:FastJsonHttpMessageConverter它就是一個子類實現
public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {
// 它主要內部維護了這兩個屬性,可議構造器賦值,也可以set方法賦值~~
private List<MediaType> supportedMediaTypes = Collections.emptyList();
@Nullable
private Charset defaultCharset;
// supports是個抽象方法,交給子類自己去決定自己支持的轉換類型~~~~
// 而canRead(mediaType)表示MediaType也得在我支持的范疇了才行(入參MediaType若沒有指定,就返回true的)
@Override
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
return supports(clazz) && canRead(mediaType);
}
// 原理基本同上,supports和上面是同一個抽象方法 所以我們發現並不能入參處理Map,出餐處理List等等
@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
return supports(clazz) && canWrite(mediaType);
}
// 這是Spring的慣用套路:readInternal 雖然什么都沒做,但我覺得還是挺有意義的。Spring后期也非常的好擴展了~~~~
@Override
public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return readInternal(clazz, inputMessage);
}
// 整體上就write方法做了一些事~~
@Override
public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
// 設置一個headers.setContentType 和 headers.setContentLength
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
// StreamingHttpOutputMessage增加的setBody()方法,關於它下面會給一個使用案例~~~~
streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
// 注意此處復寫:返回的是outputStream ,它也是靠我們的writeInternal對它進行寫入的~~~~
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
}
// 最后它執行了flush,這也就是為何我們自己一般不需要flush的原因
else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
// 三個抽象方法
protected abstract boolean supports(Class<?> clazz);
protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
protected abstract void writeInternal(T t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
關於StreamingHttpOutputMessage的使用:
表示允許設置流正文的HTTP輸出消息,需要注意的是,此類消息通常不支持getBody()訪問
// @since 4.0
public interface StreamingHttpOutputMessage extends HttpOutputMessage {
// 設置一個流的正文,提供回調
void setBody(Body body);
// 定義可直接寫入@link outputstream的主體的協定。
// 通過回調機制間接的訪問HttpClient庫很有作用
@FunctionalInterface
interface Body {
// 把當前的這個body寫進給定的OutputStream
void writeTo(OutputStream outputStream) throws IOException;
}
}
SourceHttpMessageConverter
處理一些和xml相關的資源,比如DOMSource、SAXSource、SAXSource等等。
ResourceHttpMessageConverter
負責讀取資源文件和寫出資源文件數據
它來處理把Resource進行寫出去。當然它也可以把body的內容寫進到Resource里來。
public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
// 是否支持讀取流信息
private final boolean supportsReadStreaming;
// 默認支持所有的MediaType~~~~~ 但是它有個類型匹配,所以值匹配入參/返回類型是Resource類型的
public ResourceHttpMessageConverter() {
super(MediaType.ALL);
this.supportsReadStreaming = true;
}
@Override
protected boolean supports(Class<?> clazz) {
return Resource.class.isAssignableFrom(clazz);
}
// 直觀感受:讀的時候也只支持InputStreamResource和ByteArrayResource這兩種resource的直接封裝
@Override
protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
if (this.supportsReadStreaming && InputStreamResource.class == clazz) {
return new InputStreamResource(inputMessage.getBody()) {
@Override
public String getFilename() {
return inputMessage.getHeaders().getContentDisposition().getFilename();
}
};
}
// 若入參類型是Resource接口,也是當作ByteArrayResource處理的
else if (Resource.class == clazz || ByteArrayResource.class.isAssignableFrom(clazz)) {
// 把inputSteeam轉換為byte[]數組~~~~~~
byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
return new ByteArrayResource(body) {
@Override
@Nullable
public String getFilename() {
return inputMessage.getHeaders().getContentDisposition().getFilename();
}
};
}
else {
throw new HttpMessageNotReadableException("Unsupported resource class: " + clazz, inputMessage);
}
}
@Override
protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
writeContent(resource, outputMessage);
}
// 寫也非常的簡單,就是把resource這個資源的內容寫到body里面去,此處使用的StreamUtils.copy這個工具方法,專門處理流
// 看到此處我們自己並不需要flush,但是需要自己關閉流
protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
try {
InputStream in = resource.getInputStream();
try {
StreamUtils.copy(in, outputMessage.getBody());
}
catch (NullPointerException ex) {
// ignore, see SPR-13620
}
finally {
try {
in.close();
}
catch (Throwable ex) {
// ignore, see SPR-12999
}
}
}
catch (FileNotFoundException ex) {
// ignore, see SPR-12999
}
}
}
使用它模擬完成上傳功能
上傳表單如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>測試FormHttpMessageConverter</title>
</head>
<body>
<!-- 表單的enctype一定要標注成multipart形式,否則是拿不到二進制流的 -->
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
用戶名 <input type="text" name="userName">
頭像 <input type="file" name="touxiang">
<input type="submit">
</form>
</body>
</html>
// 模擬使用Resource進行文件的上傳~~~
@ResponseBody
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public String upload(@RequestBody Resource resource) { //此處不能用接口Resource resource
dumpStream(resource);
return "success";
}
// 模擬寫文件的操作(此處寫到控制台)
private static void dumpStream(Resource resource) {
InputStream is = null;
try {
//1.獲取文件資源
is = resource.getInputStream();
//2.讀取資源
byte[] descBytes = new byte[is.available()];
is.read(descBytes);
System.out.println(new String(descBytes, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//3.關閉資源
is.close();
} catch (IOException e) {
}
}
}
控制台結果為:
由此可見利用它是可以把客戶端的資源信息都拿到的,從而間接的實現文件的上傳的功能。
ByteArrayHttpMessageConverter
和上面類似。
ObjectToStringHttpMessageConverter
它是對StringHttpMessageConverter的一個擴展。它在Spring內部並沒有裝配進去。若我們需要,可以自己裝配到Spring MVC里面去
public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
// 我們只需要自定定義這個轉換器 讓它實現String到Obj之間的互相轉換~~~
private final ConversionService conversionService;
private final StringHttpMessageConverter stringHttpMessageConverter;
... // 下面省略
// 讀的時候先用stringHttpMessageConverter讀成String,再用轉換器轉為Object對象
// 寫的時候先用轉換器轉成String,再用stringHttpMessageConverter寫進返回的body里
}
Json相關轉換器
可以看到一個是谷歌陣營,一個是jackson陣營。
GsonHttpMessageConverter
利用谷歌的Gson進行json序列化的處理~~~
// @since 4.1 可見它被Spring選中的時間還是比較晚的
public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
private Gson gson;
public GsonHttpMessageConverter() {
this.gson = new Gson();
}
// @since 5.0 調用者可以自己指定一個Gson對象了
public GsonHttpMessageConverter(Gson gson) {
Assert.notNull(gson, "A Gson instance is required");
this.gson = gson;
}
// 因為肯定是文本,所以這里使用Reader 沒有啥問題
// 父類默認用UTF-8把inputStream轉為了更友好的Reader
@Override
protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
return getGson().fromJson(reader, resolvedType);
}
@Override
protected void writeInternal(Object o, @Nullable Type type, Writer writer) throws Exception {
// 如果帶泛型 這里也是特別的處理了兼容處理~~~~
if (type instanceof ParameterizedType) {
getGson().toJson(o, type, writer);
} else {
getGson().toJson(o, writer);
}
}
// 父類定義了它支持的MediaType類型~
public AbstractJsonHttpMessageConverter() {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
setDefaultCharset(DEFAULT_CHARSET);
}
}
MappingJackson2HttpMessageConverter
利用Jackson進行json序列化
// @since 3.1.2 出來可謂最早。正統太子
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
// 該屬性在父類定義~~~
protected ObjectMapper objectMapper;
@Nullable
private String jsonPrefix;
// 支持指定的MediaType類型~~
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
// 所有的讀、寫都在父類AbstractJackson2HttpMessageConverter里統一實現的,稍微有點復雜性
}
若你的返回值是Map、List等,只要MediaType對上了,這種json處理器都是可以處理的。因為他們泛型上都是Object表示入參、 返回值任意類型都可以處理~~~
StringHttpMessageConverter
這個是使用得非常廣泛的一個消息轉換器,專門處理入參/出參字符串類型。
// @since 3.0 出生非常早
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
// 這就是為何你return中文的時候會亂碼的原因(若你不設置它的編碼的話~)
public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;
@Nullable
private volatile List<Charset> availableCharsets;
// 標識是否輸出 Response Headers:Accept-Charset(默認true表示輸出)
private boolean writeAcceptCharset = true;
public StringHttpMessageConverter() {
this(DEFAULT_CHARSET);
}
public StringHttpMessageConverter(Charset defaultCharset) {
super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
}
//Indicates whether the {@code Accept-Charset} should be written to any outgoing request.
// Default is {@code true}.
public void setWriteAcceptCharset(boolean writeAcceptCharset) {
this.writeAcceptCharset = writeAcceptCharset;
}
// 只處理String類型~
@Override
public boolean supports(Class<?> clazz) {
return String.class == clazz;
}
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
// 哪編碼的原則為:
// 1、contentType自己指定了編碼就以指定的為准
// 2、沒指定,但是類型是`application/json`,統一按照UTF_8處理
// 3、否則使用默認編碼:getDefaultCharset ISO_8859_1
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
// 按照此編碼,轉換為字符串~~~
return StreamUtils.copyToString(inputMessage.getBody(), charset);
}
// 顯然,ContentLength和編碼也是有關的~~~
@Override
protected Long getContentLength(String str, @Nullable MediaType contentType) {
Charset charset = getContentTypeCharset(contentType);
return (long) str.getBytes(charset).length;
}
@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
// 默認會給請求設置一個接收的編碼格式~~~(若用戶不指定,是所有的編碼都支持的)
if (this.writeAcceptCharset) {
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
}
// 根據編碼把字符串寫進去~
Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
StreamUtils.copy(str, charset, outputMessage.getBody());
}
...
}
我們也可以這么來寫,達到我們一定的目的:
// 因為它支持MediaType.TEXT_PLAIN, MediaType.ALL所有類型,所以你的contentType無所謂~~~ 它都能夠處理
@ResponseBody
@RequestMapping(value = "/test", method = RequestMethod.POST)
public String upload(@RequestBody String body) {
return "Hello World";
}
這種書寫方式它不管是入參,還是返回值處理的轉換器,都是用到的StringHttpMessageConverter。用它來接收入參和上面例子Resource有點像,只是StringHttpMessageConverter它只能解析文本內容,而Resource可以處理所有。
需要注意的是:若你的項目中大量使用到了此轉換器,請一定要注意編碼問題。一般不建議直接使用StringHttpMessageConverter,而是我們配置好編碼(UTF-8)后,再把它加入到Spring MVC里面,這樣就不會有亂碼問題了
BufferedImageHttpMessageConverter
處理java.awt.image.BufferedImage,和awt相關。
GenericHttpMessageConverter 子接口
GenericHttpMessageConverter接口繼承自HttpMessageConverter接口,二者都是在org.springframework.http.converter包下。它的特點就是:它處理目標類型為泛型類型的類型~~~
public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T> {
//This method should perform the same checks than {@link HttpMessageConverter#canRead(Class, MediaType)} with additional ones related to the generic type.
// 它的效果同父接口的canRead,但是它是加了一個泛型類型~~~來加以更加詳細的判斷
boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType);
// 一樣也是加了泛型類型
T read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
//@since 4.2
boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType);
// @since 4.2
void write(T t, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}
可以看出處理Json方面的轉換器,都實現了此接口。此處主要以阿里巴巴的FastJson轉換器為例加以說明:
FastJsonHttpMessageConverter
// Fastjson for Spring MVC Converter. Compatible Spring MVC version 3.2+
// @since 1.2.10
public class FastJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
implements GenericHttpMessageConverter<Object> {
public FastJsonHttpMessageConverter() {
super(MediaType.ALL);
}
// 永遠返回true,表示它想支持所有的類型,所有的MediaType,現在這算一個小Bug
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
// 它竟然對泛型Type都沒有任何的實現,這也是一個小bug
// 包括讀寫的時候 對泛型類型都沒有做很好的處理~~~
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
return super.canRead(contextClass, mediaType);
}
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return super.canWrite(clazz, mediaType);
}
// 這是處理讀的方法,主要依賴於JSON.parseObject這個方法解析成一個object
private Object readType(Type type, HttpInputMessage inputMessage) {
try {
InputStream in = inputMessage.getBody();
return JSON.parseObject(in,
fastJsonConfig.getCharset(),
type,
fastJsonConfig.getParserConfig(),
fastJsonConfig.getParseProcess(),
JSON.DEFAULT_PARSER_FEATURE,
fastJsonConfig.getFeatures());
} catch (JSONException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getMessage(), ex);
} catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex);
}
}
}
總體來說,如果你是FastJson的死忠粉,你可以替換掉默認的Jackson的實現方式。但是由於FastJson在效率在對標Jackson並沒有多少優勢,所以絕大多數情況下,我並不建議修改Spring MVC處理json的默認行為。
ResourceRegionHttpMessageConverter
和org.springframework.core.io.support.ResourceRegion有關,它只能寫為一個ResourceRegion或者一個它的List
只能寫不能讀,讀方法都會拋異常~
// 這個類很簡單,就是對Resource的一個包裝 所以它和`application/octet-stream`也是有關的
// @since 4.3
public class ResourceRegion {
private final Resource resource;
private final long position;
private final long count;
...
}
若你報錯說ResourceRegionHttpMessageConverter類找不到,請檢查你的Spring版本。因此此類@since 4.3
自定義消息轉換器PropertiesHttpMessageConverter處理Properties類型數據
自定義的主要目的是加深對消息轉換器的理解。此處我們仍然是通過繼承AbstractHttpMessageConverter方式來擴展:
public class PropertiesHttpMessageConverter extends AbstractHttpMessageConverter<User> {
// 用於僅僅只處理我自己自定義的指定的MediaType
private static final MediaType DEFAULT_MEDIATYPE = MediaType.valueOf("application/properties");
public PropertiesHttpMessageConverter() {
super(DEFAULT_MEDIATYPE);
setDefaultCharset(StandardCharsets.UTF_8);
}
// 要求入參、返回值必須是User類型我才處理
@Override
protected boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(User.class);
}
@Override
protected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
InputStream is = inputMessage.getBody();
Properties props = new Properties();
props.load(is);
// user的三個屬性
String id = props.getProperty("id");
String name = props.getProperty("name");
String age = props.getProperty("age");
return new User(Integer.valueOf(id), name, Integer.valueOf(age));
}
@Override
protected void writeInternal(User user, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
OutputStream os = outputMessage.getBody();
Properties properties = new Properties();
// 屬性判空此處我就不做了~~~
properties.setProperty("id", user.getId().toString());
properties.setProperty("name", user.getName());
properties.setProperty("age", user.getAge().toString());
properties.store(os, "user comments");
}
}
其實發現,處理代碼並不多。需要注意的是:此處我們只處理我們自定義的:application/properties-user這一種MediaType即可,職責范圍放到最小。
接下來就是要注冊進Spring MVC里:
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 因為此轉換器職責已經足夠單一,所以放在首位是木有問題的~
converters.add(0, new PropertiesHttpMessageConverter());
// 若放在末尾,將可能不會生效~~~~(比如如果Fastjson轉換器 處理所有的類型的話,所以放在首位最為保險)
//converters.add(0, new PropertiesHttpMessageConverter());
}
}
這里需要注意的是,為了避免意外,一定要注意自定義消息轉換器的注冊順序問題。
編寫Handler處理器如下:
@ResponseBody
@RequestMapping(value = "/test/properties", method = RequestMethod.POST)
public User upload(@RequestBody User user) {
System.out.println(user);
return user;
}
Spring MVC默認注冊哪些HttpMessageConverter
說明:此處情況完全以Spring MVC版本講解,和Spring Boot無關。
Spring 版本號為:5.1.6.RELEASE
- 不開啟該注解:@EnableWebMvc
- 開啟該注解:@EnableWebMvc
可以看到@EnableWebMvc注解的“威力”還是蠻大的,一下子讓Spring MVC變強不少,所以一般情況下,我是建議開啟它的。
在純Spring環境下,我是無理由建議標注@EnableWebMvc上此注解的。
而且從上面可以看出,若我們classpath下有Jackson的包,那裝配的就是MappingJackson2HttpMessageConverter,若沒有jackson包有gson包,那裝配的就是gson轉換器。
小細節
- 如果一個Controller類里面所有方法的返回值都需要經過消息轉換器,那么可以在類上面加上@ResponseBody注解或者將@Controller注解修改為@RestController注解,這樣做就相當於在每個方法都加上了@ResponseBody注解了(言外之意別的方式都是不會經歷消息轉換器的)
- @ResponseBody和@RequestBody都可以處理Map類型的對象。如果不確定參數的具體字段,可以用Map接收。@ReqeustBody同樣適用。(List也是木有問題的)
- 方法上的和類上的@ResponseBody都可以被繼承
- 默認的xml轉換器Jaxb2RootElementHttpMessageConverter需要類上有@XmlRootElement注解才能被轉換(雖然很少使用但此處還是指出)
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType));
}
- 返回值類型可聲明為基類的類型,不影響轉換(比如我們返回值是Object都是木有關系的)。但參數的類型必需為特定的類型(最好不要用接口類型,當然有的時候也是可以的比如Map/List/Resource等等)。這是顯而易見的
總結
請求和響應都有對應的body,而這個body就是需要關注的主要數據。
請求體與請求的查詢參數或者表單參數是不同的:
請求體的表述一般就是一段字符串(當然也可能是二進制),而查詢參數可以看作url的一部分,這兩個是位於請求報文的不同地方。表單參數可以按照一定格式放在請求體中,也可以放在url上作為查詢參數。
響應體則是瀏覽器渲染頁面的依據,對於一個普通html頁面得響應,響應體就是這個html頁面的源代碼。請求體和響應體都是需要配合Content-Type頭部使用的,這個頭部主要用於說明body中得字符串是什么格式的,比如:text,json,xml等。
- 對於請求報文,只有通過此頭部,服務器才能知道怎么解析請求體中的字符串
- 對於響應報文,瀏覽器通過此頭部才知道應該怎么渲染響應結果,是直接打印字符串還是根據代碼渲染為一個網頁
還有一個與body有關的頭部是Accept,這個頭部標識了客戶端期望得到什么格式的響應體。服務器可根據此字段選擇合適的結果表述。
對於HttpServletRequest和HttpServletResponse,可以分別調用getInputStream和getOutputStream來直接獲取body,但是獲取到的僅僅只是一段字符串。
而對於Java來說,處理一個對象肯定比處理一個字符串要方便得多,也好理解得多。
所以根據Content-Type頭部,將body字符串轉換為java對象是常有的事。反過來,根據Accept頭部,將java對象轉換客戶端期望格式的字符串也是必不可少的工作。
參考: |