SpringBoot核心技术-Web开发-数据响应与内容协商

 1、响应JSON

返回值处理:将Controller中的方法在执行完成后,返回值处理器要将返回的类型转为前端需要的类型,如果方法上加了@ResponseBody,返回值类型就变成标注了@ResponseBody的JSON类型。

    @ResponseBody
    @GetMapping("/test/person")
    public Person getPerson(){
        Person person = new Person();
        person.setAge(11);
        person.setBirth(new Date());
        person.setUserName("zhangsan");
        return person;
    }

1.1、jackson.jar+@ResponseBody

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    上面的web场景自动引入了json场景
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-json</artifactId>
      <version>2.3.4.RELEASE</version>
      <scope>compile</scope>
    </dependency>

底层具体的依赖如下,框架已经帮忙引入, 自己只需要导入spring-boot-starter-web即可。

image.png

 返回值处理器:

之前的流程在请求参数解析中,返回值在invokeAndHandle

 public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
        this.setResponseStatus(webRequest);
        if (returnValue == null) {
            if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {
                this.disableContentCachingIfNecessary(webRequest);
                mavContainer.setRequestHandled(true);
                return;
            }
        } else if (StringUtils.hasText(this.getResponseStatusReason())) {
            mavContainer.setRequestHandled(true);
            return;
        }

        mavContainer.setRequestHandled(false);
        Assert.state(this.returnValueHandlers != null, "No return value handlers");

        try {
            //执行完目标方法之后,进行返回值处理
            this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
        } catch (Exception var6) {
            if (logger.isTraceEnabled()) {
                logger.trace(this.formatErrorForReturnValue(returnValue), var6);
            }

            throw var6;
        }
    }

this.returnValueHandlers.handleReturnValue();中进行返回值处理。

returnValueHandlers是所以返回值处理器的集合,下图是所有的返回值处理器。

image.png

handleReturnValue方法的内部实现:

HandlerMethodReturnValueHandlerComposite类
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);
        }
    }

 首先选择好对应的返回值处理器,再调用handler.handleReturnValue进入到下面方法:

//以处理@ResponseBody返回值类型的RequestResponseBodyMethodProcessor类为例	
@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);
	}

返回值解析器原理

image.png

  1. selectHandler中,返回值处理器判断是否支持这种类型返回值 supportsReturnType
  2. 返回值处理器调用 handleReturnValue 进行处理
  3. RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的
    1. 利用MessageConverters将数据写为JSON
      1. 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受的内容类型)

      2. 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,

      3. SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?

        1. 得到MappingJackson2HttpMessageConverter可以将对象写为json(返回值处理,将标注@ResponseBody的自定义类型,如Person转为JSON)

        2. 利用MappingJackson2HttpMessageConverter将对象转为json再写出去(内容协商完成发给前端要的json格式)。

1.2、SpringMVC支持哪些返回值

ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;

1.3、HTTPMessageConverter原理

1.3.1 MessageConverter规范

image.png

HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。

1.3.2 默认的MessageConverter

image.png

0 - 只支持Byte类型的

1 - String

2 - String

3 - Resource

4 - ResourceRegion

5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class

6 - MultiValueMap

7 - true

8 - true

9 - 支持注解方式xml处理的。

最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)

image.png

2、内容协商(writeWithMessageConverters的具体实现)

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

总体流程:

1、根据返回值类型,找到能够处理此类型的返回值处理器

2、返回值处理器中有converters,找到能够处理返回值类型的converters

3、查看客户端接受哪些媒体类型,查看上一步的converters可以将返回值转化为哪些媒体类型。

4、双重循环,得到最终确定的媒体类型。

2.1 引入xml依赖

<dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);

if (jackson2XmlPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
		}

导入了jackson处理xml的包,xml的converter就会自动进来 

2.2 postman分别测试返回json和xml

只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。

image.png

2.3 开启浏览器参数方式内容协商功能

一般内容协商是写在请求头的Accept里的,为了方便内容协商,开启基于请求参数的内容协商功能。

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true

localhost:8080/test/person?format=xml

localhost:8080/test/person?format=json

内容协商管理器就变成了基于参数策略基于请求头策略,优先遍历第一个策略

image.png

 确定客户端接受什么样的内容:

1、Parameter策略优先确定要返回数据类型(获取请求参数中的format的值)

image.png

