Spring常见问题解决 - Body返回体中对应值为null的不输出?

30 篇文章 3 订阅

前言

这篇文章主要针对于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.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):
在这里插入图片描述

二. 总结

  1. SpringBoot默认情况下,使用jackson来作为消息转换器。同时默认禁用了DEFAULT_VIEW_INCLUSION功能。因此null值会被输出。
  2. 我们可以添加对应的pom依赖,增加消息转换器。例如fastjson。但是fastjson默认情况下,不会输出null值。
  3. 不同的编解码器的实现可能有一些细节上的不同,例如本文的jacksonfastjson。所以要注意当依赖一个新的依赖时,是否会引起默认编解码器的改变,从而影响到一些局部行为的改变。例如null是否会输出。

本文在讲1.2.1小节的时候,其实代码里有涉及到一点,就是contentType的获取。其实它是用来判断当前的消息体应该由哪一个转换器来解析的一个重要条件。

contentType = inputMessage.getHeaders().getContentType();

不仅如此,还需要其他的条件,这里简单一笔带过:

  1. 查看请求头中是否有 ACCEPT 头,如果没有则可以使用任何类型的转换器。
  2. 查看当前针对返回类型,本文中是 User 实例,它可以采用的编码转换器类型是什么。
  3. 然后取上面两步获取的结果的交集来决定用什么转换器。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值