Spring MVC九大組件之HandlerAdapter中的消息轉換器HttpMessageConverter


本文介紹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對象轉換客戶端期望格式的字符串也是必不可少的工作。

 

參考:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM