内容协商原理(1)

本篇内容与POJO转JSON之原理解析有重复。

将Person类转换为JSON

首先新建spring项目:demo5,添加Spring Web、Lombok依赖,自动生成的pom.xml中包含如下依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

com.exmaple.boot包下新建类bean.Person。

package com.example.boot.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person {
    private String username;
    private Integer age;
    private Date birth;
}

com.example.boot包下新建控制器类controller.Demo5Controller。

package com.example.boot.controller;

import com.example.boot.bean.Person;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;

@Controller
public class Demo5Controller {
    @GetMapping("/person")
    @ResponseBody
    public Person person(){
        Person person = new Person("张三", 1, new Date());
        return person;
    }
}

resources.static下新建静态页面index.html。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
<a href="/person">测试</a>
</body>
</html>

最后,启动应用,访问接口localhost:8080/person,返回的结果如下。
在这里插入图片描述

1. 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);

	// Try even with null return value. ResponseBodyAdvice could get involved.
	writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
2. AbstractMessageConverterMethodProcessor#writeWithMessageConverters
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
		ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
		throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
		//...
		MediaType selectedMediaType = null;
		MediaType contentType = outputMessage.getHeaders().getContentType();
		HttpServletRequest request = inputMessage.getServletRequest();
		//...
		List<MediaType> acceptableTypes;
		try {
			acceptableTypes = getAcceptableMediaTypes(request);
		}
		List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
		List<MediaType> mediaTypesToUse = new ArrayList<>();
		for (MediaType requestedType : acceptableTypes) {
			for (MediaType producibleType : producibleTypes) {
				if (requestedType.isCompatibleWith(producibleType)) {
					mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
				}
			}
		}
		MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
		for (MediaType mediaType : mediaTypesToUse) {
			if (mediaType.isConcrete()) {
				selectedMediaType = mediaType;
				break;
			}
		}
		if (selectedMediaType != null) {
			selectedMediaType = selectedMediaType.removeQualityValue();
			for (HttpMessageConverter<?> converter : this.messageConverters) {
				if (genericConverter != null ?
						((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
						converter.canWrite(valueType, selectedMediaType)) {
							genericConverter.write(body, targetType, selectedMediaType, outputMessage);
						}
			}
		}

}
  • MediaType contentType = outputMessage.getHeaders().getContentType(),判断当前响应是否已有确定的媒体类型。
  • acceptableTypes = getAcceptableMediaTypes(request),读取浏览器请求字段Accept,从而获取浏览器支持接受哪些内容类型。
//AbstractMessageConverterMethodProcessor#getAcceptableMediaTypes
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
		throws HttpMediaTypeNotAcceptableException {

	return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}

//ContentNegotiationManager#resolveMediaTypes
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#resolveMediaTypes
public List<MediaType> resolveMediaTypes(NativeWebRequest request)
		throws HttpMediaTypeNotAcceptableException {

	String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
	//...
}
  • List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType),统计出服务器能够生产的内容类型。
    在getProducibleMediaTypes这个方法中,将通过for (HttpMessageConverter<?> converter : this.messageConverters)遍历当前系统所有的消息转换器,找到能够操作Person类型的消息转换器,然后通过result.addAll(converter.getSupportedMediaTypes(valueClass))将该消息转换器支持的所有媒体类型统计出来。
//oAbstractMessageConverterMethodProcessor#getProducibleMediaTypes
protected List<MediaType> getProducibleMediaTypes(
			HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
			
		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));
			}
		}
		//...
}
  • 关于消息转换器

当前系统有10个消息转换器,如下。
在这里插入图片描述
这些消息转换器全部实现了接口类HttpMessageConverter,HttpMessageConverter定义如下,包含5个方法。

public interface HttpMessageConverter<T> {
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
	List<MediaType> getSupportedMediaTypes();
	default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
		return (canRead(clazz, null) || canWrite(clazz, null) ?
				getSupportedMediaTypes() : Collections.emptyList());
	}
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;
}

在这里插入图片描述
下面对以上10个消息转换器进行简单介绍。

//第一个消息转换器:   ByteArrayHttpMessageConverter,仅支持操作byte[]类型。
public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<byte[]> {
	@Override
	public boolean supports(Class<?> clazz) {
		return byte[].class == clazz;
	}
}

//第二个消息转换器:StringHttpMessageConverter,仅支持操作String类型
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
	@Override
	public boolean supports(Class<?> clazz) {
		return String.class == clazz;
	}
}

//第三个消息转换器:StringHttpMessageConverter,仅支持操作String类型

