SpringMVC数据响应与内容协商
一 响应json
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.5.1</version>
<scope>compile</scope>
</dependency>
2. 返回值解析器
springMVC支持图中的15种返回数据
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor
3. 返回值处理流程
1. 进行返回值处理
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
2. 查询返回值处理器
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
3. 循环便利容器中的返回值处理器,依次判断该处理支不支持对应的返回值类型
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
4. 获取到能处理该返回值的处理器
handler.supportsReturnType(returnType)
5. 使用获取到的处理器处理返回值
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
6. 将处理好的数据写出去
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
4. 数据写出流程
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
判断返回值是不是CharSequence(字符序列)类型
判断返回值是不是isResourceType(资源)类型
获取原生的HttpServerletRequest,并且从request中获取浏览器支持的返回值类型
1. HttpServletRequest request = inputMessage.getServletRequest(); 2. acceptableTypes = getAcceptableMediaTypes(request);
获取服务器支持的返回值类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
最终获取到支持的返回值类型
遍历容器中的消息转换器,使用能支持转换器将消息转换并写出
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)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); addContentDispositionHeader(inputMessage, outputMessage); if (genericConverter != null) { //将消息写出 genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } return; } }
5. 自定义消息转换器HttpMessageConverter
添加默认的消息转换器的源码 WebMvcConfigurationSupport.class
/** * Adds a set of default HttpMessageConverter instances to the given list. * Subclasses can call this method from {@link #configureMessageConverters}. * @param messageConverters the list to add the default message converters to */ protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) { messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(new StringHttpMessageConverter()); messageConverters.add(new ResourceHttpMessageConverter()); messageConverters.add(new ResourceRegionHttpMessageConverter()); if (!shouldIgnoreXml) { try { messageConverters.add(new SourceHttpMessageConverter<>()); } catch (Throwable ex) { // Ignore when no TransformerFactory implementation is available... } } messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (romePresent) { messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } if (!shouldIgnoreXml) { if (jackson2XmlPresent) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build())); } else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } } if (kotlinSerializationJsonPresent) { messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); } if (jackson2Present) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build())); } else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } else if (jsonbPresent) { messageConverters.add(new JsonbHttpMessageConverter()); } if (jackson2SmilePresent) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build())); } if (jackson2CborPresent) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build())); } }
MVC在配置消息转换器时,先从容器中获取配置的消息转换器,然后再去添加默认的消息转换器
//WebMvcAutoConfiguration.class 配置消息转换器 @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { this.messageConvertersProvider .ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters())); } //HttpMessageConverters.class //1. 从容器中获取配置的HttpMessageConverter (因此,我们可以直接配置自定义的消息转换器) public HttpMessageConverters(Collection<HttpMessageConverter<?>> additionalConverters) { this(true, additionalConverters); } //2.再去添加默认的消息转换器 public HttpMessageConverters(boolean addDefaultConverters, Collection<HttpMessageConverter<?>> converters) { List<HttpMessageConverter<?>> combined = getCombinedConverters(converters, addDefaultConverters ? getDefaultConverters() : Collections.emptyList()); combined = postProcessConverters(combined); this.converters = Collections.unmodifiableList(combined); }
自定义消息转换器
//必须实现HttpMessageConverter<T>接口,并重写其相关方法 @Configuration public class MyHttpMessageConverter implements HttpMessageConverter<MyUser> { ////根据需求去调整 @Override public boolean canRead(Class clazz, MediaType mediaType) { return false; } //根据需求去调整 @Override public boolean canWrite(Class clazz, MediaType mediaType) { if (null==mediaType){ //判断该返回类型是否是MyUser类型或两者能否相互转换(此种写法只能处理MyUser类型的返回值) return clazz.isAssignableFrom(MyUser.class); } //判断该媒体类型是否是自定义的媒体类型 当请求头的accept="application/xx-guigu" 时,即可写出 return mediaType.isCompatibleWith(MediaType.parseMediaType("application/xx-guigu")); } //根据需求去调整 @Override public List<MediaType> getSupportedMediaTypes() { List<MediaType> mediaTypes = new ArrayList<>(); MediaType mediaType = new MediaType(MediaType.parseMediaType("application/xx-guigu")); mediaTypes.add(mediaType); return mediaTypes; } @Override public MyUser read(Class<? extends MyUser> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } //根据需求去调整 @Override public void write(MyUser myUser, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { String out = myUser.getUserName()+";"+myUser.getUserAge()+";"+myUser.getPet().getPetName()+";"+myUser.getPet().getPetAge(); outputMessage.getBody().write(out.getBytes("GBK")); } ////根据需求去调整 @Override public List<MediaType> getSupportedMediaTypes(Class clazz) { List<MediaType> mediaTypes = new ArrayList<>(); MediaType mediaType = new MediaType(MediaType.parseMediaType("application/xx-guigu")); mediaTypes.add(mediaType); return mediaTypes; } }
二 内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
1. 内容协商原理
以下顺序来源AbstractMessageConverterMethodProcessor.class
获取浏览器支持的返回值类型
List<MediaType> acceptableTypes; try { acceptableTypes = getAcceptableMediaTypes(request); }
获取服务端能产生的返回值类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); protected List<MediaType> getProducibleMediaTypes( HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) { Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } 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)); } } return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); }
对比浏览器能接收的返回值类型和服务端能产生的返回值 类型 ,得出能用的媒体类型MediaType
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; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } }
最后从消息转换器中找到能写出该媒体类型的转换器,进行数据写出
if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); //循环遍历消息转换器,找到能处理该媒体类型的转换器 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)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); addContentDispositionHeader(inputMessage, outputMessage); if (genericConverter != null) { //写出消息 genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } return; } } }
2. 引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
3. 测试
引入jackson-dataformat-xml依赖后,使用浏览器访问,后台返回的数据全部变成了xml,使用postman访问,数据依然是json
1. 浏览器:
请求头携带参数
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
在内容协商流程第四步,通过权重排序,选出浏览器最终的接受媒体类型,最后得到的是application/xml类型数据
2.postman
请求头携带参数 Accept:*/*
在内容协商流程第一步,得到客户端能接受任意的返回值参数,第四步,通过权重排序,选出最终的媒体类型,最后得到的是application/json类型数据
结论:浏览器和postman测试的最终返回结果不同,是因为判断客户端接收媒体类型时不同,浏览器是xml,postman是json
4.jackson-dataformat-xml
为什么没有引入jackson-dataformat-xml依赖前,浏览器依然是返回的json,而引入依赖后,浏览器返回却变成了xml
引入Jackson.xml前,服务端能处理的消息类型,一共有10种,其中没有xml处理的消息转换器
引入jackson.xml后,多出了jackson2xml的消息转换器
因此,引入jackson.xml后,获取到的媒体类型有支持xml的,浏览器访问时,通过权重排序会获取到xml的媒体类型,最终使用jackson2xml的消息转换器
5.开启浏览器参数方式内容协商功能
spring:
mvc:
contentnegotiation:
favor-parameter: true
parameter-name: guigu #自定义参数名
自定义内容协商参数值:
内容协商功能由ContentNegotiationManager.class管理,通过ContentNegotiationConfigurer进行配置
默认只有两种参数值xml和json(WebMvcConfigurationSupport.class)
ContentNegotiationConfigurer中有mediaType方法可以添加mediaType,通过WebMvcConfigurer实现自定义mediaType参数名称和对应的解析值
public ContentNegotiationConfigurer mediaType(String extension, MediaType mediaType) {
this.mediaTypes.put(extension, mediaType);
return this;
}
//配置内容协商,添加媒体自定义媒体类型
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer(){
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
MediaType mediaType = new MediaType(MediaType.APPLICATION_JSON);
configurer.mediaType("test",mediaType);
}
};
}
- 本文标签: Java Spring Boot SpringMVC
- 本文链接: https://www.tianyajuanke.top/article/38
- 版权声明: 本文由吴沛芙原创发布,转载请遵循《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权