SpringBoot集成SpringMVC之返回值处理和内容协商管理器

SpringBoot集成SpringMVC之返回值处理和内容协商管理器

一、方法返回值概述

从名字上就可以看到对于controller层中的方法返回值,SpringMVC中支持HanlerMethod方法对应的返回值给客户端进行响应。

二、方法返回值解析器接口

public interface HandlerMethodReturnValueHandler {
  // 支持哪种返回值类型
  boolean supportsReturnType(MethodParameter returnType);
  // 如何处理返回值
  void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                         ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}

三、方法返回值解析器源码

直接来到org.springframework.web.servlet.DispatcherServlet#doDispatch方法中的一行代码:

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

然后直接跳到org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod

mav = invokeHandlerMethod(request, response, handlerMethod);

来到方法中的一行代码:

invocableMethod.invokeAndHandle(webRequest, mavContainer);

直接来到:org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle

源码如下所示:

	public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {
		// 确定返回值
		Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
		// 省略代码
	}

问题:确定了参数的值、知道对应的方法和对象,接下来就是直接利用反射来直接获取得到方法执行完成之后的返回值。

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
                               Object... providedArgs) throws Exception {
  // 得到方法参数值
  Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
  // 直接利用反射获取得到方法返回值
  return doInvoke(args);
}

那么接下来就是如何对返回值来进行处理?那么接下来就重新看一下对应的源代码:

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
                            Object... providedArgs) throws Exception {

  Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
  // 设置响应状态
  // 可以设置@ResponseStatus,但是不能够设置reseaon
  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), mavContainer, webRequest);
  }
  catch (Exception ex) {
    throw ex;
  }
}

所以直接来到org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite#handleReturnValue方法中看看如何处理方法返回值的。

public void handleReturnValue( 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);
}

3.1、判断是否支持返回值类型

private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
  // 是否是异步返回值!一般都不会是
  boolean isAsyncValue = isAsyncReturnValue(value, returnType);
  for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
    // 所以当前if根本不需要来看
    if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
      continue;
    }
    // 只需要来看这个返回值处理器
    if (handler.supportsReturnType(returnType)) {
      return handler;
    }
  }
  return null;
}

那么查看一下SpringMVC内置的返回值处理器。

3.2、例子

来看一个最常用的返回值解析器:返回值方法上添加了@ResponseBody注解

  @GetMapping(path = "/test")
  @ResponseBody
  public String test1(){
    return "GetMapping success";
  }

来到RequestResponseBodyMethodProcessor类中

public boolean supportsReturnType(MethodParameter returnType) {
  // 支持类上存在@ResponseBody注解 || 方法上存在@ResponseBody注解
  return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
          returnType.hasMethodAnnotation(ResponseBody.class));
}

然后看下如何处理返回值:

public void handleReturnValue(Object returnValue, MethodParameter returnType,
                              ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
  throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
  
  mavContainer.setRequestHandled(true);
  // 获取得到HttpRequest和HttpResponse
  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(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());
  }
  // 是否是资源类型
  // 直接省略掉,正常来说都不是资源类型
 
  MediaType selectedMediaType = null;
  // 客户端是否在header中是否指定需要的媒体类型
  MediaType contentType = outputMessage.getHeaders().getContentType();
  boolean isContentTypePreset = contentType != null && contentType.isConcrete();
  if (isContentTypePreset) {
    // 如果指定,使用客户端的类型
    selectedMediaType = contentType;
  }
  // 没有指定客户端想要的媒体类型
  else {
    HttpServletRequest request = inputMessage.getServletRequest();
    // 获取得到客户端能够接收的媒体类型
    List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
    // 获取得到服务端能够产生的媒体类型
    List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
	// 如果服务端不能够处理任何媒体类型,那么报错
    if (body != null && producibleTypes.isEmpty()) {
      throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
    }
    // 选择最佳匹配的能够应用的媒体类型
    // 双层for循环
    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);
      }
      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 (selectedMediaType != null) {
    selectedMediaType = selectedMediaType.removeQualityValue();
    // 消息转换器来将对象转换成能够转换的数据
    for (HttpMessageConverter<?> converter : this.messageConverters) {
      GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
                                                      (GenericHttpMessageConverter<?>) converter : null);
      // 调用当前MessageConverter的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;    
          addContentDispositionHeader(inputMessage, outputMessage);
          // 真正开始写出
          if (genericConverter != null) {            
            genericConverter.write(body, targetType, selectedMediaType, outputMessage);
          }
          else {
            ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
          }
        }
        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);
  }
}