//第四个消息转换器:ResourceHttpMessageConverter,仅支持操作Resource类型
public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
	@Override
	protected boolean supports(Class<?> clazz) {
		return Resource.class.isAssignableFrom(clazz);
	}
}

//第五个消息转换器:ResourceRegionHttpMessageConverter,支持操作ResourceRegion类型
public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
	public boolean canWrite(@Nullable Type type, @Nullable Class<?> clazz, @Nullable MediaType mediaType) {
		if (!(type instanceof ParameterizedType)) {
			return (type instanceof Class && ResourceRegion.class.isAssignableFrom((Class<?>) type));
		}
		//...
	}
}

//第六个消息转换器:SourceHttpMessageConverter,支持操作DOMSource、SAXSource、StAXSource、StreamSource和Source类型
public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMessageConverter<T> {
	static {
		SUPPORTED_CLASSES.add(DOMSource.class);
		SUPPORTED_CLASSES.add(SAXSource.class);
		SUPPORTED_CLASSES.add(StAXSource.class);
		SUPPORTED_CLASSES.add(StreamSource.class);
		SUPPORTED_CLASSES.add(Source.class);
	}
	@Override
	public boolean supports(Class<?> clazz) {
		return SUPPORTED_CLASSES.contains(clazz);
	}
}

//第七个消息转换器:AllEncompassingFormHttpMessageConverter 
public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {}
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
	@Override
	public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
		if (!MultiValueMap.class.isAssignableFrom(clazz)) {
			return false;
		}
		//...
	}
}

//第八个消息转换器:MappingJackson2HttpMessageConverter 
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {}
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
	@Override
	public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
		if (!canWrite(mediaType)) {
			return false;
		}
	}
}
public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHttpMessageConverter<T>
		implements GenericHttpMessageConverter<T> {
	@Override
	public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
		return canWrite(clazz, mediaType);
	}

}

//第九个消息转换器:MappingJackson2HttpMessageConverter 

//第十个消息转换器:Jaxb2RootElementHttpMessageConverter 
public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter<Object> {}
  • 经过以上步骤后,已经知晓:浏览器端能够接受哪些内容类型,服务器能够生产哪些内容类型。接下来通过内容协商(嵌套for循环、排序、去重)找到最佳匹配的媒体类型。
//嵌套for循环
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
	for (MediaType producibleType : producibleTypes) {
		if (requestedType.isCompatibleWith(producibleType)) {
			mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
		}
	}
}

//排序
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

//去重
for (MediaType mediaType : mediaTypesToUse) {
	if (mediaType.isConcrete()) {
		selectedMediaType = mediaType;
		break;
	}
}
  • 确定了最终的媒体类型后,接下来就是使用 支持将Person类型转换为指定媒体类型 的消息转换器进行转换,并将转换后的结果作为响应返回给浏览器。
3. 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();
	addDefaultHeaders(headers, t, contentType);

	writeInternal(t, type, outputMessage);
	outputMessage.getBody().flush();
}
4. AbstractJackson2HttpMessageConverter#writeInternal
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
		throws IOException, HttpMessageNotWritableException {
		ObjectWriter objectWriter = (serializationView != null ?
				objectMapper.writerWithView(serializationView) : objectMapper.writer());
		objectWriter.writeValue(generator, value);
		writeSuffix(generator, object);
		generator.flush();
}

将Person类转换为XML

pom.xml中添加xml依赖,如下。

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.13.1</version>
</dependency>

启动应用,访问接口:localhost:8080/person,结果如下。
在这里插入图片描述
调试一下,可以看到,当前系统中的消息转换器从刚刚的10个,增加到了11个。新增的消息转换器MappingJackson2XmlHttpMessageConverter可以将实体类转换为XML。

返回json和xml

在demo5根目录下新建目录http,并在http目录下新建文件demo5.http,内容如下,

GET localhost:8080/person
Accept: application/json
###

GET localhost:8080/person
Accept: application/xml
###

启动应用,访问接口。

  • Accept: application/json 时,返回结果为
GET http://localhost:8080/person

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 06 Jan 2022 08:34:43 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "username": "张三",
  "age": 1,
  "birth": "2022-01-06T08:34:43.520+00:00"
}
  • Accept: application/xml 时,返回结果为
GET http://localhost:8080/person

HTTP/1.1 200 
Content-Type: application/xml;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 06 Jan 2022 08:31:58 GMT
Keep-Alive: timeout=60
Connection: keep-alive

<Person>
    <username>张三</username>
    <age>1</age>
    <birth>2022-01-06T08:31:57.987+00:00</birth>
</Person>

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值