前言
最近看了Spring MVC源碼,感覺特別有趣腰奋,像發(fā)現(xiàn)了新大陸一般裆赵,不能自拔双絮。
闡述問題
最近發(fā)現(xiàn)一個關(guān)于FastJsonHttpMessageConverter特別有趣的一個點弛矛,它默認的supportMediaType竟然是MediaType.ALL够吩。
/**
* Can serialize/deserialize all types.
*/
public FastJsonHttpMessageConverter() {
super(MediaType.ALL);
}
說明FastJsonHttpMessageConverter不配置supportMediaType,那么默認是MediaType.ALL汪诉。
FastJsonHttpMessageConverter應(yīng)該顯式配置其支持的消息格式是MediaType.APPLICATION_JSON_UTF8
//使用阿里 FastJson 作為JSON MessageConverter
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
log.info("converters:" + converters.toString());
List<MediaType> supportMediaTypeList = new ArrayList<>();
// supportMediaTypeList.add(MediaType.TEXT_HTML);
supportMediaTypeList.add(MediaType.APPLICATION_JSON_UTF8);
// supportMediaTypeList.add(MediaType.IMAGE_GIF);
// supportMediaTypeList.add(MediaType.IMAGE_JPEG);
// supportMediaTypeList.add(MediaType.IMAGE_PNG);
FastJsonConfig config = new FastJsonConfig();
// config.setDateFormat("yyyy-MM-dd HH:mm:ss");
config.setSerializerFeatures(
SerializerFeature.WriteMapNullValue,//保留空的字段
SerializerFeature.WriteNullStringAsEmpty,//String null -> ""
SerializerFeature.WriteNullNumberAsZero,//Number null -> 0
SerializerFeature.WriteNullListAsEmpty,//List null-> []
SerializerFeature.WriteNullBooleanAsFalse);//Boolean null -> false
converter.setFastJsonConfig(config);
converter.setSupportedMediaTypes(supportMediaTypeList);
converter.setDefaultCharset(Charset.forName("UTF-8"));
converters.add(converter);
}
我有一個業(yè)務(wù)場景废恋,需要緩存商品詳情頁谈秫,手動渲染Thymeleaf模板扒寄,在Controller中返回html片段鱼鼓。
配置如下:
@ResponseBody
public String list(MiaoshaUser miaoshaUser) throws IOException {
modelMap.addAttribute("user", miaoshaUser);
//取緩存
String htmlCached = redisService.get(GoodsKey.getGoodsList, "", String.class);
if (!StringUtils.isEmpty(htmlCached)) {
return htmlCached;
}
List<GoodsVo> goodsVoList = goodsService.listGoodsVo();
modelMap.addAttribute("goodsList", goodsVoList);
SpringWebContext springWebContext = new SpringWebContext(request, response, request.getServletContext(),
request.getLocale(), modelMap, applicationContext);
String html = thymeleafViewResolver.getTemplateEngine().process("goods_list", springWebContext);
if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
可是最終渲染的界面慘不忍睹,界面出現(xiàn)了大量的\n\t這樣的字符
如果我不用FastJsonHttpMessageConverter呢该编,界面卻顯示很正常迄本,這是為什么呢。
雖然在Controller中可以通過手動打開輸出流课竣,設(shè)置ContentType嘉赎,把Thymeleaf模板輸出到Response的body,可以解決問題于樟。
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setStatus(200);
try {
response.getWriter().write(html);
} catch (IOException ex) {
log.error(ex.getMessage());
}
但是本著阿Q精神公条,我Debug消息轉(zhuǎn)換器的流程,發(fā)現(xiàn)問題出在FastJsonHttpMessageConverter沒有配置MediaType.APPLICATION_JSON_UTF8
,
導致其默認的MediaType.ALL影響Thymeleaf模板最后的輸出迂曲。
Debug之路
RequestResponseBodyProcessor
實現(xiàn)了HandlerMethodReturnValueHandler
靶橱、HandlerMethodAgumentResolver
接口,實現(xiàn)了boolean supportsParamter()
路捧、void resolveArgument()
关霸、boolean supportsReturnType()
、void handleReturnValue()
所以具備了處理參數(shù)杰扫、處理返回值的能力队寇。官方解釋.RequestResponseBodyProcessor
能夠解析用@RequestBody
注解的參數(shù)和通過使用HttpMessageConverter
讀取并寫入請求體或響應(yīng)來處理用@ResponseBody
注解的方法的返回值。
/**
* 處理返回值
*/
public interface HandlerMethodReturnValueHandler {
/**
* 方法返回值是否被處理程序支持
*/
boolean supportsReturnType(MethodParameter returnType);
/**
* 通過向模型添加屬性并設(shè)置視圖或設(shè)置視圖來處理給定的返回值
* {@link ModelAndViewContainer#setRequestHandled}標志到{@code true}章姓,表明響應(yīng)已被直接處理佳遣。
*/
void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
/**
* 解析請求中的參數(shù)值
*/
public interface HandlerMethodArgumentResolver {
/**
* 是否能支持解析{@linkplain MethodParameter參數(shù)}
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 將方法參數(shù)解析為來自給定請求的參數(shù)值。
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {}
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {}
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {}
handleReturnValue()獲取inputMessage,outMessage對象凡伊,調(diào)用writeWithMessageConverters()方法零渐,讓消息轉(zhuǎn)換器對消息進行下一步處理.
public interface HttpMessage {
/**
* Return the headers of this message.
* @return a corresponding HttpHeaders object (never {@code null})
*/
HttpHeaders getHeaders();
}
public interface HttpOutputMessage extends HttpMessage {
/**
* Return the body of the message as an output stream.
* @return the output stream body (never {@code null})
* @throws IOException in case of I/O Errors
*/
OutputStream getBody() throws IOException;
}
public interface HttpInputMessage extends HttpMessage {
/**
* Return the body of the message as an input stream.
* @return the input stream body (never {@code null})
* @throws IOException in case of I/O Errors
*/
InputStream getBody() throws IOException;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
在AbstarctMessageConverterProcessor
的writeWithMessageConverters()
中,
根據(jù)HandlerMethod中的ReturnValueMethodParameter對象獲valueType(返回值類型),同時也能得到outputValue(返回值)窗声、decalredType(返回值實際類型)
Object outputValue;
Class<?> valueType;
Type declaredType;
if (value instanceof CharSequence) {
outputValue = value.toString();
valueType = String.class;
declaredType = String.class;
}
else {
outputValue = value;
valueType = getReturnValueType(outputValue, returnType);
declaredType = getGenericType(returnType);
}
根據(jù)request對象中獲取請求中Accept的屬性值相恃,得requestedMediaTypes
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
}
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
continue;
}
return mediaTypes;
}
return Collections.emptyList();
}
根據(jù)HandlerMapping中的produces屬性獲得producibleMediaTypes。
如果沒有顯式設(shè)置produces屬性笨觅,我們只能通過遍歷所有的HttpMessageConverter拦耐,通過canWrite()方法找到支持解析Java對象的HttpMessageConverter,并且把其所支持的mediaType加入mediaTypes集合里面见剩。
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<MediaType>();
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
通過兩次for循環(huán)杀糯,比較requestedMediaTypes和producibleMediaTypes
得到compatiableMediaTypes。如果compatiableMediaTypes為空苍苞,會拋出HttpMediaTypeNotAcceptableException異常固翰。白話意思就是producibleMediaTypes沒有一個MediaType與requestedMediaTypes匹配狼纬,肯定無法執(zhí)行下一步了。
if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
}
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (compatibleMediaTypes.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
排序骂际、for循環(huán)compatiableMediaTypes,通過isConcrete()判斷消息格式是否具體(類型和子類型是否為通配符*)疗琉,得到selectedMediaType(最終的MediaType)
List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(mediaTypes);
MediaType selectedMediaType = null;
for (MediaType mediaType : mediaTypes) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
for循環(huán)已配置所有的HttpMessageConverter,調(diào)用canWrite()方法歉铝,根據(jù)valueType(返回值類型)和selectedMediaType來判斷消息是否可以轉(zhuǎn)換盈简。
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
}
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
如果沒有配置MediaType.APPLICATION_JSON_UTF8
,默認值是MediaType.ALL
,FastJsonHttpMessageConverter
會去處理消息格式為"text/html;charset=UTF-8"太示,會執(zhí)行這一段代碼
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
接著調(diào)用FastJsonHttpMessageConverter的write()方法寫入消息
/*
* @see org.springframework.http.converter.GenericHttpMessageConverter.write
*/
public void write(Object o, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
super.write(o, contentType, outputMessage);// support StreamingHttpOutputMessage in spring4.0+
//writeInternal(o, outputMessage);
}
接著調(diào)用AbstaractHttpMessageConverter
中的write()寫入響應(yīng)頭和消息內(nèi)容
/**
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
* and then calls {@link #writeInternal}.
*/
@Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage =
(StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
接著調(diào)用FastJsonHttpMessageConverter的writeInternal()進行消息轉(zhuǎn)換
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
ByteArrayOutputStream outnew = new ByteArrayOutputStream();
try {
HttpHeaders headers = outputMessage.getHeaders();
//獲取全局配置的filter
SerializeFilter[] globalFilters = fastJsonConfig.getSerializeFilters();
List<SerializeFilter> allFilters = new ArrayList<SerializeFilter>(Arrays.asList(globalFilters));
boolean isJsonp = false;
//不知道為什么會有這行代碼柠贤, 但是為了保持和原來的行為一致,還是保留下來
Object value = strangeCodeForJackson(object);
if (value instanceof FastJsonContainer) {
FastJsonContainer fastJsonContainer = (FastJsonContainer) value;
PropertyPreFilters filters = fastJsonContainer.getFilters();
allFilters.addAll(filters.getFilters());
value = fastJsonContainer.getValue();
}
//revise 2017-10-23 ,
// 保持原有的MappingFastJsonValue對象的contentType不做修改 保持舊版兼容类缤。
// 但是新的JSONPObject將返回標準的contentType:application/javascript 臼勉,不對是否有function進行判斷
if (value instanceof MappingFastJsonValue) {
if(!StringUtils.isEmpty(((MappingFastJsonValue) value).getJsonpFunction())){
isJsonp = true;
}
} else if (value instanceof JSONPObject) {
isJsonp = true;
}
int len = JSON.writeJSONString(outnew, //
fastJsonConfig.getCharset(), //
value, //
fastJsonConfig.getSerializeConfig(), //
//fastJsonConfig.getSerializeFilters(), //
allFilters.toArray(new SerializeFilter[allFilters.size()]),
fastJsonConfig.getDateFormat(), //
JSON.DEFAULT_GENERATE_FEATURE, //
fastJsonConfig.getSerializerFeatures());
if (isJsonp) {
headers.setContentType(APPLICATION_JAVASCRIPT);
}
if (fastJsonConfig.isWriteContentLength()) {
headers.setContentLength(len);
}
outnew.writeTo(outputMessage.getBody());
} catch (JSONException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
} finally {
outnew.close();
}
}
我們可以觀察到這個方法中有一個JSON.writeJSONString()方法,我點進去看一看
public static final int writeJSONString(OutputStream os, //
Charset charset, //
Object object, //
SerializeConfig config, //
SerializeFilter[] filters, //
String dateFormat, //
int defaultFeatures, //
SerializerFeature... features) throws IOException {
SerializeWriter writer = new SerializeWriter(null, defaultFeatures, features);
try {
JSONSerializer serializer = new JSONSerializer(writer, config);
if (dateFormat != null && dateFormat.length() != 0) {
serializer.setDateFormat(dateFormat);
serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
}
if (filters != null) {
for (SerializeFilter filter : filters) {
serializer.addFilter(filter);
}
}
serializer.write(object);
int len = writer.writeToEx(os, charset);
return len;
} finally {
writer.close();
}
}
一看嚇一跳餐弱。臥槽宴霸,把html序列化了,GG岸裙。終于找到問題的始作俑者了猖败。
如果配置了MediaType.APPLICATION_JSON_UTF8,FastJsonHttpMessageConverter
只能處理"application/json;charset=UTF-8"的消息,"text/html;charset=UTF-8"格式的消息被StringHttpMessageConverter得到了處理,會執(zhí)行這一段代碼
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
接著調(diào)用AbstaractHttpMessageConverter
中的write()寫入響應(yīng)頭和消息內(nèi)容
@Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage =
(StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
最終調(diào)用StringHttpMessageConverter
的writeInternal寫入消息到outputMessage.getBody()中降允,輸出html片段恩闻。
@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());
}
關(guān)于HttpMessageConverter
加載順序,可以在WebMvcConfigurationSupport
看到端倪剧董。
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter<Source>());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jackson2XmlPresent) {
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(
Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build()));
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
messageConverters.add(new MappingJackson2HttpMessageConverter(
Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build()));
}
else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
}
}
我們其實是重寫configureMessageConverters()方法去配置FastJsonHttpMessageConverter,所以它是第一個幢尚。
尾言
等休息的時候,再寫Spring MVC源碼分析請求響應(yīng)流程翅楼,源碼分析RequestMappingHandlerMapping和RequestMappingHandlerAdapter尉剩。