/**
* 为什么要有内容协商策略?
* 因为在客户端和服务端之前请求响应的时候,客户端期望得到服务端返回指定类型的内容
* 而这个过程,是客户端和服务端之间进行协商的过程,从而可以有多种协商方式,也称为协商策略
*/
class ContentNegotiationManager {
// 进行内容协商的各种策略集合
private final List<ContentNegotiationStrategy> strategies = new ArrayList<>();
// 解析对于文件类型的后缀,因为对于文件,文件后缀名一般就是响应的类型
private final Set<MediaTypeFileExtensionResolver> resolvers = new LinkedHashSet<>();
// 给定内容协商策略
public ContentNegotiationManager(ContentNegotiationStrategy... strategies) {
this(Arrays.asList(strategies));
}
// 初始化
public ContentNegotiationManager(Collection<ContentNegotiationStrategy> strategies) {
this.strategies.addAll(strategies);
for (ContentNegotiationStrategy strategy : this.strategies) {
// 保存媒体类型文件的解析器,因为对于文件,文件后缀名一般就是响应的类型
if (strategy instanceof MediaTypeFileExtensionResolver) {
this.resolvers.add((MediaTypeFileExtensionResolver) strategy);
}
}
}
// 支持默认的协商策略为获取请求头中的支持的类型
public ContentNegotiationManager() {
this(new HeaderContentNegotiationStrategy());
}
// 使用内容协商策略进行解析客户端需要的响应类型
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;
}
// 解析文件扩展名
@Override
public List<String> resolveFileExtensions(MediaType mediaType) {
// 使用文件解析器对该媒体类型进行解析
return doResolveExtensions(resolver -> resolver.resolveFileExtensions(mediaType));
}
// 使用文件解析器对该媒体类型进行解析
private List<String> doResolveExtensions(Function<MediaTypeFileExtensionResolver, List<String>> extractor) {
List<String> result = null;
// 遍历所有的文件类型解析器
for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
// 执行解析,获取文件的扩展名
List<String> extensions = extractor.apply(resolver);
// 如果该解析器没有解析到扩展名,不处理
if (CollectionUtils.isEmpty(extensions)) {
continue;
}
// 解析到扩展名
result = (result != null ? result : new ArrayList<>(4));
// 遍历所有的扩展名
for (String extension : extensions) {
// 保存解析到的所有扩展名
if (!result.contains(extension)) {
result.add(extension);
}
}
}
return (result != null ? result : Collections.emptyList());
}
}
// 内容协商策略,还可以自定义,就是获取某个地方设置的需要的响应类型,比如请求头,请求参数,请求路径中
public class ContentNegotiationStrategy {
/**
* 基于参数的内容协商策略实现
* {@link org.springframework.web.accept.ParameterContentNegotiationStrategy}
*/
public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
// private String parameterName = "format";这就是默认的参数名称,/url?format=json,表示客户端需要json类型的数据
String key = request.getParameter(getParameterName());
// 如果存在该参数的值
if (StringUtils.hasText(key)) {
// 查询服务器端支持的所有媒体类型,是否存在客户端指定的媒体类型
MediaType mediaType = this.lookupMediaType(key);
// 如果存在
if (mediaType != null) {
// 处理匹配上的逻辑
this.handleMatch(key, mediaType);
// 返回协商的结果类型
return Collections.singletonList(mediaType);
}
// 没有找到客户端匹配的媒体类型,处理没有匹配上的逻辑,用其他方式看一下是否能返回该媒体类型
mediaType = this.handleNoMatch(webRequest, key);
// 如果其他方式匹配上了
if (mediaType != null) {
// 保存映射关系,防止下次再去找
this.addMapping(key, mediaType);
// 返回兜底的媒体类型
return Collections.singletonList(mediaType);
}
}
// 如果客户端没有指定参数format的key媒体类型,默认支持所有
return MEDIA_TYPE_ALL_LIST;
}
/**
* 基于请求头的内容协商策略实现
* {@link org.springframework.web.accept.HeaderContentNegotiationStrategy}
*/
public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
// 获取请求头ACCEPT
String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
// 如果没有请求头
if (headerValueArray == null) {
// 支持所有
return MEDIA_TYPE_ALL_LIST;
}
//
List<String> headerValues = Arrays.asList(headerValueArray);
// 将请求头的字符串解析成MediaType
List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
// 找到最合适的媒体类型,根据权重等等方式
MediaType.sortBySpecificityAndQuality(mediaTypes);
// 返回找到的媒体类型
return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
}
}
// 这是一个处理返回值的处理器,内部调用所有的消息转换器,将内容进行响应
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {
// 内容协商管理器,管理了多个内容协商策略
private final ContentNegotiationManager contentNegotiationManager;
// 使用消息处理器对消息进行响应,我们只保留内容协商的核心源码
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) {
// 最终需要响应给客户端的数据类型
MediaType selectedMediaType = null;
// 响应流中是否已经设置了响应类型
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
// 如果设置了,直接使用指定的响应类型,不需要进行协商
if (isContentTypePreset) {
selectedMediaType = contentType;
}
// 开始进行内容协商
else {
HttpServletRequest request = inputMessage.getServletRequest();
// 获取客户端需要的响应类型
List<MediaType> acceptableTypes = this.getAcceptableMediaTypes(request);
// 获取服务端需要的响应类型
List<MediaType> producibleTypes = this.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));
}
}
}
// 如果没有协商一致,抛出异常
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
return;
}
// 因为协商之后,可能存在多种符合条件的组合,我们需要选出最佳的一组响应类型
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
}
// 协商完成,得到最终的响应类型
if (selectedMediaType != null) {
// 开始遍历所有的消息转换器,将最终的结果按照指定的媒体类型响应
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)) {
// 如果可以,执行ResponseBodyAdvice接口的回调,ResponseBodyAdvice接口是响应之前对象返回值做的最后操作
// 可以改变最终的返回结果,达到最终的统一结果返回
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;
}
}
}
}
// 获取客户端能接受的内容类型
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) {
// 使用内容协商管理器,获取客户端需要的响应类型
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
// 获取服务端能响应的内容类型
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// 如果在请求域中设置当前请求服务器响应的媒体类型,因为可以在@RequestMapping中设置
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<>(mediaTypes);
}
// 如果没有设置
List<MediaType> result = new ArrayList<>();
// 遍历所有的消息转换器,获取所有可以处理指定类型的消息转换器支持的媒体类型
for (HttpMessageConverter<?> converter : this.messageConverters) {
// 如果可以响应该返回值
if (converter.canWrite(valueClass, null)) {
// 则获取该转换器支持的媒体类型
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
// 返回消息转换器中支持写出的媒体类型
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
}
}
/**
* 使用案例
*/
@Configuration
class Demo {
/**
* http://localhost:8080/luck?format=luck,这个时候,就会执行LuckHttpMessageConverter的write方法,返回luck.toString()
* {@link LuckConfig#configureMessageConverters}
*/
@GetMapping("/luck")
public Luck luck() {
return new Luck(100, "luck");
}
@Configuration
public class LuckConfig implements WebMvcConfigurer {
/**
* 启请求参数内容协商,可以通过?format=json,xxx来与服务器协商需要得到什么样的数据内容
* {@link WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureContentNegotiation(org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer)}
* 如果是SpringBoot,会有自动配置,使用spring.mvc.contentnegotiation.favor-parameter=true开启之后,就会自动添加ParameterContentNegotiationStrategy
* 否则只有默认的HeaderContentNegotiationStrategy
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 基于请求参数的内容协商策略,基于参数的内容协商支持一下这三种响应类型
Map<String, MediaType> mediaTypes = Map.of(
"json", MediaType.APPLICATION_JSON,
"xml", MediaType.APPLICATION_XML,
// 自定义的响应类型
"luck", MediaType.parseMediaType("application/luck")
);
// 基于参数的协商策略,可以自定义参数名称,默认为format
ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
// 基于请求头的内容协商策略
HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
// 配置mvc应用的所有内容协商策略,这些协商策略在请求响应的时候发生作用,客户端和服务端进行内容协商,服务器响应与客户端期望的媒体类型
configurer.strategies(Arrays.asList(paramStrategy, headerStrategy));
}
// 添加自定义的消息内容转换器,与内容协商策略可以搭配使用,完成自定义响应媒体类型的响应
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 这个一个自定义类型的案例
class LuckHttpMessageConverter implements HttpMessageConverter<Luck> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// 该消息转换器只支持写入,不支持读取,读取的操作交给其他的消息转换器处理
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Luck.class);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
// 只支持自定义的媒体类型
return Collections.singletonList(MediaType.parseMediaType("application/luck"));
}
@Override
public Luck read(Class<? extends Luck> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
// 因为当前消息转换器不可读,所以不用处理
return null;
}
@Override
public void write(Luck luck, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// 当前消息转换器可写,所以在这里处理响应
OutputStream stream = outputMessage.getBody();
stream.write(luck.toString().getBytes(StandardCharsets.UTF_8));
}
}
converters.add(new LuckHttpMessageConverter());
}
}
}
SpringBoot-SpringMVC中的ContentNegotiationStrategy内容协商策略的作用和原理
于 2024-04-20 16:07:17 首次发布