SpringCloud请求响应数据转换(一)
异常现象
近期做Spring Cloud项目,工程中对Controller添加ResponseBodyAdvice切面,在切片中将返回的结果封装到ResultMessage(自定义结构),但在Controller的方法返回值为字符串,客户端支持的类型为application/json时,出现以下异常:
java.lang.ClassCastException: com.service.view.ResultMessage cannot be cast to java.lang.String
即无法将ResultMessage对象转换为String。调试发现,当返回的是String字符串类型时,则会调StringHttpMessageConverter 将数据写入响应流,添加响应头等信息。
在获取接口数据与写入响应流之间,会将切面处理后的ResultMessage对象交由StringHttpMessageConverter 写入响应流,出现将ResultMessage赋值给一个String对象,从而导致类型转换异常。
响应数据处理流程
大致流程(简化请求端)如下:
源码分析
工程中自定义ResponseBodyAdvice切面时,对声明@RestController注解的控制层接口,在返回数据的时候会对数据进行转换,转换过程中会调自定义切面对数据处理。具体进行什么转换,会以客户端支持的类型(如application/json或text/plain等)以及控制层返回数据的类型为依据。Spring底层包含几种转换器,如下:
MVC中,从控制层返回数据到写入响应流,需要通过RequestResponseBodyMethodProcessor类的handleReturnValue方法进行处理,其中会调AbstractMessageConverterMethodProcessor类中方法writeWithMessageConverters,通过消息转换器将数据写入响应流,包含3个关键步骤:
(1)转换器的确定,该类包含属性List<HttpMessageConverter<?>> messageConverters,其中包含支持的所有转换器,如上图。从前往后依次遍历所有转换器,直到找到支持返回数据类型或媒体类型的转换器。
(2)切面数据处理,调自定义ResponseBodyAdvice切面(如果存在的话),对返回数据进行处理
(3)写入响应流,通过消息转换器将数据ServletServerHttpResponse。
关键方法为writeWithMessageConverters:
/**
* Writes the given return type to the given output message.
* @param value the value to write to the output message
* @param returnType the type of the value
* @param inputMessage the input messages. Used to inspect the {@code Accept} header.
* @param outputMessage the output message to write to
* @throws IOException thrown in case of I/O errors
* @throws HttpMediaTypeNotAcceptableException thrown when the conditions indicated by {@code Accept} header on
* the request cannot be met by the message converters
*/
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object outputValue;
Class<?> valueType;
Type declaredType;
//判断控制层返回的value类型,对String进行特殊处理,其他获取对应类型valueType(如java.util.ArrayList)和声明类型declaredType(列表元素具体类型,如java.util.List<com.service.entity.PersonVO>)
if (value instanceof CharSequence) {
outputValue = value.toString();
valueType = String.class;
declaredType = String.class;
}
else {
outputValue = value;
valueType = getReturnValueType(outputValue, returnType);
declaredType = getGenericType(returnType);
} HttpServletRequest request = inputMessage.getServletRequest();
//获取浏览器支持的媒体类型,如*/*
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
//获取控制层指定的返回媒体类型,默认为*/*,如@RequestMapping(value = "/test", produces = MediaType.APPLICATION_JSON_UTF8_VALUE),表示服务响应的格式为application/json格式。
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
}
//判断浏览器支持的媒体类型是否兼容返回媒体类型
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (compatibleMediaTypes.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
} List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(mediaTypes); MediaType selectedMediaType = null;
for (MediaType mediaType : mediaTypes) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
} if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
//遍历所有Http消息转换器,如上图,(1)首先Byte和String等非GenericHttpMessageConverter转换器;
(2)MappingJackson2HttpMessageConverter转换器继承GenericHttpMessageConverter,会将对象类型转换为json(采用com.fasterxml.jackson)
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
//判断转换器是否为GenericHttpMessageConverter,其中canWrite()方法判断是否能通过该转换器将响应写入响应流,见后续代码
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
//获取切片;调切片的beforeBodyWrite方法,处理控制层方法返回值,最终outputValue为处理后的数据,如工程中返回的ResultMessage
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
//将处理后的数据写入响应流,同时添加响应头,并调该转换器的写入方法;如MappingJackson2HttpMessageConverter的writeInternal方法,会将数据写入json中,具体见后续代码
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
//处理Byte和String等类型的数据
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
} if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}
(1)确定消息转换器
canWrite()方法判断是否能通过该转换器将响应写入响应流,以控制层返回一个自定义对象为例,会调AbstractJackson2HttpMessageConverter,即将数据已json格式返回到前端,其代码如下:
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
//判断客户端是否支持返回的媒体类型
if (!canWrite(mediaType)) {
return false;
}
if (!logger.isWarnEnabled()) {
return this.objectMapper.canSerialize(clazz);
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
//判断是否可以通过ObjectMapper对clazz进行序列化
if (this.objectMapper.canSerialize(clazz, causeRef)) {
return true;
}
logWarningIfNecessary(clazz, causeRef.get());
return false;
}
其中方法参数,clazz为上文中的valueType,即控制层返回数据类型;mediaType为要写入响应流的媒体类型,可以为null,典型值为请求头Accept(the media type to write, can be null if not specified. Typically the value of an Accept header.)。
对String或Byte等类型,在对应的转换器中都重写canWrite方法,以StringHttpMessageConverter为例,代码如下:
@Override
public boolean supports(Class<?> clazz) {
return String.class == clazz;
}
(2)切面数据处理
beforeBodyWrite:RequestResponseBodyAdviceChain类的beforeBodyWrite方法,会获取到ResponseBodyAdvice子类对应的切面,并调support方法判断是否可以处理某类型数据,调beforeBodyWrite方法进行数据处理
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) { return processBody(body, returnType, contentType, converterType, request, response);
} @SuppressWarnings("unchecked")
private <T> Object processBody(Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
//获取并遍历所有与ResponseBodyAdvice匹配的切面,其中returnType包含了请求方法相关信息
for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
//调切面的supports方法,判断切面是否支持返回类型和转换类型
if (advice.supports(returnType, converterType)) {
//调切面的beforeBodyWrite方法,进行数据处理
body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
contentType, converterType, request, response);
}
}
return body;
}
@SuppressWarnings("unchecked")
private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
//获取所有切面
List<Object> availableAdvice = getAdvice(adviceType);
if (CollectionUtils.isEmpty(availableAdvice)) {
return Collections.emptyList();
}
List<A> result = new ArrayList<A>(availableAdvice.size());
//遍历所有切面,找到符合adviceType的切面
for (Object advice : availableAdvice) {
if (advice instanceof ControllerAdviceBean) {
ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
continue;
}
advice = adviceBean.resolveBean();
}
//判断adviceType 是否为advice.getClass()的父类或父接口等
if (adviceType.isAssignableFrom(advice.getClass())) {
result.add((A) advice);
}
}
return result;
}
第16和18行会调自定义ResponseBodyAdvice切面对应的方法,如下,其中还包含对异常情况的处理。
@RestControllerAdvice(annotations = RestController.class)
public class ControllerInterceptor implements ResponseBodyAdvice<Object>{
//异常情况处理
@ExceptionHandler(value = BizException.class)
public String defaultErrorHandler(HttpServletRequest req, BizException e) throws Exception {
ResultMessage rm = new ResultMessage();
ErrorMessage errorMessage = new ErrorMessage(e.getErrCode(), e.getErrMsg());
rm.setErrorMessage(errorMessage);
rm.setSuccess(false);
return JSONUtil.ObjectToString(rm);
} //数据处理
@Override
public Object beforeBodyWrite(Object object, MethodParameter methodPram, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> clazz, ServerHttpRequest request, ServerHttpResponse response) {
ResultMessage rm = new ResultMessage();
rm.setSuccess(true);
rm.setData(object); Object obj;
//处理控制层返回字符串情况,解决上文说的类型转换异常
if(object != null && object.getClass().equals(String.class)){
obj = JSONObject.fromObject(rm).toString();
}else{
obj = rm;
}
return obj;
} //确定是否支持,此处返回true
@Override
public boolean supports(MethodParameter methodPram, Class<? extends HttpMessageConverter<?>> clazz) {
return true;
}
}
其中,第23行是对控制层返回值为字符串情况的处理,防止出现类型转换异常。
另外,@RestControllerAdvice支持@ControllerAdvice and @ResponseBody,即为控制层的切面,doc的介绍如下:
A convenience annotation that is itself annotated with @ControllerAdvice and @ResponseBody.
Types that carry this annotation are treated as controller advice where @ExceptionHandler methods assume @ResponseBody semantics by default.
(3)写入响应流
write方法会将(2)中处理后的数据写入响应流,对String或Byte等类型,会调HttpMessageConverter的write方法;对对象等类型会调GenericHttpMessageConverter的write方法。
对象类型时,会调GenericHttpMessageConverter父类AbstractGenericHttpMessageConverter的write方法,如下:
/**
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
* and then calls {@link #writeInternal}.
*/
public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException { final HttpHeaders headers = outputMessage.getHeaders();
//添加默认的响应头,包括Content-Type和Content-Length
addDefaultHeaders(headers, t, contentType); if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
//非StreamingHttpOutputMessage情况下,会调该方法将数据写入响应流
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}
/**
* Add default headers to the output message.
* <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a content
* type was not provided, set if necessary the default character set, calls
* {@link #getContentLength}, and sets the corresponding headers.
* @since 4.2
*/
protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
//设置Content-Type
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentTypeToUse = getDefaultContentType(t);
}
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(t);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
}
}
headers.setContentType(contentTypeToUse);
}
}
//设置Content-Length,当t为ArrayList对象时,值为null
if (headers.getContentLength() < 0) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
}
第32行会调AbstractJackson2HttpMessageConverter的writeInternal方法。object为经切面处理后的数据,通过com.fasterxml.jackson.databind.ObjectMapper写入json。
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException { JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object); Class<?> serializationView = null;
FilterProvider filters = null;
Object value = object;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
ObjectWriter objectWriter;
if (serializationView != null) {
objectWriter = this.objectMapper.writerWithView(serializationView);
}
else if (filters != null) {
objectWriter = this.objectMapper.writer(filters);
}
else {
objectWriter = this.objectMapper.writer();
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
//通过ObjectWrite构建json数据结构
objectWriter.writeValue(generator, value); writeSuffix(generator, object);
generator.flush(); }
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex);
}
}
String或Byte等类型时,会调HttpMessageConverter的父类AbstractHttpMessageConverter的write方法,代码与上文类似,只是getContentLength和writeInternal方法不同。以String为例,会调StringHttpMessageConverter的writeInternal方法,代码如下:
//返回字符串对应的字节数长度,作为Content-Length,上文中的异常就出现在此处。
@Override
protected Long getContentLength(String str, MediaType contentType) {
Charset charset = getContentTypeCharset(contentType);
try {
return (long) str.getBytes(charset.name()).length;
}
catch (UnsupportedEncodingException ex) {
// should not occur
throw new IllegalStateException(ex);
}
} @Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
if (this.writeAcceptCharset) {
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
}
Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
//将字符串数据copy后写入输出流
StreamUtils.copy(str, charset, outputMessage.getBody());
}
StreamUtils类:
/**
* Copy the contents of the given String to the given output OutputStream.
* Leaves the stream open when done.
* @param in the String to copy from
* @param charset the Charset
* @param out the OutputStream to copy to
* @throws IOException in case of I/O errors
*/
public static void copy(String in, Charset charset, OutputStream out) throws IOException {
Assert.notNull(in, "No input String specified");
Assert.notNull(charset, "No charset specified");
Assert.notNull(out, "No OutputStream specified");
Writer writer = new OutputStreamWriter(out, charset);
writer.write(in);
writer.flush();
}
至此,控制层接口返回的数据,经过切面处理后,写入输出流中,返回给前端。
返回数据处理过程涉及的类
SpringCloud请求响应数据转换(一)的更多相关文章
- SpringCloud请求响应数据转换(二)
上篇文章记录了从后端接口返回数据经过切面和消息转换器处理后返回给前端的过程.接下来,记录从请求发出后到后端接口调用过的过程. web请求处理流程 源码分析 ApplicationFilterChain ...
- 一比一还原axios源码(二)—— 请求响应处理
上一章,我们开发了一些简单的代码,这部分代码最最核心的一个方法就是buildURL,应对了把对象处理成query参数的方方面面.虽然我们现在可以发起简单的请求了,但是第一,我们无法接收到服务器的响应, ...
- HTTP协议请求响应过程和HTTPS工作原理
HTTP协议 HTTP协议主要应用是在服务器和客户端之间,客户端接受超文本. 服务器按照一定规则,发送到客户端(一般是浏览器)的传送通信协议.与之类似的还有文件传送协议(file transfer p ...
- NetMQ(二): 请求响应模式 Request-Reply
ZeroMQ系列 之NetMQ 一:zeromq简介 二:NetMQ 请求响应模式 Request-Reply 三:NetMQ 发布订阅模式 Publisher-Subscriber 四:NetMQ ...
- 浅谈WCF的三种通信模式:请求响应模式、数据报模式和双工通讯模式
一: WCF的服务端与客户端在通信时有三种模式:请求响应模式.数据报模式和双工通讯模式. 说一下基本知识, 1.如果想要将当前接口作为wcf服务器,则一定要加上[ServiceContract] 契 ...
- BizTalk开发系列(三十)单向端口实现请求-响应
BizTalk本质上是异步的消息处理引擎.BizTalk的请求与响应模式是基于异步之上的同步消息交换.消息引擎通过消息的扩展架构链接许 多异步消息,消息的相关集关联请求与响应消息.例如,客户端发送一个 ...
- 使用新浪云(SAE)实现基于mySql和微信公众平台的关键字请求响应服务
本例是作者初次尝试微信公众平台开发之作,实现传统的关键字请求响应功能.即:用户发关键字,公众号通过关键字进行检索, 在mysql数据库中读取与关键字相关的信息,并返回给用户.本例在微信订阅号(开发者模 ...
- Fiddler-007-修改HTTP请求响应数据
前文简述了如何通过 Fiddler 修改 HTTP请求 的请求参数,详情请参阅:Fiddler-006-修改HTTP请求参数. 在进行 App 测试时,经常需要修改请求参数,以获得不同的显示效果,以查 ...
- HTTP请求&响应、POST与GET
HTTP请求&响应 既然说从入门级开始就说说Http请求包的结构.一次请求就是向目标服务器发送一串文本.什么样的文本?有下面结构的文本.HTTP请求包结构 请求包例子: POST /meme. ...
随机推荐
- PHP之ctype扩展
$str = "121"; 检查成功返回true 否则返回false //检测一个字符串是否是纯字符(a-z,A-Z) var_dump(ctype_alpha($str));// ...
- 创建list方法总结
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/sheismylife/article/details/28878593 构建一个list 注意要标记 ...
- 【剑指offer】合并两个排序的链表
一.题目: 输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则. 二.思路: 递归思想,首先判断一方为空的情况,一方为空则直接返回另一方.都不为空时,首先找到表 ...
- 让你分分钟了解Web接口测试
因为前后端架构分离技术的兴起,接口测试也越来越重要,最近一直想总结下,作为一个近三年的测试人员,接口这个词是耳濡目染的,而开发张口闭口也都是这个接口或那个接口怎么怎么样,自己遇到的bug也很多是接口问 ...
- 性能测试之nmon对linux服务器的监控
大家都知道在做性能测试的时候,需要监控服务器的资源情况,而大多数服务器是Linux系统,网上资料嘿多,这里汇总介绍下Nmon监控工具: -------------------------------- ...
- python处理图片验证码
WebDriver中实现对特定的Web区域截图方法 import pytesseract from PIL import Image image=Image.open('new.jpg') vcode ...
- iOS开发需要学习哪些内容?
看图:
- Fuzzy and fun on Air Jordan 12 Doernbecher design
Carissa Navarro keeps it warm, fuzzy and fun on her 2017 Air Jordan 12 Doernbecher design. Nike's 20 ...
- C#在splitContainer1控件和panel控件中显示窗体
现在有两个窗体 Form1 和Form2 Form1中有控件splitContainer1 和panel .控件.我们希望Form2在splitContainer1或者panel控件中显示 1:首先看 ...
- 【环境变量】删掉centos原有的openjdk并安装sun jdk
一.卸载原有openjdk rpm -qa | grep java 之后,将展示出来的全部卸载掉,我这里是5个 rpm -e --nodeps java-1.7.0-openjdk-1.7.0.111 ...