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), mavContainer, 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,
			ModelAndViewContainer mavContainer, 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, MethodParameter returnType,
			ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
			throws IOException, HttpMediaTypeNotAcceptableException, 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);
		// 可产生的媒体类型
		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);
			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) {
						// 使用类型转换器将请求写入到 response body 中
						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(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, type, 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 的输出流

到这里我们已经实现了将数据转化为 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 注解即可加在方法中,也可以通过 @RestController 注解加在类上
  • 类上添加了 @RestController 注解等效于为该类的所有方法上添加 @ResponseBody 注解
  • @ResponseBody 通过各种类型转换器实现数据的转换,如将数据转换为 String、JSON、XML 等格式。并将数据写入到 response body 中。而且它们使用的都是 UTF-8 编码。
  • 对于自定义的 Java 类转换为 JSON 格式的数据,该类要是可序列化的。
  • 使用了 @ResponseBody 注解标记的方法不再做视图解

到此这篇关于Java源码解析之@ResponseBody注解原理的文章就介绍到这了,更多相关@ResponseBody注解原理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 关于@ResponseBody 默认输出的误区的解答

    背景 @ResponseBody 默认情况返回的数据格式是什么?所谓默认情况 后台接口不指定 produces MediaType @Controller public class DemoController { @ResponseBody @GetMapping(value = "/demo") public DemoVO demo() { return new DemoVO("lengleng", "123456"); } } 使用百度搜索

  • 解决使用@ResponseBody后返回500错误的问题

    在springmvc+mybatis的项目中,利用mybatis分页插件mybatis-paginator进行分页查询,结果出现了500异常,后端又没有明显的报错. 原来的写法,返回Map对象,由springmvc里面的机制转为json对象,这样就会导致,在转json过程中的报错,都隐藏了,无法抛出,前端获取不到正确的数据, 最后就出现了500的异常. @RequestMapping(value = "/query") @ResponseBody public Map<Strin

  • SpringBoot使用@ResponseBody返回图片的实现

    以前使用HttpServletResponse可以通过输出流的方式来向前台输出图片.现在大部分都是使用springboot,在使用springboot之后,我们应该如何来修改代码呢? Spring Boot项目搭建配置略过,可直接从官网简历一个demo 首先写一个Controller类,包括一个方法,如下: package com.example.demo.common; import org.springframework.http.MediaType; import org.springfr

  • SpringMVC中解决@ResponseBody注解返回中文乱码问题

    昨天在做项目的时候用@ResponseBody注解,发现返回页面上的中文是乱码,解决过程也是让我很郁闷!!!特此记录一些.目前有下面几种解决方案: @RequestMapping的produces方法 第一种解决方案是使用@RequestMapping注解的produces方法.写法如下: 复制代码 代码如下: @RequestMapping(value = "testPersonalValidtor.do",produces = "application/json;char

  • spring+mybatis 通过@ResponseBody返回结果中文乱码的解决方法

    问题发生: 通过@Responsebody返回 @ResponseBody @RequestMapping(value ="/selectByFormId",method = RequestMethod.GET) public Map<String,Object> getClassName(String formId){ List<String> list =formInfoService.selectClassName(formId); Map<Stri

  • @ResponseBody 和 @RequestBody 注解的区别

    @ResponseBody 和 @RequestBody 注解的区别 1 前言 在详述 @ResponseBody 和 @RequestBody 注解之前,咱先了解一下 @RequestMapping 注解,@RequestMapping 是一个用来处理请求地址映射的注解,可用于类或方法上.用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径:用于方法上,表示在类的父路径下追加方法上注解中的地址将会访问到该方法.例如. /** * 用于类上,可以没有 */ @RequestMapping

  • spring boot @ResponseBody转换JSON 时 Date 类型处理方法【两种方法】

    spring boot @ResponseBody转换JSON 时 Date 类型处理方法[两种方法],Jackson和FastJson两种方式. spring boot @ResponseBody转换JSON 时 Date 类型处理方法 ,这里一共有两种不同解析方式(Jackson和FastJson两种方式) 第一种方式:默认的json处理是 jackson 也就是对configureMessageConverters 没做配置时 mybatis数据查询返回的时间,是一串数字,如何转化成时间.

  • Springmvc 4.x利用@ResponseBody返回Json数据的方法

    下面是官方文档对于@ResponseBody注解的解释: Mapping the response body with the @ResponseBody annotation The @ResponseBody annotation is similar to @RequestBody. This annotation can be put on a method and indicates that the return type should be written straight to

  • 详解SpringBoot中添加@ResponseBody注解会发生什么

    SpringBoot版本2.2.4.RELEASE. [1]SpringBoot接收到请求 ① springboot接收到一个请求返回json格式的列表,方法参数为JSONObject 格式,使用了注解@RequestBody 为什么这里要说明返回格式.方法参数.参数注解?因为方法参数与参数注解会影响你使用不同的参数解析器与后置处理器!通常使用WebDataBinder进行参数数据绑定结果也不同. 将要调用的目标方法如下: @ApiOperation(value="分页查询") @Re

  • springMvc注解之@ResponseBody和@RequestBody详解

    简介 springmvc对json的前后台传输做了很好封装,避免了重复编码的过程,下面来看看常用的@ResponseBody和@RequestBody注解 添加依赖 springmvc对json的处理依赖jackson <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-core-asl</artifactId> <version>1.9.1

  • 详解SpringBoot定制@ResponseBody注解返回的Json格式

     1.引言 在SpringMVC的使用中,后端与前端的交互一般是使用Json格式进行数据传输,SpringMVC的@ResponseBody注解可以很好的帮助我们进行转换,但是后端返回数据给前端往往都有约定固定的格式,这时候我们在后端返回的时候都要组拼成固定的格式,每次重复的操作非常麻烦. 2.SpringMVC对@ResponseBody的处理 SpringMVC处理@ResponseBody注解声明的Controller是使用默认的.RequestResponseBodyMethodProc

随机推荐