ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

SpringBoot集成SpringMVC之返回值处理

2022-05-16 16:00:23  阅读:202  来源: 互联网

标签:body return SpringBoot SpringMVC outputMessage returnType 返回值 null public


返回值处理

目录

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 注解的。

      1. 利用 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的时候,才会使用这个类型来进行转换。

标签:body,return,SpringBoot,SpringMVC,outputMessage,returnType,返回值,null,public
来源: https://www.cnblogs.com/likeguang/p/16277257.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有