spring mvc 中的数据响应和内容协商

一、数据响应

spring mvc 中存在2中响应,一个是数据响应(常用于API开发),另一种是页面响应(常用于单体项目的开发)。

JSON的响应

spring boot 中只要引入了 web 启动器,就会自动的引入json的相关功能。在接口上标注@ResponseBody即可。

返回值解析器,一共15中:

 处理返回值:

  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        //寻找返回值处理器
        HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
        if (handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        } else {
            handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }
    }

跟参数处理器一样,返回值处理器也是先判断是否支持,然后在调用返回值处理器进行处理。

 支持的返回值:

 返回值处理,利用MessageConverters进行处理:

	@Override
	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);
	}

内容协商:

  • 请求方回根据自己的实际情况,告诉服务器端,需要什么类型的数据。 
  • 服务器根据自身能力,生成请求方需要的数据
  • 根据确定的数据类型,选择对应类型的消息转换器,消息转换器的作用就是是否支持将class类型的对象转换成MediaType类型的数据。
  • 默认的消息转换器,有以下几个:

最终使用MappingJackJon2HttpMessageConverter,转换成了json中。注意一点的是MappingJackJon2HttpMessageConverter是可以支持所有的返回值类型。

二、内容协商

内容协商就是客户端和服务端根据实际情况,约定双方需要的数据格式。也可能在某种情况下,相同的接口web端需要返回json而手机端需要返回xml,这个时候就可以用到spring mvc的内容协商。

根据客户端接收能力的不同,而返回不同媒体类型的数据。

也可以开启参数方式,参数中包含format=json\xml 就可以返回不同的模式。当然也可以自定义MediaType,也就是重写 MessageConverter

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true #开启请求参数内容协商模式 

接口端会根据 Accept 参数,进行判断返回格式 

  • application/json 返回json
  • application/xml 返回xml

如果同时定义了,就会根据权重进行判断,

Accept:

text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

上面的q=0.9 就是权重,因为xml的权重0.9大于json的权重0.8,所以会有限返回xml。

内容协商的原理:

  • 判断当前响应头中是否已经有确定的媒体类型,如果存在直接使用
  • 如果不存在,获取客户端支持的内容类型,也就是获取Accept字段
    • contentNegotiationManager 内容协商管理器,默认使用基于请求头的策略
    • 之前将的format参数,就是基于参数策略
  • 遍历循环当前系统中全部的MessageConverter(默认10中+xml),看谁支持操作返回值
  • 找到支持操作返回值的Converter,把这些Converter的媒体对象收集起来
  •  匹配客户端需要的和服务器能够处理的媒体类型,进行最佳匹配
  •  用支持将对象转为最佳媒体的Converter进行转换

自定义MessageConverter

再次整理一下内容协商的流程:

  • 在方法或者类型标注@ResponseBody证明响应的是数据,spring mvc 会调用 RequestResponseBodyMethodProcessor处理
  • RequestResponseBodyMethodProcessor通过MessageConverter处理
  • 所有的MessageConverter合起来支持大部分的媒体类型操作(读、写)
  • 内容协商找到最终的MessageConverter处理返回数据

自定义MessageConverter过程:

  • 添加自定义的MessageConverter
  • spring mvc 遍历所有MessageConverter
  • 找到支持自定义类型返回的Converter

WebMvcConfigurationSupport,xml的Converter支持源码,发现系统中存在相关类,直接导入Converter:

在spring boot 配置spring mvc 相关内容,只有一个入口就是 WebMvcConfigurer,当使用者期望配置相关Converter的时候,直接向容器中注入@Bean:

@Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                //添加自定义的Converter
                converters.add(new MgMessageConverter());
            }
        };
    }

自定义Converter:

package com.example.demo.converter;

import com.example.demo.dto.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

import java.io.IOException;
import java.util.List;

/**
 * 自定义的Converter
 */
public class MgMessageConverter implements HttpMessageConverter<Person> {

    /**
     * 是否能够读取,按照自动以类型读
     * @param clazz
     * @param mediaType
     * @return
     */
    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return false;
    }

    /**
     * 是否能读
     * 是 person类型的就能读
     * @param clazz
     * @param mediaType
     * @return
     */
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return clazz.isAssignableFrom(Person.class);
    }

    /**
     * 支持媒体类型,服务要知道这个Converter能够支持哪些媒体类型
     * application/x-mg
     * @return
     */
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/x-mg");
    }

    @Override
    public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    /**
     * 数据写出
     * @param person
     * @param contentType
     * @param outputMessage
     * @throws IOException
     * @throws HttpMessageNotWritableException
     */
    @Override
    public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        String data = person.getName() + ";" + person.getSex() + ";" + person.getAge();
        //数据写到流中
        outputMessage.getBody().write(data.getBytes());
    }
}

客户端请求:

 

 跟踪源码可以看到,获取客户端的媒体类型为:

 服务器可以处理消息的Converter:

以上是使用请求头的方式,下面可以说一下使用参数方式。

因为参数协商策略只支持json和xml,所以就需要自定义策略:

自定义策略也十分的简单:

package com.example.demo.conf;

import com.example.demo.converter.MgMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
@Configuration(proxyBeanMethods = false)
public class WebMvcConfig {

    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {

            /**
             * 自定义Converter
             * @param converters
             */
            @Override
            public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                //添加自定义的Converter
                converters.add(new MgMessageConverter());
            }

            /**
             * 自定义协商管理
             * @param configurer
             */
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                //支持的媒体类型
                Map<String, MediaType> mediaTypes = new HashMap<>();
                mediaTypes.put("json", MediaType.APPLICATION_JSON);
                mediaTypes.put("xml", MediaType.APPLICATION_ATOM_XML);
                mediaTypes.put("mg", MediaType.parseMediaType("application/x-mg"));
                ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
                configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy));
            }
        };
    }

}

跟踪代码可以看到,已经从原来的2中支持到现在的3中了: 

在自定义功能的时候,小心默认功能失效,这个时候需要dubeg源码,查看哪里出现问题,解决即可。 

猜你喜欢

转载自blog.csdn.net/liming0025/article/details/120626547