四、内容协商管理器

4.1、概念

内容协商:浏览器

认会以请求头的方式告诉服务器浏览器能接受什么媒体类型的数据,服务器最终根据消息转换器能够产生的媒体类型,决定服务器能转换什么样媒体类型的数据

那么内容协商管理器需要从客户端接收到客户端能够接收到的媒体类型。直接来到org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#getAcceptableMediaTypes方法中:

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;
}

内容协商策略从request对象中获取得到每种内容协商管理器能够解决的媒体类型,让实现类自己来进行实现自己能够产生的媒体类型。

4.2、内容协商管理器有哪几种

在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;
}

1、HeaderContentNegotiationStrategy

从请求头中获取得到媒体类型

public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
  // 基于请求头获取得到媒体类型
  String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
  // 为空,表示支持所有
  if (headerValueArray == null) {
    return MEDIA_TYPE_ALL_LIST;
  }
  
  List<String> headerValues = Arrays.asList(headerValueArray);
  try {
    // 解析成为媒体类型!所以得按照规范书写
    List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
    // 选择最佳匹配
    MediaType.sortBySpecificityAndQuality(mediaTypes);
    return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
  }
  catch (InvalidMediaTypeException ex) {
    throw new HttpMediaTypeNotAcceptableException(
      "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
  }
}

2、ParameterContentNegotiationStrategy

从参数中获取得到媒体类型。

public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
  
  // 参数默认都会是format
  private String parameterName = "format";

  public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
    super(mediaTypes);
  }

  public void setParameterName(String parameterName) {
    Assert.notNull(parameterName, "'parameterName' is required");
    this.parameterName = parameterName;
  }

  public String getParameterName() {
    return this.parameterName;
  }
  
  // 从request请求中获取得到参数format对应的媒体类型
  protected String getMediaTypeKey(NativeWebRequest request) {
    return request.getParameter(getParameterName());
  }

}

那么来看一下其解析媒体类型的方法

public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
  return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
}

接着向下看:

public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key)
  throws HttpMediaTypeNotAcceptableException {
  // 根据key找到对应的媒体类型
  if (StringUtils.hasText(key)) {
    MediaType mediaType = lookupMediaType(key);
    if (mediaType != null) {
      // 匹配的话,添加到集合中来
      handleMatch(key, mediaType);
      return Collections.singletonList(mediaType);
    }
    mediaType = handleNoMatch(webRequest, key);
    if (mediaType != null) {
      addMapping(key, mediaType);
      // 匹配的话,添加到集合中来
      return Collections.singletonList(mediaType);
    }
  }
  // 什么都没写,返回空集合
  return Collections.emptyList();
}

3、PathExtensionContentNegotiationStrategy

基于路径扩展找到对应的媒体类型。首先来看处理媒体类型的方法

public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
  throws HttpMediaTypeNotAcceptableException {

  return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
}

PathExtensionContentNegotiationStrategy重写了getMediaTypeKey方法:

protected String getMediaTypeKey(NativeWebRequest webRequest) {
  HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
  if (request == null) {
    logger.warn("An HttpServletRequest is required to determine the media type key");
    return null;
  }
  // 从url中解析得到路径
  String path = this.urlPathHelper.getLookupPathForRequest(request);
  // 获取得到扩展符
  String extension = UriUtils.extractFileExtension(path);
  return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null);
}

