问题描述
在 SpringBoot 应用中可以使用以下几种方式来控制是否返回值为空的字段
- 在配置文件
application.yml
中配置全局自动忽略 1 2 3
| spring: jackson: default-property-inclusion: NON_NULL
|
- 在类或字段上添加注解
@JsonInclude(JsonInclude.Include.NON_NULL)
- 实现一个
Jackson2ObjectMapperBuilderCustomizer
定制器 1 2 3 4 5 6
| @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return jacksonObjectMapperBuilder -> { jacksonObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL); }; }
|
- 参考
JacksonObjectMapperConfiguration
自定义一个 ObjectMapper
,覆盖默认的定义 1 2 3 4 5 6 7 8
| @Bean @Primary @ConditionalOnMissingBean public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper mapper = builder.createXmlMapper(false).build(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; }
|
这 4 种方式要么是全局的要么是局部的,它们都是一次性的,设置后就不能改变,每次请求都会返回相同的结果。现在我们要实现根据请求参数中是否包含某个值动态的设置是否返回值为空的字段,具体的说我们要根据请求头中是否包含 X-Include-Non-Null
来控制是否返回值为空的字段。
自定义 MappingJackson2HttpMessageConverter
首先我们需要实现一个拦截器,把请求中的 X-Include-Non-Null
放入响应头或者线程本地变量中,方便在后面获取这个值
1 2 3 4 5 6 7
| public class NullValueSerializationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { response.setHeader("X-Include-Non-Null", request.getHeader("X-Include-Non-Null")); return true; } }
|
然后把它加入拦截器集合中使它生效
1 2 3 4 5 6 7
| @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new NullValueSerializationInterceptor()); } }
|
接下来我们要自定义两个 ObjectMapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@Bean @Primary public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper mapper = builder.createXmlMapper(false).build(); return mapper; }
@Bean public ObjectMapper customObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper mapper = builder.createXmlMapper(false).build(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper; }
|
现在我们需要实现自己的 MappingJackson2HttpMessageConverter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.PrettyPrinter; import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.ser.FilterProvider; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.util.TypeUtils;
import java.io.IOException; import java.lang.reflect.Type;
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { private final ObjectMapper customObjectMapper;
private final PrettyPrinter ssePrettyPrinter;
public CustomMappingJackson2HttpMessageConverter(ObjectMapper jacksonObjectMapper, ObjectMapper customObjectMapper) { super(jacksonObjectMapper); this.customObjectMapper = customObjectMapper;
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:")); this.ssePrettyPrinter = prettyPrinter; }
@Override protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { MediaType contentType = outputMessage.getHeaders().getContentType(); JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator; if (includeNonNull(outputMessage)) { generator = getJacksonObjectMapper().getFactory().createGenerator(outputMessage.getBody(), encoding); } else { generator = getCustomObjectMapper().getFactory().createGenerator(outputMessage.getBody(), encoding); }
try { writePrefix(generator, object);
Object value = object; Class<?> serializationView = null; FilterProvider filters = null; JavaType javaType = null;
if (object instanceof MappingJacksonValue) { MappingJacksonValue container = (MappingJacksonValue) object; value = container.getValue(); serializationView = container.getSerializationView(); filters = container.getFilters(); } if (type != null && TypeUtils.isAssignable(type, value.getClass())) { javaType = getJavaType(type, null); }
ObjectWriter objectWriter; if (includeNonNull(outputMessage)) { objectWriter = (serializationView != null ? getJacksonObjectMapper().writerWithView(serializationView) : getJacksonObjectMapper().writer()); } else { objectWriter = (serializationView != null ? getCustomObjectMapper().writerWithView(serializationView) : getCustomObjectMapper().writer()); }
if (filters != null) { objectWriter = objectWriter.with(filters); } if (javaType != null && javaType.isContainerType()) { objectWriter = objectWriter.forType(javaType); } SerializationConfig config = objectWriter.getConfig(); if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { objectWriter = objectWriter.with(this.ssePrettyPrinter); } objectWriter.writeValue(generator, value);
writeSuffix(generator, object); generator.flush(); } catch (MismatchedInputException ex) { throw new HttpMessageNotWritableException("Invalid JSON input: " + ex.getOriginalMessage(), ex); } catch (InvalidDefinitionException ex) { throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex); } catch (JsonMappingException ex) { throw new HttpMessageConversionException("JSON mapping problem: " + ex.getPathReference(), ex); } catch (JsonProcessingException ex) { throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex); } }
public ObjectMapper getCustomObjectMapper() { return customObjectMapper; }
public ObjectMapper getJacksonObjectMapper() { return super.getObjectMapper(); }
public boolean includeNonNull(HttpOutputMessage outputMessage) { String includeNonNull = outputMessage.getHeaders().getFirst("X-Include-Non-Null"); return includeNonNull == null; } }
|
最后我们把它加入 Spring 容器中
1 2 3 4 5 6 7
|
@Bean public CustomMappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(@Qualifier("jacksonObjectMapper") ObjectMapper jacksonObjectMapper, @Qualifier("customObjectMapper") ObjectMapper customObjectMapper) { return new CustomMappingJackson2HttpMessageConverter(jacksonObjectMapper, customObjectMapper); }
|
自定义 ResponseBodyAdvice
到这里基本就结束了,但是我们还有另外一种办法可以解决这个问题,只需要加入一个 ResponseBodyAdvice
的实现类即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.util.ClassUtils; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Iterator; import java.util.Map;
@RestControllerAdvice public class CustomResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Autowired private ObjectMapper objectMapper;
@Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; }
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body == null || ClassUtils.isPrimitiveOrWrapper(body.getClass()) || body instanceof String) { return body; }
if (includeNonNull(request)) { return body; }
try { String content = objectMapper.writeValueAsString(body); JsonNode jsonNode = objectMapper.readTree(content); trimNull(jsonNode, 0); return jsonNode; } catch (JsonProcessingException e) { return body; } }
private void trimNull(JsonNode jsonNode, int depth) { if (jsonNode == null || depth >= 2) { return; }
if (jsonNode.isObject()) { ObjectNode objectNode = (ObjectNode) jsonNode; Iterator<Map.Entry<String, JsonNode>> iterator = objectNode.fields(); while (iterator.hasNext()) { Map.Entry<String, JsonNode> entry = iterator.next(); if (entry.getValue().isNull()) { iterator.remove(); } else { if (entry.getValue().isArray() || entry.getValue().isObject()) { trimNull(entry.getValue(), depth + 1); } } } } else if (jsonNode.isArray()) { ArrayNode arrayNode = (ArrayNode) jsonNode; for (JsonNode node : arrayNode) { trimNull(node, depth + 1); } } }
public boolean includeNonNull(ServerHttpRequest request) { String includeNonNull = request.getHeaders().getFirst("X-Include-Non-Null"); return includeNonNull != null; } }
|
这种方式改动较小,效率可能要慢一点,无伤大雅。