Spring常见问题解决 - Body返回体中对应值为null的不输出?
前言
这篇文章主要针对于Spring
开发中关于Web
返回请求体的相关问题。
一. Body返回体中对应值为null的不输出问题
1.1 案例复现
自定义一个Controller
:
@RestController
public class MyController {
@GetMapping("/hello")
public User hello() throws InterruptedException {
User user = new User();
user.setName("Ljj");
return user;
}
}
其中的User
类,有两个属性:
public class User {
private String name;
private Integer age;
// get/set
}
那么我们访问一下页面看看是咋样的:http://localhost:8080/hello
:
那么假设此时我们希望使用fastjson
来替代默认的序列化器(默认的是jackson
,后文会说),首先添加对应的pom
依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.71</version>
</dependency>
我们可以自定义一个配置类:
@Configuration
public class MyConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return new HttpMessageConverters(converter);
}
}
然后记得重启一下项目,再次访问相同的URL
,看看结果:
可见原本age
的值为null
,但是输出结果中age
就被吞了 。那么我们来进入分析阶段。
1.2 原理分析
首先我们要看下,Spring
中对于返回的User
对象,做出了怎样的序列化处理。入口在于DispatcherServlet.doDispatch()
函数中,它会对请求和返回做出对应的解析和转换。
public class DispatcherServlet extends FrameworkServlet {
// ...
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// ...
}
↓↓↓↓↓↓
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// ...
mav = invokeHandlerMethod(request, response, handlerMethod);
// ...
}
↓↓↓↓↓↓
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
invocableMethod.invokeAndHandle(webRequest, mavContainer);
}
}
↓↓↓↓↓↓
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 处理请求,这部分主要对request做解析和转换
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// ...
try {
// 处理返回结果
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
// ...
}
}
可以看到最后一步:
- 先对请求进行处理(包含请求报文的转换)。
- 然后对返回结果做处理(包含返回报文的转换)。
我们这里进行进一步的展开。
1.2.1 请求报文的转换
我们从这行代码开始:
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
↓↓↓↓↓↓
public class InvocableHandlerMethod extends HandlerMethod {
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 1.获取参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
// 2.再对参数进行对象转换操作
return doInvoke(args);
}
}
到这里,我把对应Controller
的方法改成了:
@PostMapping("/hello")
public User hello(@RequestBody User user){
return user;
}
然后用postman
去推一条数据:
{
"name":"LJJ"
}
此时我们可以看下getMethodArgumentValues()
函数返回的args
是什么:
说明了这个函数就已经将参数和实体类对象进行了转换和值的绑定过程。而下面的doInvoke()
函数则是执行对应的函数逻辑了,这部分就不探讨了。我们接着往下走,看下getMethodArgumentValues()
:
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 1.获取方法的参数对象,以本文为例,即查看hello()这个函数有什么参数
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
// 对每个参数进行解析
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 获取参数对应的解析器
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 通过解析器来解析参数,进行参数绑定
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// ..
}
}
return args;
}
首先我们可以看下第一点,本文的hello
函数,只有一个参数,其类型为User
。
这里找到对应的参数类型之后,就会去进行解析和赋值操作了:
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
↓↓↓↓↓↓↓
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
// 1.消息的转换,读取请求体,转化为对应的Java对象.这里获得的结果是User对象
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
// 2.获取参数名,这里获得的结果是 user
String name = Conventions.getVariableNameForParameter(parameter);
// ..
return adaptArgumentIfNecessary(arg, parameter);
}
}
结果如下:
我们继续跟踪该函数:
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
// 最终走到AbstractMessageConverterMethodArgumentResolver类中做转换器的匹配
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
try {
// 获取的是请求的类型,然后对应的字符集是什么,比如 :application/json;charset=UTF-8
contentType = inputMessage.getHeaders().getContentType();
}
// ...
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message;
try {
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
// 循环遍历每一种消息转换器,看哪个转换器支持这个消息的转换。一旦找到了,就直接解析,然后break循环
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
// ..
return body;
}
}
此时我们发现,在默认的情况下,SpringBoot
使用jackson
来进行消息的转换:
另外,在最终进行转换的时候,实际上是通过ObjectMapper
来读取值的:
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = getCharset(contentType);
boolean isUnicode = ENCODINGS.containsKey(charset.name());
try {
// ..
if (isUnicode) {
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
// ..
}
// ..
}
}
我们可以看下本文案例中,参数接收的时候确实是通过ObjectMapper
来完成的(后续要说):
那么这里我们可以做个总结,本文案例中,SpringBoot
在接收请求的请求体的时候,是通过jackson
来进行反序列化成Java
对象的。那么接下来就可以看下返回报文的转换流程了。
1.2.2 返回报文的转换
我们上文主要讲的是关于请求体-->Java对象
的一个转换过程。那么对于本文案例来说,我们看的是结果,也正是结果上出了问题(是否吐出null
的字段)。因此本文的重点实则在于Java对象-->返回体
的这么一个转换过程,我们回到这段代码来,然后继续往后走:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
↓↓↓↓↓↓
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@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);
// 根据返回值,创建ServletServerHttpResponse对象,
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
}
而writeWithMessageConverters
这个函数,实则跟请求报文的转换流程很相似,重点也在于循环所有的转换器,然后尝试去转换。这里不贴里面的代码了,太长了,只贴debug
过程的一个截图:
那么到这里我们知道了,本文案例中,最终程序呈现给我们用户的结果,是通过jackson
来序列化的。那么和使用fastjson
有什么区别呢?我们知道,上文提到了一个名词:ObjectMapper
,它有啥用呢?当使用 JSON
格式时,SpringBoot
将使用一个ObjectMapper
实例来序列化响应和反序列化请求。
1.2.3 为何jackson返回结果有null而fastjson没有?
对于jackson
而言,ObjectMapper
的构建在于:Jackson2ObjectMapperBuilder.json().build();
Jackson2ObjectMapperBuilder.json().build();
↓↓↓↓↓
public class Jackson2ObjectMapperBuilder {
public <T extends ObjectMapper> T build() {
ObjectMapper mapper;
// ...
configure(mapper);
return (T) mapper;
}
↓↓↓↓↓
public void configure(ObjectMapper objectMapper) {
// ..
customizeDefaultFeatures(objectMapper);
// ..
}
↓↓↓↓↓
private void customizeDefaultFeatures(ObjectMapper objectMapper) {
// 一开始都是false,不包含,因此包括下面的if分支,都会走进去
if (!this.features.containsKey(MapperFeature.DEFAULT_VIEW_INCLUSION)) {
configureFeature(objectMapper, MapperFeature.DEFAULT_VIEW_INCLUSION, false);
}
if (!this.features.containsKey(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) {
configureFeature(objectMapper, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
}
}
我们看到两个关键属性:
-
MapperFeature.DEFAULT_VIEW_INCLUSION
:是否排除未显式映射到视图的属性。默认禁用。即不排除。可以理解为没有值的属性,默认情况下会输出,比如本文案例中的age
属性,输出为null
。
-
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
:反序列化时,遇到未知属性时是否引起结果失败。即如果遇到未知属性时会抛一个JsonMappingException
。该功能默认清空下是禁用的,因为上述代码后面传了一个参数false
。
回过头我们再来看下fastjson
,我们自定义转换器的代码如下:
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return new HttpMessageConverters(converter);
}
相关的序列化配置在于SerializerFeatures
这个属性中,里面定义了各种各样的动作属性。假设,咱们啥也不设置,就new FastJsonConfig();
,我们看下它的构造:
如果你希望有别的属性,需要你自己去手动再配置。 对于null
值的显示,正好也有对应的定义:SerializerFeature.WriteMapNullValue
。但是如果不设置它, 默认就不输出了。并且我们可以看到默认的构造里面是没有设置这个值的。
1.3 问题解决
我们在自定义序列化器的时候,需要手动设置null
值需要输出:
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue);
更改后结果如下(使用fastjson
):
二. 总结
SpringBoot
默认情况下,使用jackson
来作为消息转换器。同时默认禁用了DEFAULT_VIEW_INCLUSION
功能。因此null
值会被输出。- 我们可以添加对应的
pom
依赖,增加消息转换器。例如fastjson
。但是fastjson
默认情况下,不会输出null
值。 - 不同的编解码器的实现可能有一些细节上的不同,例如本文的
jackson
和fastjson
。所以要注意当依赖一个新的依赖时,是否会引起默认编解码器的改变,从而影响到一些局部行为的改变。例如null
是否会输出。
本文在讲1.2.1小节的时候,其实代码里有涉及到一点,就是contentType
的获取。其实它是用来判断当前的消息体应该由哪一个转换器来解析的一个重要条件。
contentType = inputMessage.getHeaders().getContentType();
不仅如此,还需要其他的条件,这里简单一笔带过:
- 查看请求头中是否有
ACCEPT
头,如果没有则可以使用任何类型的转换器。 - 查看当前针对返回类型,本文中是
User
实例,它可以采用的编码转换器类型是什么。 - 然后取上面两步获取的结果的交集来决定用什么转换器。