只需要看下extension到底是什么即可:

	/**
	 * Extract the file extension from the given URI path.
	 * @param path the URI path (e.g. "/products/index.html")
	 * @return the extracted file extension (e.g. "html")
	 * @since 4.3.2
	 */
	@Nullable
	public static String extractFileExtension(String path) {
		int end = path.indexOf('?');
		int fragmentIndex = path.indexOf('#');
		if (fragmentIndex != -1 && (end == -1 || fragmentIndex < end)) {
			end = fragmentIndex;
		}
		if (end == -1) {
			end = path.length();
		}
		int begin = path.lastIndexOf('/', end) + 1;
		int paramIndex = path.indexOf(';', begin);
		end = (paramIndex != -1 && paramIndex < end ? paramIndex : end);
		int extIndex = path.lastIndexOf('.', end);
		if (extIndex != -1 && extIndex > begin) {
			return path.substring(extIndex + 1, end);
		}
		return null;
	}

从案例中可以看到,URL路径中的最后一个为媒体类型扩展符号。

4、FixedContentNegotiationStrategy

从类中注释上可以看到,在创建对象的时候指定什么媒体类型,返回的是设置的固定的媒体类型。

public class FixedContentNegotiationStrategy implements ContentNegotiationStrategy {

	private final List<MediaType> contentTypes;


	/**
	 * Constructor with a single default {@code MediaType}.
	 */
	public FixedContentNegotiationStrategy(MediaType contentType) {
		this(Collections.singletonList(contentType));
	}

	/**
	 * Constructor with an ordered List of default {@code MediaType}'s to return
	 * for use in applications that support a variety of content types.
	 * <p>Consider appending {@link MediaType#ALL} at the end if destinations
	 * are present which do not support any of the other default media types.
	 * @since 5.0
	 */
	public FixedContentNegotiationStrategy(List<MediaType> contentTypes) {
		Assert.notNull(contentTypes, "'contentTypes' must not be null");
		this.contentTypes = Collections.unmodifiableList(contentTypes);
	}


	/**
	 * Return the configured list of media types.
	 */
	public List<MediaType> getContentTypes() {
		return this.contentTypes;
	}


	@Override
	public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
		return this.contentTypes;
	}

}

5、OptionalPathExtensionContentNegotiationStrategy

根据类注释说明,这个类是对PathExtensionContentNegotiationStrategy的扩展。根据请求属性来进行设置。

static class OptionalPathExtensionContentNegotiationStrategy implements ContentNegotiationStrategy {

  private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class.getName() + ".SKIP";

  private final ContentNegotiationStrategy delegate;

  OptionalPathExtensionContentNegotiationStrategy(ContentNegotiationStrategy delegate) {
    this.delegate = delegate;
  }

  @Override
  public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
    throws HttpMediaTypeNotAcceptableException {
    // 根据属性判断
    Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
    if (skip != null && Boolean.parseBoolean(skip.toString())) {
      return MEDIA_TYPE_ALL_LIST;
    }
    return this.delegate.resolveMediaTypes(webRequest);
  }

}

6、AbstractMappingContentNegotiationStrategy

抽象类,可以由子类来自定义内容协商策略。

public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
  throws HttpMediaTypeNotAcceptableException {
  return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
}

留下了抽象方法交给子类来进行重写:

protected abstract String getMediaTypeKey(NativeWebRequest request);

所以返回什么类型,那么就交给子类自己来进行实现。

4.3、默认实现

默认使用的是基于请求头的方式来进行处理的:HeaderContentNegotiationStrategy

直接参考在org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Contentnegotiation中的默认配置,对应的是:

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureContentNegotiation

public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
  // mvcProperties从请求配置中绑定的
  WebMvcProperties.Contentnegotiation contentnegotiation = this.mvcProperties.getContentnegotiation();
  configurer.favorPathExtension(contentnegotiation.isFavorPathExtension());
  // 是否开启,关键是看配置
  configurer.favorParameter(contentnegotiation.isFavorParameter());
  if (contentnegotiation.getParameterName() != null) {
    configurer.parameterName(contentnegotiation.getParameterName());
  }
  Map<String, MediaType> mediaTypes = this.mvcProperties.getContentnegotiation().getMediaTypes();
  mediaTypes.forEach(configurer::mediaType);
}

而在这里默认的添加了HeaderContentNegotiationStrategy策略的!所以可以有选择性的开启内容协商管理器的使用。

所以如果想要在自定义实现媒体类型的时候,如果不基于HeaderContentNegotiationStrategy请求头策略的话,那么还需要重新添加消息转换器。

