SpringMVC之内容协商策略

内容协商原理

一、引言

所谓的内容协商原理,就是客户端想要发送什么样数据格式的数据,期望服务端返回什么样数据格式的数据。

常用方式:

1、通过请求头中的Content-Type字段告知服务端,本次发送给服务端的是什么类型的数据格式;

2、通过Accept格式告知服务端,服务端需要响应给客户端的数据格式。

双方做了规范,所以就有了内容协商的产生。

二、正常请求

如果是在浏览器上发送的请求,那么对于Accept字段来说,是无法选择的;但是对于一些工具来说,如Postman、APIPost等

可以在发送请求的时候选择:请求方式、携带参数、请求头、请求格式类型等等

适用于精准匹配类型,springmvc中可以让后端开发人员和前端开发人员规定好。

请求数据格式确定和返回值数据格式确定

在请求达到DispatcherServlet的时候,首先需要找到解析出来的RequestMappingInfo信息

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod

在这个方法中,会来确定返回值数据格式

request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);

因为RequestMappingInfoHandlerMapping中重写了handleMatch方法

将后端响应的返回值处理信息存储到reqeust作用域对象中,在后面做内容协商的时候会直接从request作用域中获取得到服务端产生的数据格式

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

但是对于请求的数据格式来说,这里是没有做任何处理的。因为springmvc利用内容协商管理策略可以从请求头、参数、路径中获取得到请求的数据格式信息。

三、内容协商

下面以解析@ResponseBody为例来进行分析:

可以看到获取得到客户端能够接收到的数据格式和服务端返回的数据格式

确定客户端接收数据格式

List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);

利用内容协商管理器来解析请求希望返回的数据格式类型

private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
    throws HttpMediaTypeNotAcceptableException {

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

基于内容协商策略来确定,当前的请求的数据内容格式

留给了开发者扩展方式,期望开发者给出请求的内容数据格式

看一下接口实现体系

springmvc中提供了基于参数、路径、请求头的几种方式来确定请求的数据格式,而在springmvc中,默认的是基于请求头的方式来确定请求数据内容格式。因为内容协商策略中什么都不添加,那么默认添加的是基于请求头来确定客户端请求的数据格式。

默认基于请求头确定请求数据格式

那么看一下服务端默认基于请求头来确定客户端请求的数据类型格式

可以看到这正是基于Accept请求头获取得到媒体类型,然后按照权重来进行排序,将排序好了媒体类型进行返回。

确定服务端返回数据格式

从上面可看到,可以直接从请求作用域对象中获取得到服务端返回的媒体类型,然后就直接返回。

而不需要再次遍历消息转换器来确定每个消息转换器支持的数据类型。这样子做就相当于是提高响应性能。

选择最佳匹配

得到了客户端的请求类型和服务端能够产生的数据类型,那么接下来得到最佳匹配类型,然后利用消息转换器将对象以最佳匹配格式类型写出去。

选择最佳匹配并进行排序。从这里可以看到如果服务端确定了服务端能够产生的数据类型,那么这里将会减少匹配次数。

如果没有匹配到,那么就说明,不支持该种媒体类型。

将数据写出

1、首先判断是否是GenericHttpMessageConverter;

2、然后再次判断能够将该类型的对象以指定的媒体类型写出去;

3、判断成立,就直接进行写出操作;

四、自定义内容协商

上面提到过,可以使用不同的方式来确定请求的媒体类型:参数、请求头、路径等

那么基于请求头的方式来进行确定

但是首先有一点,需要在配置文件中来进行配置一下:

spring:
  application:
    name: springboot-negotiation-mvc
  mvc:
    contentnegotiation:
      # 开启基于请求参数的内容协商策略。默认携带参数:format
      favor-parameter: true

注:这个format参数开发人员也可以自定义

顺便引入另外一种数据格式:xml

如果是基于请求头的方式,因为xml的权重高于json,所以浏览器不会响应json,而是xml。

但是现在通过基于参数的方式,可以实现自定义化操作。

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

根据上面的源码分析,如果开启了基于参数确定请求的媒体类型,那么应该首先确定基于参数的请求类型的方式

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


	/**
	 * Create an instance with the given map of file extensions and media types.
	 */
	public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
		super(mediaTypes);
	}

	// 可以自定义设置
	/**
	 * Set the name of the parameter to use to determine requested media types.
	 * <p>By default this is set to {@code "format"}.
	 */
	public void setParameterName(String parameterName) {
		Assert.notNull(parameterName, "'parameterName' is required");
		this.parameterName = parameterName;
	}

	public String getParameterName() {
		return this.parameterName;
	}
	
	// 可以获取得到请求参数携带的值
	@Override
	@Nullable
	protected String getMediaTypeKey(NativeWebRequest request) {
		return request.getParameter(getParameterName());
	}

}

而在构造函数中,看看看看对应的处理方式

	public MappingMediaTypeFileExtensionResolver(@Nullable Map<String, MediaType> mediaTypes) {
		if (mediaTypes != null) {
			Set<String> allFileExtensions = new HashSet<>(mediaTypes.size());
			mediaTypes.forEach((extension, mediaType) -> {
				String lowerCaseExtension = extension.toLowerCase(Locale.ENGLISH);
				this.mediaTypes.put(lowerCaseExtension, mediaType);
				addFileExtension(mediaType, lowerCaseExtension);
				allFileExtensions.add(lowerCaseExtension);
			});
			this.allFileExtensions.addAll(allFileExtensions);
		}
	}

无非是将能够产生的媒体类型保存起来而已,在用到的时候将存入进去的取出来而已

下面就是要来编写,消息转换器,支持将对应的媒体类型写出的操作

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

    /**
     * 这里分为了两个地方来进行使用!
     * 第一次使用:判断能不能写,这里的媒体类型为null;
     * 第二次使用:能不能写出User这种数据类型,以自定义的方式写出
     *
     * @param clazz
     * @param mediaType
     * @return
     */
    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        // 如果返回值类型是这种类型的就可以写出去
        return clazz.isAssignableFrom(User.class);
    }

    /**
     * 当前的消息转换器能够支持的媒体类型,在需要写的时候可以获取得到对应的类型
     *
     * @return
     */
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        // 支持的媒体类型
        return MediaType.parseMediaTypes("application/lg");
    }

    @Override
    public void write(User user, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        StringJoiner stringJoiner = new StringJoiner(";");
        String resultData = stringJoiner.add(user.getId().toString()).add(user.getName()).toString();
        OutputStream body = outputMessage.getBody();
        body.write(resultData.getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public User read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }


}

然后需要将该类型匹配到web容器中来:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new PersonHttpMessageConverter());
    }


    /**
     * @param configurer
     */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy();
        Map<String, MediaType> mediaTypeMap = new HashMap<>();
        mediaTypeMap.put("lg", MediaType.parseMediaType("application/lg"));
        mediaTypeMap.put("xml", MediaType.parseMediaType("application/xml"));
        mediaTypeMap.put("json", MediaType.parseMediaType("application/json"));
        ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(mediaTypeMap);
        // 开启请求头参数
        configurer.favorParameter(true);
        configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy, headerContentNegotiationStrategy));
    }
}

总结

1、首先通过@ReqeustMapping中的produces确定服务端产生类型;
2、通过内容协商策略判断客户端的请求数据内容格式;
3、得到最佳匹配媒体类型;
4、调用消息转换器找到能够将类型写出的转换器,然后将其写出;

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