一次Springboot返回值问题排查

346 阅读3分钟

一次Springboot返回值问题排查

问题

一个很简单的springboot的demo。一般我们写springboot的demo的时候为了测试功能是否正常,返回值一般是简单类型比如String,Long等等,这次写demo的时候想写一个稍微复杂一点的demo,因此返回值定义了一个BaseResponse的泛型,如下所示:

public class BaseResponse<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    private String code;
    private String messageInternal;
    private String message;
    private T data;

    public BaseResponse() {
        this.code = "200";
        this.messageInternal = null;
    }

    public BaseResponse(T data) {
        this.code = "200";
        this.messageInternal = null;
        this.data = data;
    }
}

在返回的时候,将生成的数据set到BaseResponse对象中:

AddressResp resp = getCodeByName(req, response);
BaseResponse baseResponse = new BaseResponse(resp);
return baseResponse;

预期的返回结果应该报告code,messageInternal等字段。但是代码执行之后的返回结果如下:

![image-20200411115051547](/Users/hujun/Library/Application Support/typora-user-images/image-20200411115051547.png)

找不到converter。

解决

  1. 既然找不到converter,那说明工程中没有配置converter,那就在工程配置一个。springboot默认使用jackson作为json格式转换器,也可以采用fastjson。fastjson配置如下:

    @Bean
    	public HttpMessageConverters fastJsonHttpMessageConverters() {
    		//1、定义一个convert转换消息的对象
    		FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
    		//2、添加fastjson的配置信息
    		FastJsonConfig fastJsonConfig = new FastJsonConfig();
    		fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
    		//3、在convert中添加配置信息
    		fastConverter.setFastJsonConfig(fastJsonConfig);
    		//4、将convert添加到converters中
    		HttpMessageConverter<?> converter = fastConverter;
    		List<MediaType> supportedMediaTypes = new ArrayList<>();
    		supportedMediaTypes.add(MediaType.APPLICATION_JSON);
    		fastConverter.setSupportedMediaTypes(supportedMediaTypes);  //需要配置支持的类型,否则会报错
    		return new HttpMessageConverters(converter);
    	}
    
  2. 配置完成之后继续执行代码,结果返回结果的body是空值。

    ![image-20200411115522022](/Users/hujun/Library/Application Support/typora-user-images/image-20200411115522022.png)

    这个结果非常奇怪,跟进代码之后发现确实set到baseresponse中的对象的值是存在的,但是返回结果确是{}。看来只能跟到源码中找答案了。

源码跟踪

  1. 跟踪源码进入InvocableHandlerMethod类的doInvoke方法,这个类主要是根据传进来的参数调用类的对象的方法,可以认为是一个代理类。

  2. 继续跟踪代码一直到HandlerMethodReturnValueHandlerComposite类的handleReturnValue方法,这个方法中有一个调用handler.handleReturnValue,看名称就这知道是处理返回值的。

  3. 继续跟踪代码,一直跟踪到如下代码段:

    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;
    				}
    			}
    		}
    

    跟踪genericConverter.write方法,这里的genericConverter实际上是FastJsonHttpMessageConverter,真正的消息处理就是在这个类中进行的。

  4. FastJsonHttpMessageConverter消息处理在write方法中进行,

    public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
    			throws IOException, HttpMessageNotWritableException {
    
    		final HttpHeaders headers = outputMessage.getHeaders();
    		addDefaultHeaders(headers, t, contentType);
    
    		if (outputMessage instanceof StreamingHttpOutputMessage) {
    			StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
    			streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
    				@Override
    				public OutputStream getBody() {
    					return outputStream;
    				}
    				@Override
    				public HttpHeaders getHeaders() {
    					return headers;
    				}
    			}));
    		}
    		else {
    			writeInternal(t, outputMessage);  //处理消息
    			outputMessage.getBody().flush();
    		}
    	}
    
  5. 最后调用到的是JSONSerializer的write方法

    public final void write(Object object) {
            if (object == null) {
                out.writeNull();
                return;
            }
    
            Class<?> clazz = object.getClass();
            ObjectSerializer writer = getObjectWriter(clazz);
    
            try {
                writer.write(this, object, null, null, 0);
            } catch (IOException e) {
                throw new JSONException(e.getMessage(), e);
            }
        }
    

    这里的writer.write方法将Java对象转换为json string。debug的时候发现这里面的writer是一个ASM构造出来的writer(ASM是一种字节码增强的技术)。FastJson怎么样通过ASM技术来实现对象的转换,还没看明白。

  6. 代码跟踪到这一步似乎无法进行下去了,突然回头发现是不是BaseResponse中没有属性的set和get方法导致的FastJson转换失败,重新回到原点加上get和set方法。再次执行代码,果然拿到了正确的返回值,猜测fastjson在做对象转换的时候需要调用对象的set/get方法来做转换,具体如何转换需要继续研究。

小结

​ 看似十分简单的问题,分析起来其实并不容易,springboot一方面给我们的web开发带来了极大的便利,另一方面封装太多,给我们分析和定位问题也带来了极大的苦难。如果要对web开发有更深的理解,还是需要自己分析研读源码,只有对springboot的内在实现机制有比较深刻的理解,才能在出现一些疑难杂症的时候知道如何排查!