4.4、服务端支持的媒体类型

服务端能够支持哪些媒体类型,取决于所有消息转换器能够支持的媒体类型

protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass,Type targetType) {
  // 获取得到之前set到request作用域中的对象---(下面分析)
  // 如果设置能够产生的媒体类型,直接返回;
  // 如果没有设置产生的媒体类型,那么遍历消息转换器
  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);
  }
}

什么时候在reqeust作用域中设置媒体类型

因为如果在request作用域中设置了之后,接下来就不会走下面的的消息转换器循环遍历得到对应的媒体类型了,可以提升接口效应性能。

因为这里是get,那么我们只需要看到set在哪里执行的即可。所以直接来到org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping#handleMatch中的一段代码:

if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
  Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
  request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
}

对应着的是@RequestMapping(produces ={MediaType.APPLICATION_JSON_VALUE})

五、自定义实现内容协商策略

自定义媒体协商策略时,要考虑到服务端是否能够解析对应的媒体类型。

因为媒体类型也可以自定义实现,也可以复用服务端中自带的媒体数据类型。

如果是自定义的媒体类型,那么服务端是否存在对应的消息转换器能够来进行转换?需要思考两个问题:

  • 1、存在消息转换器支持的媒体类型,是否能够得到我们想要的数据格式;
  • 2、如果不存在对应的媒体类型,那么需要自己编写消息转换器来进行实现;

5.1、自定义消息转换器

需要明白的是内容协商管理器是在已经得到了返回值之后的情况下,决定要将数据以什么样子的数据格式返回到客户端。

基于这样子的一个愿景来进行实现的。

  • 需求:如果返回值类型是Cat类型,那么从请求参数中获取得到媒体类型。
    • 如果媒体类型为application/nb-lig,那么将属性进行拼接成指定格式;
    • 如果媒体类型是json,那么就返回JSON;
    • 如果是xml,就返回对应的xml类型数据;

定义消息转换器

从需求描述中,可以知道客户端接收的媒体类型不在消息转换器能够支持的范围内,所以需要自定义消息转换器来支持对应的媒体类型。

public class ResolveSelfHttpMessageConverter implements HttpMessageConverter<Cat> {
  @Override
  public boolean canRead(Class<?> clazz, MediaType mediaType) {
    return false;
  }

  @Override
  public boolean canWrite(Class<?> clazz, MediaType mediaType) {
    return Cat.class.isAssignableFrom(clazz);
  }

  @Override
  public List<MediaType> getSupportedMediaTypes() {       
    return MediaType.parseMediaTypes("application/nb-lig");
  }

  public Cat read(Class<? extends Cat> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    return null;
  }

  @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());    
  }
}

配置内容协商策略

那么就自定义一个内容协商策略。就选择一个基于请求参数的策略。

// 如果想要自定义springmvc,那么只需要在容器中添加一个WebMvcConfigurer类型的组件
@Configuration(proxyBeanMethods = false)
public class CustomWebMVCConfig implements WebMvcConfigurer {

  // 当前消息转换器能够支持的媒体类型
  @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);
    parameterContentNegotiationStrategy.setParameterName("flag");
    // 如果只是添加了一个,那么将会导致基于请求头的数据格式全部返回json,所以为了避免这种情况发生,
    // 也要支持其他的数据类型!所以把基于请求头的也要添加进来
    HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy();
    configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy, headerContentNegotiationStrategy));
  }
}  

测试代码

@RestController
@RequestMapping(value = "/hello")
public class HelloController {
  
  @GetMapping(value = "/userVO" )
  public UserVO userVO(){
    UserVO userVO = new UserVO();
    userVO.setAddress("深圳");
    userVO.setId(1);
    userVO.setName("lig");
    userVO.setSex(false);
    return userVO;
  }
}

六、总结

1、SpringMVC中支持哪些返回值类型;

2、初始化流程:RequestMappingHandlerAdapter中进行的;

3、返回值处理流程;

4、内容协商管理器作用;

5、内容协商管理器从request获取得到媒体类型;

posted @ 2022-05-16 15:55  雩娄的木子  阅读(202)  评论(0编辑  收藏  举报