SpringMVC註解之@ResponseBody註解原理
一、介紹
- @ResponseBody 註解的作用是將方法的返回值通過適當的轉換器轉換為指定的格式之後,寫入到 response 物件的 body 區,通常用來返回 jsON、XML 資料。
- 使用了 @ResponseBody 註解標記的方法不再做檢視解析
二、作用範圍
- 標記在方法上
- 標記在類上
通過 @RestController 註解實現,此時所有的方法都將會被新增 @ResponseBody 註解
三、原始碼分析
具體為何呼叫了以下方法可以看我的另一篇文章。SpringMVC 執行流程解析
ServletInvocableHandlerMethod # invokeAndHandle
public void invokeAndHandle(ServletWebRequest webRequest,ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest,mavContainer,providedArgs); setResponseStatus(webRequest); if (returnValue == null) { if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { disableContentCachingIfNecessary(webRequest); mavContainer.setRequestHandled(true); return; } } else if (StringUtils.hasText(getResponseStatusReason())) { mavContainer.setRequestHandled(true); return; } mavContainer.setRequestHandled(false); Assert.state(this.returnValueHandlers != null,"No return value handlers"); try { // 處理返回值 this.returnValueHandlers.handleReturnValue( returnValue,getReturnValueType(returnValue),webRequest); } catch (Exception ex) { if (logger.isTraceEnabled()) { logger.trace(formatErrorForReturnValue(returnValue),ex)程式設計客棧; } throw ex; } }
該方法中呼叫了 handleReturnValue() 方法去處理返回值。SpringMVC 中使用 RequestResponseBodyMethodProcessor 類來處理 @ResponseBody 標記的方法
RequestResponseBodyMethodProcessor # handleReturnValue
public void handleReturnValue(@Nullable Object returnValue,MethodParameter returnType,NativeWebRequest webRequest) throws IOException,HttpMediaTypeNotAcceptableException,HttpMessageNotWritableException { // 設定請求已經被完全處理了,則後面不再做檢視解析 // 後面我還會提到的 mavContainer.setRequestHandled(true); ServletServerHttpRequest inputMessage = createInputMessage(webRequest); ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); writeWithMessageConverters(returnValue,returnType,inputMessage,outputMessage); }
該方法中通過 mavContainer.setRequestHandled(true); 設定請求已經被完全處理了,則後面不再做檢視解析。然後呼叫了 writeWithMessageConverters() 方法。
protected <T> void writeWithMessageConverters(@Nullable T value,ServletServerHttpRequest inputMessage,ServletServerHttpResponse outputMessage) throws IOException,HttpMessageNotWritableException { // 儲存響應體的資訊 Object body; // 返回值型別 Class<?> valueType; // 目標型別 Type targetType; // 返回值型別是否是 CharSequence // 是則將 返回值型別和目標型別設定為 String.class if (value instanceof CharSequence) { body = value.toString(); valueType = String.class; targetType = String.class; } // 不是 CharSequence 型別,一般是我們的自定義類 else { body = value; valueType = getReturnValueType(body,returnType); targetType = GenericTypeResolver.resolveType(getGenericType(returnType),returnType.getContainingClass()); } // 返回值型別是否是實現了 Resource 介面的資源 // 這裡我就不分析了 if (isResourceType(value,returnType)) { outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES,"bytes"); if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null && outputMessage.getServletResponse().getStatus() == 200) { Resource resource = (Resource) value; try { List<HttpRange> httpRanges = inputMessage.getHeaders().getRange(); outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value()); body = HttpRange.toResourceRegions(httpRanges,resource); valueType = body.getClass(); targetType = RESOURCE_REGION_LIST_TYPE; } catch (IllegalArgumentException ex) { outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE,"bytes */" + resource.contentLength()); outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value()); } } } // 選中的媒體型別 MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null && contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response"); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); // 可接受的媒體型別 List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); // 可產生的媒體型別 Lwww.cppcns.comist<MediaType> producibleTypes = getProducibleMediaTypes(request,valueType,targetType); if (body != null && producibleTypes.isEmpty()) { throw new HttpMessageNotWritableException( "No converter found for return value of type: " + valueType); } // 將要被使用的媒體型別 List<MediaType> mediaTypesToUse = new ArrayList<>(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType,producibleType)); } } } if (mediaTypesToUse.isEmpty()) { if (body != null) { throw new HttpMediaTypeNotAcceptableException(producibleTypes); } if (logger.isDebugEnabled()) { logger.debug("No match for " + acceptableTypes + ",supported: " + producibleTypes); } return; } MediaType.sortBySpecificityAndQuality(mediaTypesToUse); for (MediaType mediaType : mediaTypesToUse) { // 該媒體型別是否是具體的 // 也就是不包含類似於 * 這樣的萬用字元 if (mediaType.isConcrete()) { // 選中要使用的媒體型別 selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } if (logger.isDebugEnabled()) { logger.debug("Using '" + selectedMediaType + "',given " + acceptableTypes + " and supported " + producibleTypes); } } if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> converter : this.messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType,selectedMediaType) : converter.canWrite(valueType,selectedMediaType)) { body = getAdvice().beforeBodyWrite(body,selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(),outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger,traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody,!traceOn) + "]"); addContentDispositionHeader(inputMessage,outputMessage); if (genericConverter != null) { // 使用型別轉換器將請求寫入到 response body 中 genericConverter.write(body,targetType,outputMessage); } else { ((HttpMessageConverter) converter).write(body,outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } return; } } } if (body != null) { Set<MediaType> producibleMediaTypes = (Set<MediaType>) inputMessage.getServletRequest() .getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) { throw new HttpMessageNotWritableException( "No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'"); } throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); } }
該方法中通過呼叫 getAcceptableMediaTypes() 方法獲取到 acceptableTypes,getProducibleMediaTypes() 方法獲取到 producibleTypes,然後呼叫 isCompatibleWith() 方法比較 acceptableTypes 和 producibleTypes,獲取到兩者都相容的型別。最後通過呼叫 isConcrete() 獲取到一個具體使用的媒體型別。
AbstractMessageConverterMethodProcessor # getProducibleMediaTypes
protected List<MediaType> getProducibleMediaTypes( HttpServletRequest request,Class<?> valueClass,@Nullable Type targetType) { Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } else if (!this.allSupportedMediaTypes.isEmpty()) { List<MediaType> result = new ArrayList<>(); // 遍歷型別轉化器,獲取支援的媒體型別 for (HttpMessageConverter<?> converter : this.messageConverters) { if (converter instanceof GenericHttpMessageConverter && targetType != null) { if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType,valueClass,null)) { result.addAll(converter.getSupportedMediaTypes()); } } else if (converter.canWrite(valueClass,null)) { result.addAll(converter.getSupportedMediaTypes()); } } return result; } else { return Collections.singletonList(MediaType.ALL); } }
該方法中通過遍歷型別轉換器,根據型別轉換器獲取到支援的媒體型別。常見的型別轉化器有 StringHttpMessageConverter 支援轉換為 String 型別,MappingJackson2HttpMessageConverter 支援轉換為 json 型別,MappingJackson2XmlHttpMessageConverter 支援轉換為 XML 型別。
以轉換為 JSON 資料為例。我們最終選擇的媒體型別就是 “application/json” ,然後呼叫 AbstractGenericHttpMessageConverter # write 方法將資料寫入到 response body 中。
AbstractGenericHttpMessageConverter # write
public final void write(final T t,@Nullable final Type type,@Nullable MediaType contentType,HttpOutputMessage outputMessage) throws IOException,HttpMessageNotWritableException { final HttpHeaders headers = outputMessage.getHeaders(); // 新增響應頭 // 設定 Content-Type 為 application/json addDefaultHeaders(headers,t,contentType); if (outputMessage instanceof StreamingHttpOutputMessage) { StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage; streamingOutputMessage.setBody(outputStream -> writeInternal(t,type,new HttpOutputMessage() { @Override public OutputStream getBody() { return outputStream; } @Override public HttpHeaders getHeaders() { return headers; } })); } else { // 寫入資料到 response body 中 writeInternal(t,outputMessage); outputMessage.getBody().flush(); } }
該方法中設定了響應頭的 Content-Type 為 application/json,然後呼叫 writeInternal() 方法寫資料
AbstractJackson2HttpMessageConverter # writeInternal
protected void writeInternal(Object object,@Nullable Type type,HttpOutputMessage outputMessage) throws IOException,HttpMessageNotWritableException { // 獲取媒體型別為 application/json MediaType contentType = outputMessage.getHeaders().getContentType(); // 獲取 JSON 資料的編碼為 UTF-8 JsonEncoding encoding = getJsonEncoding(contentType); // 獲取到 HttpServletResponse 的輸出流物件 // 用於將資料寫入到 response body 中 OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody()); // 生成 JSON 資料的類 JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream,encoding); try { writePrefix(generator,object); Object value = object; Class<?> serializationView = null; FilterProvider filters = null; javaType javaType = null; if (object instanceof MappingJacksonValue) { MappingJacksonValue container = (MappingJacksonValue) object; value = container.getValue(); serializationView = container.getSerializationView(); filters = container.getFilters(); } if (type != null && TypeUtils.isAssignable(type,value.getClass())) { // 獲取 java 型別,一般是我們自定義的類 javaType = getJavaType(type,null); } // 用於操作可序列化物件的類 // 我們自定義的類一定要實現 Serializable 介面,並設定 get/set 方法 ObjectWriter objectWriter = (serializationView != null ? this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer()); if (filters != null) { objectWriter = objectWriter.with(filters); } if (javaType != null && javaType.isContainerType()) { objectWriter = objectWriter.forType(javaType); } SerializationConfig config = objectWriter.getConfig(); if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { objectWriter = objectWriter.with(this.ssePrettyPrinter); } // 寫入資料 objectWriter.writeValue(generator,value); writeSuffix(generator,object); generator.flush(); generator.close(); } catch (InvalidDefinitionException ex) { throw new HttpMessageConversionException("Type definition error: " + ex.getType(),ex); } catch (JsonProcessingException ex) { throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(),ex); } }
該方法中通過呼叫 outputMessage.getBody() 方法獲取到了 HttpServletResponse 的輸出流物件,用於將資料輸出到 response body 中。並設定了 JSON 資料的編碼格式為 UTF-8,然後通過 ObjectWriter 物件操作我們自定義的可序列化的物件,將該物件轉換為 JSON程式設計客棧 格式輸出到 response body 中。
AbstractJackson2HttpMessageConverter # getJsonEncoding
protected JsonEncoding getJsonEncoding(@Nullable MediaType contentType) { if (contentType != null && contentType.getCharset() != null) { Charset charset = contentType.getCharset(); JsonEncoding encoding = ENCODINGS.get(charset.name()); if (encoding != null) { return encoding; } } return JsonEncoding.UTF8; }
設定 JSON 資料的編碼格式為 UTF-8
ServletServerHttpResponse # getBody
public OutputStream getBody() throws IOException { this.bodyUsed = true; writeHeaders(); return this.servletResponse.getOutputStream(); }
獲取 HttpServletResponse 的輸出流
到這裡我們已經實現了將資料轉www.cppcns.com化為 JSON 格式輸出到 response body 中了。
還記得前面提到的 mavContainer.setRequestHandled(true) 這個方法嗎,前面我說了呼叫了這個方法後,就不再做檢視解析了,我們這裡再具體分析一下。
RequestMappingHandlerAdapter # invokeHandlerMethod
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response,HandlerMethod handlerMethod) throws Exception { ... invocableMethod.invokeAndHandle(webRequest,mavContainer); ... return getModelAndView(mavContainer,modelFactory,webRequest); ... }
可以看到 getModelAndView() 方法是在 invokeAndHandle() 方法之後呼叫了,也就是在呼叫 getModelAndView() 方法前,我們已經呼叫了 mavContainer.setRequestHandled(true) 方法了。getModelAndView() 方法就是做檢視解析的,我們來看一下該方法。
RequestMappingHandlerAdapter # getModelAndView
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,ModelFactory modelFactory,NativeWebRequest webRequest) throws Exception { modelFactory.updateModel(webRequest,mavContainer); // 是否已經完全處理了,若為 true,則直接返回 null // mavContainer.setRequestHandled(true) 已設定為 true 了 if (mavContainer.isRequestHandled()) { return null; } // 下面的程式碼是做檢視解析 ModelMap model = mavContainer.getModel(); ModelAndView mav = new ModelAndView(mavContainer.getViewName(),model,mavContainer.getStatus()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String,?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); if (request != null) { RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } } return mav; }
可以看到,該方法一開始就呼叫了 mavContainer.isRequestHandled() 方法,如果為 true,則返回 null,並進行下面的檢視解析。而 mavContainer.setRequestHandled(true) 方法已經將其設定為 true 了。這就是為什麼加了 @ResponseBody 註解的方法不做檢視解析的原因。
四、總結
- @ResponseBody 註解即可加在方法中,也可以buSYRQiPG通過 @RestController 註解加在類上
- 類上添加了 @RestController 註解等效於為該類的所有方法上新增 @ResponseBody 註解
- @ResponseBody 通過各種型別轉換器實現資料的轉換,如將資料轉換為 String、JSON、XML 等格式。並將資料寫入到 response body 中。而且它們使用的都是 UTF-8 編碼。
- 對於自定義的 Java 類轉換為 JSON 格式的資料,該類要是可序列化的。
- 使用了 @ResponseBody 註解標記的方法不再做檢視解
到此這篇關於Java原始碼解析之@ResponseBody註解原理的文章就介紹到這了,更多相關@ResponseBody註解原理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!