2、 解析出了媒体类型,直接返回如果是*/*模糊匹配,将继续匹配类型。最终确定了客户端可以接受的类型,进行内容协商返回给客户端即可。

2.4 内容协商原理

1、判断当前响应头是否已经有确定的媒体类型。MediaType

2、获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】,contentNegotiationManager内容协商管理器,默认使用基于请求头的策略(HeaderContentNegotiationStrategy)最终知道客户端支持哪些类型。

List acceptableTypes;
try {
      acceptableTypes = this.getAcceptableMediaTypes(request);
}

=======
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
        return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}

以上图PostMan为例,此时的acceptableTypes为application/xml

3、遍历循环所有当前系统的MessageConverter,看哪些MessageConverter支持操作Controller方法的返回类型,如Person类型。(返回值处理器查找过程没写)

4、找到支持操作Person类型的converter,把converter支持转化的媒体类型统计出来

List<MediaType> producibleTypes = this.getProducibleMediaTypes(request, valueType, (Type)targetType);

5、客户端需要【application/xml】,服务端能处理的类型producibleTypes :如下

image.png

 6、进行内容协商的最佳匹配媒体类型,使用双层循环,找到服务器能实现客户端指定内容类型的类型因为客户端可能允许接受多种类型,所有会选择最佳匹配媒体类型。

List<MediaType> mediaTypesToUse = new ArrayList();
            Iterator var15 = acceptableTypes.iterator();//客户端允许接受的类型迭代器
            MediaType mediaType;
            while(var15.hasNext()) {
                mediaType = (MediaType)var15.next();
                //服务器可以返回的类型迭代器
                Iterator var17 = producibleTypes.iterator();
                while(var17.hasNext()) {
                    MediaType producibleType = (MediaType)var17.next();
                    if (mediaType.isCompatibleWith(producibleType)) {
                        mediaTypesToUse.add(this.getMostSpecificMediaType(mediaType, producibleType));
                    }
                }
            }

7、用支持 将对象转为 最佳匹配媒体类型 的converter。调用它将他转化。

if (selectedMediaType != null) {
                //在上一步的双层遍历结果中,选择一个最终确定的类型
                selectedMediaType = selectedMediaType.removeQualityValue();
                Iterator var23 = this.messageConverters.iterator();
                
                //遍历所有messageConverters,看谁可以转化此类型
                while(var23.hasNext()) {
                    converter = (HttpMessageConverter)var23.next();
                    genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
                    if (genericConverter != null) {
                        if (((GenericHttpMessageConverter)converter).canWrite((Type)targetType, valueType, selectedMediaType)) {
                            break label183;
                        }
                    } else if (converter.canWrite(valueType, selectedMediaType)) {
                        break label183;
                    }
                }
            }

总结:先看客户端可以接受哪些类型,再看服务器端哪些MessageConverter可以处理Controller中方法的返回值类型(如上述方法的Person)。根据上一步的MessageConverter结果,看他们可以转化为哪些类型。再使用双层循环,循环遍历客户端接受的类型和服务器端能处理的类型,得到最终服务器能处理的客户端接受的哪些类型。最后,找到能处理最佳匹配类型(上一步的类型集合中根据权重排序选出最佳)的MessageConverter,进行转化。

如果转化不了 会返回406错误

3、自定义MessageConverter

导入了jackson处理xml的包,xml的converter就会自动进来

WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);

if (jackson2XmlPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}

实现多协议数据兼容。json、xml、x-fang

1、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

2、Processor 处理方法返回值。通过 MessageConverter 处理

3、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

4、内容协商找到最终的 messageConverter

3.1 根据请求头内容协商,自定义MessageConverter

配置SpringMVC的功能,一个入口:在容器中放入一个WebMvcConfigurer,一般写在配置类中。

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

            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new MyMessageConverter());
            }
        }
    }

 自定义的MessageConverter:

public class MyMessageConverter implements HttpMessageConverter<Person> {
    /***
     * 是否支持把mediaType读成Person,例如标注了@RequestBody的参数,是否能将传过来的json或者xml内容转为Person类型
     * 现在只关心写逻辑不关心读
     */
    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return clazz.isAssignableFrom(Person.class);
    }
    /***
     * 服务器要统计所有MessageConverter都能写出哪些内容类型
     * 能操作自定义类型 application/x-fang
     */
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/x-fang");
    }
    @Override
    public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }
    /**
     *自定义协议数据的写出
     */
    @Override
    public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        //自定义协议数据的写出
        String data = person.getUserName() + ";" + person.getAge() + ";" + person.getBirth();
        //写出去
        OutputStream body = outputMessage.getBody();
        body.write(data.getBytes());

    }
}

返回值处理器的MessageConverts中多了一个自定义的Converter :

处理结果: 

 

 3.2 根据请求参数format的自定义内容协商

默认的请求参数内容协商管理器只有两种类型:xml和json

image.png

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        Map<String, MediaType> mediaTypes = new HashMap<>();
        mediaTypes.put("json", MediaType.APPLICATION_JSON);
        mediaTypes.put("xml", MediaType.APPLICATION_XML);
        mediaTypes.put("fang", MediaType.parseMediaType("application/x-fang"));
        ParameterContentNegotiationStrategy parameterstrategy = new ParameterContentNegotiationStrategy(mediaTypes);
        //指定支持哪些参数对应的媒体类型,替换Spring默认的策略。
        HeaderContentNegotiationStrategy headStrategy = new HeaderContentNegotiationStrategy();
        configurer.strategies(Arrays.asList(parameterstrategy,headStrategy));
    }

image.png

如果不增加服务器的请求头策略,如上图,利用请求头的内容协商策略会失效,服务器会能返回什么类型就返回什么类型。所以都要生效就必须写下面代码:

HeaderContentNegotiationStrategy headStrategy = new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(parameterstrategy,headStrategy));

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值