SpringBoot整合SpringMVC之返回值處理
返回值處理
目錄1、前提
在迴圈處理完成每個引數的賦值之後,開始來執行controller中的方法,呼叫完成之後,會有對應的返回值。如果沒有的話,另當別論了。
那麼下面討論下有返回值的情況。這裡又分為了兩種情況:1、跳轉頁面;2、響應資料,而這裡只來研究響應資料的情況,只針對於前後端專案分離的情況。
在SpringBoot專案中使用SpringMVC的時候,在web場景啟動器中,自動引入了對應的jacson的依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> web場景自動引入了json場景 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> <version>2.3.4.RELEASE</version> <scope>compile</scope> </dependency>
2、原理
-
1、返回值處理器判斷是否支援這種型別返回值 supportsReturnType
-
2、返回值處理器呼叫 handleReturnValue 進行處理
-
3、RequestResponseBodyMethodProcessor 可以處理返回值標了@ResponseBody 註解的。
-
-
- 利用 MessageConverters 進行處理 將資料寫為json
-
-
-
- 1、內容協商(瀏覽器預設會以請求頭的方式告訴伺服器他能接受什麼樣的內容型別)
- 2、伺服器最終根據自己自身的能力,決定伺服器能生產出什麼樣內容型別的資料,
- 3、SpringMVC會挨個遍歷所有容器底層的 HttpMessageConverter ,看誰能處理?
-
-
-
-
- 1、得到MappingJackson2HttpMessageConverter可以將物件寫為json
- 2、利用MappingJackson2HttpMessageConverter將物件轉為json再寫出去。
-
-
那麼直接來到關鍵性的程式碼的地方:
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; } } // 返回值不為空的時候,一定要注意這裡的方法!如果responseStatusReason響應狀態原因有值,那麼這裡就直接返回了! 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), mavContainer, webRequest); } // 如果在執行我們的controller中的程式碼有任何異常,將會丟擲對應的異常資訊,這裡選擇的記錄之後,繼續向外丟擲異常。 // 那麼可以來進行捕捉到對應的異常資訊。 catch (Exception ex) { if (logger.isTraceEnabled()) { logger.trace(formatErrorForReturnValue(returnValue), ex); } throw ex; } }
呼叫當前請求的方法如下所示
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
// 通過反射來進行呼叫,然後返回返回值
return doInvoke(args);
}
看下返回值的處理具體細節:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
主體邏輯是:獲取得到方法處理器的處理器,然後來進行執行。
獲取得到返回值處理器:
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
// 判斷是否是非同步處理
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
// 不是非同步,將在這裡來進行判斷。
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
那麼有多少種返回值處理器,也可以來看下對應的操作:
那麼對於我們來說,使用最多的是RequestResponseBodyMethodProcessor返回值處理器,那麼來看下對應的支援:
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
方法上或者是類上有@ResponseBody註解的處理,來看下對應的處理過程:
public void handleReturnValue(@Nullable 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);
}
那麼看下利用對應的訊息轉換器來進行寫的操作的時候是如何來進行操作的:
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
// 如果返回值是字串型別的
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
// 獲取得到值的型別和目標型別
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
// 是否是資源型別
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;
try {
// 獲取得到客戶端(瀏覽器)能夠接收到的型別,也就是請求頭中的accept欄位中寫的值
// 媒體型別也是按照權重來進行排序的
acceptableTypes = getAcceptableMediaTypes(request);
}
catch (HttpMediaTypeNotAcceptableException ex) {
int series = outputMessage.getServletResponse().getStatus() / 100;
if (body == null || series == 4 || series == 5) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring error response content (if any). " + ex);
}
return;
}
throw ex;
}
// 伺服器端能夠產生的媒體型別。每個訊息轉換器都有其能夠產生的媒體型別
// 這裡有個重要的操作方式:預設是從請求頭中來獲取,我們還可以開啟基於請求路徑的方式來進行操作。
List<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);
// 哪種能夠來寫?之前的@RequestBody中是用來判斷哪種能夠來讀canWrite型別的
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
// 寫之前的處理邏輯,可以進行自定義操作
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
// 不為空的時候,這裡來進行操作。
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
// 訊息轉換器來呼叫對應的寫出對應的媒體型別的資料
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, 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(getSupportedMediaTypes(body.getClass()));
}
}
直接呼叫AbstractJackson2HttpMessageConverter的方法將其寫到響應體中去,也可以從響應體中來進行獲取。但是這裡不再來進行贅述。
3、內容協商管理器
什麼是內容協商管理器?在getAcceptableMediaTypes方法中:
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
throws HttpMediaTypeNotAcceptableException {
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
// 內容寫上策略,看看這個介面對應的實現的幾個類
for (ContentNegotiationStrategy strategy : this.strategies) {
// 支援媒體型別
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
// */*型別
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
// 也就是說尋找最佳匹配找到支援的就返回
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
預設使用的是基於請求頭的方式來進行處理的:HeaderContentNegotiationStrategy
但是我們可以通過路徑擴充套件的方式或者是引數寫上也是可以的,可以看到有很多種選擇,在繼承實現了ContentNegotiationStrategy介面之後,在新增到WebMVC中去的時候,需要注意:
// 如果想要自定義springmvc,那麼只需要在容器中新增一個WebMvcConfigurer型別的元件
@Configuration(proxyBeanMethods = false)
public class CustomWebMVCConfig implements WebMvcConfigurer {
// 新增一個messageconverter,但是這裡會將預設的給替代掉,所以不適用這個
/*@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}*/
/**
* 向容器中新增訊息轉換器
* 選擇擴充套件的,而不是重置的
* @param converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ResolveSelfHttpMessageConverter());
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json", MediaType.APPLICATION_JSON);
mediaTypes.put("xml", MediaType.APPLICATION_XML);
mediaTypes.put("nb-lig", MediaType.parseMediaType("application/nb-lig"));
ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
// 如果只是添加了一個,那麼將會導致基於請求頭的資料格式全部返回json,所以為了避免這種情況發生,也要支援其他的資料型別!所以把基於請求頭的也要新增進來
HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy, headerContentNegotiationStrategy));
}
基於引數的,需要在配置檔案中來進行開啟對應的配置資訊。
spring:
mvc:
contentnegotiation:
# 引數方式的內容協商原理
# 開啟基於請求引數的內容寫上,預設是從瀏覽器的請求頭中來進行獲取的。使用者無法改變,但是可以通過瀏覽器後面的引數來進行確定
# format=xxx
favor-parameter: true
從上面的一個方法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);
}
// 遍歷所有的訊息轉換器,將能夠支援轉換的新增到集合中來
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(valueClass));
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
}
也就是說將每種型別的返回值解析器遍歷之後,然後找能夠進行寫的,將所有能夠來進行寫的返回值處理器支援的型別新增到集合中去。然後通過客戶端能夠接收到的型別來進行最佳匹配。
4、自定義訊息轉換器
4.1、新增自定義訊息轉換器
/**
* 可以讀取,可以寫!如果說在引數上加上某個註解,利用訊息轉換器來進行操作
*
* @author liguang
* @date 2022/5/13 11:29
*/
public class ResolveSelfHttpMessageConverter implements HttpMessageConverter<Cat> {
/**
* 將Cat型別能夠解析成對應的媒體型別
*
* @param clazz
* @param mediaType
* @return
*/
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
/**
* 將Cat型別能夠寫成對應的媒體型別
*
* @param clazz
* @param mediaType
* @return
*/
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return Cat.class.isAssignableFrom(clazz);
}
/**
* 獲取支援的內容型別
*
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
// 以這種方式來進行書寫!
return MediaType.parseMediaTypes("application/nb-lig");
// return MediaType.parseMediaTypes("parameter-nb-lig");
}
/**
* 讀取
*
* @param clazz
* @param inputMessage
* @return
* @throws IOException
* @throws HttpMessageNotReadableException
*/
@Override
public Cat read(Class<? extends Cat> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
/**
* 寫出操作
*
* @param cat
* @param contentType
* @param outputMessage
* @throws IOException
* @throws HttpMessageNotWritableException
*/
@Override
public void write(Cat cat, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
Integer id = cat.getId();
String name = cat.getName();
String result = "響應的內容型別是:" + contentType + " 【result】 id=" + id + ";name=" + name;
// outputMessage.getBody().write(result.getBytes(StandardCharsets.UTF_8));
outputMessage.getBody().write(result.getBytes());
// outputMessage.getBody().flush();
}
}
4.2、新增到web容器
@Configuration(proxyBeanMethods = false)
public class CustomWebMVCConfig implements WebMvcConfigurer {
// 新增一個messageconverter,但是這裡會將預設的給替代掉,所以不適用這個
/*@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}*/
/**
* 向容器中新增訊息轉換器
*
* @param converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ResolveSelfHttpMessageConverter());
}
}
4.3、對映器程式碼
@Controller
public class ResponseBodyController {
@ResponseBody
@GetMapping(path = "person")
public Person person(){
Person person = new Person();
person.setId(1);
person.setSalary(100D);
person.setName("lig");
return person;
}
@ResponseBody
@GetMapping(path = "cat")
public Cat getCat(){
Cat cat = new Cat();
cat.setId(666);
cat.setName("tom");
return cat;
}
}
這裡在訪問Cat的方法的時候,會經過自定義的ResolveSelfHttpMessageConverter訊息轉換器,而訪問person的時候不會走我們自定義的訊息轉換器,因為判斷的時候判斷了對應的型別為Cat的時候,才會使用這個型別來進行轉換。