Spring 引导:控制每个请求的序列化响应中是否存在空字段
Spring Boot: Control if null fields present in serialized response per request
简介
我们在 Spring Boot 中实现了 REST API。目前它 return 序列化时的所有字段。所以它 return 类似于
{
"foo": "A",
"bar": null,
"baz": "C",
}
我们想要不 return 空字段的选项,所以它只会 return
{
"foo": "A",
"baz": "C",
}
对于那种情况 - 但仍然(如果 bar
有一个值)
{
"foo": "A",
"bar": "B",
"baz": "C",
}
我知道你可以通过应用程序属性将它引导到不 return 空值,但这是一个现有的 AI,如果字段丢失,一些针对它实现的应用程序可能会在反序列化时失败。因此,我们想让调用客户端来引导它。我们的想法是让您可以发送 header:X-OurCompany-IncludeNulls; false
。这将允许客户进行选择,我们最初会默认为 true
,但可能会随着时间的推移以可管理的方式更改默认值。
我能找到的最近的是 this,它通过查询参数转向 pretty-printing。当我尝试做类似的事情时,它适用于 pretty-printing。但是,对于包含,它适用于我启动 API 后的第一个请求,但之后每个其他请求都从第一个请求获取值。我可以看到它正在通过断点设置它,而且我还针对同一参数添加了 pretty-print 只是为了诊断目的。
我尝试的详细信息
我们的 API 基于使用 Swagger Codegen 服务器存根生成的一个。我们使用委托模式,所以它生成一个控制器,它只有一个 auto-wired 委托和一个 getDelegate
@Controller
public class BookingsApiController implements BookingsApi {
private final BookingsApiDelegate delegate;
@org.springframework.beans.factory.annotation.Autowired
public BookingsApiController(BookingsApiDelegate delegate) {
this.delegate = delegate;
}
@Override
public BookingsApiDelegate getDelegate() {
return delegate;
}
}
委托是一个接口,每个端点都包含一个函数。这些 return CompletableFuture<ResponseEntity<T>>
(其中 T
是该响应的类型)。它也是一个 getObjectMapper()
,我假设它是 Spring 用来序列化响应的内容?
public interface BookingsApiDelegate {
Logger log = LoggerFactory.getLogger(BookingsApi.class);
default Optional<ObjectMapper> getObjectMapper() {
return Optional.empty();
}
default Optional<HttpServletRequest> getRequest() {
return Optional.empty();
}
default Optional<String> getAcceptHeader() {
return getRequest().map(r -> r.getHeader("Accept"));
}
// Functions per endpoint here. By default returns Not Implemented.
}
我们有一个 object,我们称之为 ApiContext
。这是我们称之为 ApiCallScoped
的自定义范围的范围 - 基本上是 per-request 但它处理异步并复制到创建的线程。我们已经有了实现 HandlerInterceptorAdapter
的东西(尽管我们的是 @Component
而不是上面 pretty-print 示例中的 @Bean
)。我们在 preHandle
中创建上下文,所以我想将它添加到那里以设置 object 映射器属性。撇开一些 clean-up 这看起来像:
@Component
public class RestContextInterceptor extends HandlerInterceptorAdapter {
@Autowired
private ContextService apiContextService;
@Autowired
private RestRequestLogger requestLogger;
@Autowired
private ObjectMapper mapper;
@Autowired
private Jackson2ObjectMapperBuilder objectMapperBuilder;
@Autowired
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
final Object handler) throws Exception {
requestLogger.requestStart(request);
if (request.getAttribute("apiCallContext") == null) {
ApiCallContext conversationContext;
ApiContext apiContext = readApiContext(request, mapper, objectMapperBuilder, mappingJackson2HttpMessageConverter);
if (apiContext == null) {
conversationContext = new ApiCallContext("local-" + UUID.randomUUID().toString());
} else {
conversationContext = new ApiCallContext(apiContext.getTransId());
}
ApiCallContextHolder.setContext(conversationContext);
request.setAttribute("apiCallContext", conversationContext);
if (apiContext != null) {
apiContextService.setContext(apiContext);
}
} else {
ApiCallContextHolder.setContext((ApiCallContext) request.getAttribute("apiCallContext"));
}
return true;
}
private static ApiContext readApiContext(
final HttpServletRequest request,
final ObjectMapper mapper,
final Jackson2ObjectMapperBuilder objectMapperBuilder,
final MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter) {
if (request.getHeader(ApiContext.SYSTEM_JWT_HEADER) != null) {
return new ApiContext(Optional.of(request), mapper, objectMapperBuilder, mappingJackson2HttpMessageConverter);
}
return null;
}
}
在ApiContext
我们看一下header。我试过了
public final class ApiContext implements Context {
private final ObjectMapper mapper;
public ApiContext(
final Optional<HttpServletRequest> request,
final ObjectMapper mapper,
final Jackson2ObjectMapperBuilder objectMapperBuilder,
final MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter
) {
if (!request.isPresent()) {
throw new InvalidSessionException("No request found");
}
if (getBooleanFromHeader(request, NULLMODE_HEADER).orElse(DEFAULT_INCLUDE_NULL_FIELDS_IN_OUTPUT)) {
objectMapperBuilder.serializationInclusion(JsonInclude.Include.ALWAYS);
objectMapperBuilder.indentOutput(false);
mappingJackson2HttpMessageConverter.getObjectMapper().setPropertyInclusion(
JsonInclude.Value.construct(JsonInclude.Include.ALWAYS, JsonInclude.Include.ALWAYS));
mapper.setPropertyInclusion(
JsonInclude.Value.construct(JsonInclude.Include.ALWAYS, JsonInclude.Include.ALWAYS));
} else {
objectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_EMPTY);
objectMapperBuilder.indentOutput(true);
mappingJackson2HttpMessageConverter.getObjectMapper().setPropertyInclusion(
JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY));
mapper.setPropertyInclusion(
JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY));
}
objectMapperBuilder.configure(mapper);
this.mapper = objectMapperBuilder.build().copy();
}
@Override
public ObjectMapper getMapper() {
return mapper;
}
private static Optional<Boolean> getBooleanFromHeader(final Optional<HttpServletRequest> request, final String key) {
String value = request.get().getHeader(key);
if (value == null) {
return Optional.empty();
}
value = value.trim();
if (StringUtils.isEmpty(value)) {
return Optional.empty();
}
switch (value.toLowerCase()) {
case "true":
return Optional.of(true);
case "1":
return Optional.of(true);
default:
return Optional.of(false);
}
}
}
我已经尝试通过 Jackson2ObjectMapperBuilder
和 Jackson2ObjectMapperBuilder
在注入的 ObjectMapper
上设置它。我试过(我认为)所有不同的组合。发生的情况是 prettify 部分根据请求工作,但 null-inclusion 仅在第一个请求上工作,之后它保持在该值。代码是 运行(美化工作,在调试器中经过并看到它)并且当我尝试设置包含属性但它没有使用它们时没有抛出错误。
然后我们有一个实现委托接口的 @Component
。 getObjectMapper
returns 来自我们的 ApiContext
的映射器
@Component
public class BookingsApi extends ApiDelegateBase implements BookingsApiDelegate {
private final HttpServletRequest request;
@Autowired
private ContextService contextService;
@Autowired
public BookingsApi(final ObjectMapper objectMapper, final HttpServletRequest request) {
this.request = request;
}
public Optional<ObjectMapper> getObjectMapper() {
if (contextService.getContextOrNull() == null) {
return Optional.empty();
}
return Optional.ofNullable(contextService.getContext().getMapper());
}
public Optional<HttpServletRequest> getRequest() {
return Optional.ofNullable(request);
}
// Implement function for each request
}
ContextServiceImpl
是 @ApiCallScoped
和 @Component
。通过它获得的 ApiContext 在所有其他方面都是 per-request,但是映射器的行为并不像我预期的那样。
它产生什么
例如如果我的第一个请求将 header 设置为 false(pretty-print,不包括空值),那么我会得到响应
{
"foo": "A",
"baz": "C"
}
(正确)。发送不带 header 的后续请求(不漂亮打印,包含空值)returns
{"foo": "A","baz": "C"}
这是错误的 - 它没有空值 - 尽管 pretty-printing 已 被正确关闭。后续请求with/without header return同上两个例子,取决于header值。
另一方面,如果我的第一个请求不包含 header(不要漂亮打印,请包含空值)我得到
{"foo": "A","bar":null,"baz": "C"}
(正确)。但是随后在 return
上使用 header 的请求
{
"foo": "A",
"bar": null,
"baz": "C"
}
这是错误的 - 它确实有空值 - 虽然 pretty-printing 已正确打开。后续请求with/without header return同上,取决于header值。
我的问题
为什么它尊重 pretty-print 但不尊重 属性 包含,有没有办法让它像我想要的那样工作?
更新
我认为问题在于 Jackson 根据 object 缓存了它使用的序列化程序。我想这是设计使然——它们可能是使用反射生成的,而且相当昂贵。如果我用 header 调用一个端点(在开始 API 后第一次),它 return 没有空值。美好的。随后的调用都没有空值,无论 header 是否存在。不太好。但是,如果我随后在没有 header 的情况下调用另一个相关的端点(在开始 API 后第一次),它 returns 主要 object 为空值(很好)但是对于两个响应共有的某些 sub-objects 没有空值(因为那些 object 的序列化程序已被缓存 - 不太好)。
我看到 object 映射器有一些视图概念。有没有办法用这些来解决这个?所以它每个 object 有两个缓存的序列化器,并选择了正确的一个? (我会尝试研究这个,还没有时间,但如果有人知道我在正确或错误的轨道上,那就太好了!)
你把它搞得太复杂了。
另外 ObjectMapper
不应像您那样根据请求进行初始化或重新配置。
注意:以下配置完全不依赖于您的 ApiContext 或 ApiScope,请在使用此代码之前删除那些 类 中的所有 ObjectMapper
自定义。您可以创建一个裸机 spring 启动应用程序来测试代码。
首先需要一种方法来检测您的请求是空包含还是排除
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
public class RequestUtil {
public static boolean isNullInclusionRequest() {
RequestAttributes requestAttrs = RequestContextHolder.currentRequestAttributes();
if (!(requestAttrs instanceof ServletRequestAttributes)) {
return false;
}
HttpServletRequest servletRequest = ((ServletRequestAttributes)requestAttrs).getRequest();
return "true".equalsIgnoreCase(servletRequest.getHeader(NULLMODE_HEADER));
}
private RequestUtil() {
}
}
其次,声明您的自定义消息序列化程序
import java.lang.reflect.Type;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Order(Ordered.HIGHEST_PRECEDENCE) // Need this to be in the first of the serializers
public class NullExclusionMessageConverter extends MappingJackson2HttpMessageConverter {
public NullExclusionMessageConverter(ObjectMapper nullExclusionMapper) {
super(nullExclusionMapper);
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// Do not use this for reading. You can try it if needed
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return super.canWrite(clazz, mediaType) && !RequestUtil.isNullInclusionRequest(); }
}
import java.lang.reflect.Type;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Order(Ordered.HIGHEST_PRECEDENCE)
public class NullInclusionMessageConverter extends MappingJackson2HttpMessageConverter {
public NullInclusionMessageConverter(ObjectMapper nullInclusionMapper) {
super(nullInclusionMapper);
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// Do not use this for reading. You can try it if needed
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return super.canWrite(clazz, mediaType) && RequestUtil.isNullInclusionRequest();
}
}
三、注册自定义消息转换器:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
public class JacksonConfiguration {
@Bean
public NullInclusionMessageConverter nullInclusionMessageConverter(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.build();
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
return new NullInclusionMessageConverter(objectMapper);
}
@Bean
public NullExclusionMessageConverter nullExclusionJacksonMessageConverter(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.build();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
objectMapper.disable(SerializationFeature.INDENT_OUTPUT);
return new NullExclusionMessageConverter(objectMapper);
}
}
简介
我们在 Spring Boot 中实现了 REST API。目前它 return 序列化时的所有字段。所以它 return 类似于
{
"foo": "A",
"bar": null,
"baz": "C",
}
我们想要不 return 空字段的选项,所以它只会 return
{
"foo": "A",
"baz": "C",
}
对于那种情况 - 但仍然(如果 bar
有一个值)
{
"foo": "A",
"bar": "B",
"baz": "C",
}
我知道你可以通过应用程序属性将它引导到不 return 空值,但这是一个现有的 AI,如果字段丢失,一些针对它实现的应用程序可能会在反序列化时失败。因此,我们想让调用客户端来引导它。我们的想法是让您可以发送 header:X-OurCompany-IncludeNulls; false
。这将允许客户进行选择,我们最初会默认为 true
,但可能会随着时间的推移以可管理的方式更改默认值。
我能找到的最近的是 this,它通过查询参数转向 pretty-printing。当我尝试做类似的事情时,它适用于 pretty-printing。但是,对于包含,它适用于我启动 API 后的第一个请求,但之后每个其他请求都从第一个请求获取值。我可以看到它正在通过断点设置它,而且我还针对同一参数添加了 pretty-print 只是为了诊断目的。
我尝试的详细信息
我们的 API 基于使用 Swagger Codegen 服务器存根生成的一个。我们使用委托模式,所以它生成一个控制器,它只有一个 auto-wired 委托和一个 getDelegate
@Controller
public class BookingsApiController implements BookingsApi {
private final BookingsApiDelegate delegate;
@org.springframework.beans.factory.annotation.Autowired
public BookingsApiController(BookingsApiDelegate delegate) {
this.delegate = delegate;
}
@Override
public BookingsApiDelegate getDelegate() {
return delegate;
}
}
委托是一个接口,每个端点都包含一个函数。这些 return CompletableFuture<ResponseEntity<T>>
(其中 T
是该响应的类型)。它也是一个 getObjectMapper()
,我假设它是 Spring 用来序列化响应的内容?
public interface BookingsApiDelegate {
Logger log = LoggerFactory.getLogger(BookingsApi.class);
default Optional<ObjectMapper> getObjectMapper() {
return Optional.empty();
}
default Optional<HttpServletRequest> getRequest() {
return Optional.empty();
}
default Optional<String> getAcceptHeader() {
return getRequest().map(r -> r.getHeader("Accept"));
}
// Functions per endpoint here. By default returns Not Implemented.
}
我们有一个 object,我们称之为 ApiContext
。这是我们称之为 ApiCallScoped
的自定义范围的范围 - 基本上是 per-request 但它处理异步并复制到创建的线程。我们已经有了实现 HandlerInterceptorAdapter
的东西(尽管我们的是 @Component
而不是上面 pretty-print 示例中的 @Bean
)。我们在 preHandle
中创建上下文,所以我想将它添加到那里以设置 object 映射器属性。撇开一些 clean-up 这看起来像:
@Component
public class RestContextInterceptor extends HandlerInterceptorAdapter {
@Autowired
private ContextService apiContextService;
@Autowired
private RestRequestLogger requestLogger;
@Autowired
private ObjectMapper mapper;
@Autowired
private Jackson2ObjectMapperBuilder objectMapperBuilder;
@Autowired
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
final Object handler) throws Exception {
requestLogger.requestStart(request);
if (request.getAttribute("apiCallContext") == null) {
ApiCallContext conversationContext;
ApiContext apiContext = readApiContext(request, mapper, objectMapperBuilder, mappingJackson2HttpMessageConverter);
if (apiContext == null) {
conversationContext = new ApiCallContext("local-" + UUID.randomUUID().toString());
} else {
conversationContext = new ApiCallContext(apiContext.getTransId());
}
ApiCallContextHolder.setContext(conversationContext);
request.setAttribute("apiCallContext", conversationContext);
if (apiContext != null) {
apiContextService.setContext(apiContext);
}
} else {
ApiCallContextHolder.setContext((ApiCallContext) request.getAttribute("apiCallContext"));
}
return true;
}
private static ApiContext readApiContext(
final HttpServletRequest request,
final ObjectMapper mapper,
final Jackson2ObjectMapperBuilder objectMapperBuilder,
final MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter) {
if (request.getHeader(ApiContext.SYSTEM_JWT_HEADER) != null) {
return new ApiContext(Optional.of(request), mapper, objectMapperBuilder, mappingJackson2HttpMessageConverter);
}
return null;
}
}
在ApiContext
我们看一下header。我试过了
public final class ApiContext implements Context {
private final ObjectMapper mapper;
public ApiContext(
final Optional<HttpServletRequest> request,
final ObjectMapper mapper,
final Jackson2ObjectMapperBuilder objectMapperBuilder,
final MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter
) {
if (!request.isPresent()) {
throw new InvalidSessionException("No request found");
}
if (getBooleanFromHeader(request, NULLMODE_HEADER).orElse(DEFAULT_INCLUDE_NULL_FIELDS_IN_OUTPUT)) {
objectMapperBuilder.serializationInclusion(JsonInclude.Include.ALWAYS);
objectMapperBuilder.indentOutput(false);
mappingJackson2HttpMessageConverter.getObjectMapper().setPropertyInclusion(
JsonInclude.Value.construct(JsonInclude.Include.ALWAYS, JsonInclude.Include.ALWAYS));
mapper.setPropertyInclusion(
JsonInclude.Value.construct(JsonInclude.Include.ALWAYS, JsonInclude.Include.ALWAYS));
} else {
objectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_EMPTY);
objectMapperBuilder.indentOutput(true);
mappingJackson2HttpMessageConverter.getObjectMapper().setPropertyInclusion(
JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY));
mapper.setPropertyInclusion(
JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY));
}
objectMapperBuilder.configure(mapper);
this.mapper = objectMapperBuilder.build().copy();
}
@Override
public ObjectMapper getMapper() {
return mapper;
}
private static Optional<Boolean> getBooleanFromHeader(final Optional<HttpServletRequest> request, final String key) {
String value = request.get().getHeader(key);
if (value == null) {
return Optional.empty();
}
value = value.trim();
if (StringUtils.isEmpty(value)) {
return Optional.empty();
}
switch (value.toLowerCase()) {
case "true":
return Optional.of(true);
case "1":
return Optional.of(true);
default:
return Optional.of(false);
}
}
}
我已经尝试通过 Jackson2ObjectMapperBuilder
和 Jackson2ObjectMapperBuilder
在注入的 ObjectMapper
上设置它。我试过(我认为)所有不同的组合。发生的情况是 prettify 部分根据请求工作,但 null-inclusion 仅在第一个请求上工作,之后它保持在该值。代码是 运行(美化工作,在调试器中经过并看到它)并且当我尝试设置包含属性但它没有使用它们时没有抛出错误。
然后我们有一个实现委托接口的 @Component
。 getObjectMapper
returns 来自我们的 ApiContext
@Component
public class BookingsApi extends ApiDelegateBase implements BookingsApiDelegate {
private final HttpServletRequest request;
@Autowired
private ContextService contextService;
@Autowired
public BookingsApi(final ObjectMapper objectMapper, final HttpServletRequest request) {
this.request = request;
}
public Optional<ObjectMapper> getObjectMapper() {
if (contextService.getContextOrNull() == null) {
return Optional.empty();
}
return Optional.ofNullable(contextService.getContext().getMapper());
}
public Optional<HttpServletRequest> getRequest() {
return Optional.ofNullable(request);
}
// Implement function for each request
}
ContextServiceImpl
是 @ApiCallScoped
和 @Component
。通过它获得的 ApiContext 在所有其他方面都是 per-request,但是映射器的行为并不像我预期的那样。
它产生什么
例如如果我的第一个请求将 header 设置为 false(pretty-print,不包括空值),那么我会得到响应
{
"foo": "A",
"baz": "C"
}
(正确)。发送不带 header 的后续请求(不漂亮打印,包含空值)returns
{"foo": "A","baz": "C"}
这是错误的 - 它没有空值 - 尽管 pretty-printing 已 被正确关闭。后续请求with/without header return同上两个例子,取决于header值。
另一方面,如果我的第一个请求不包含 header(不要漂亮打印,请包含空值)我得到
{"foo": "A","bar":null,"baz": "C"}
(正确)。但是随后在 return
上使用 header 的请求{
"foo": "A",
"bar": null,
"baz": "C"
}
这是错误的 - 它确实有空值 - 虽然 pretty-printing 已正确打开。后续请求with/without header return同上,取决于header值。
我的问题
为什么它尊重 pretty-print 但不尊重 属性 包含,有没有办法让它像我想要的那样工作?
更新
我认为问题在于 Jackson 根据 object 缓存了它使用的序列化程序。我想这是设计使然——它们可能是使用反射生成的,而且相当昂贵。如果我用 header 调用一个端点(在开始 API 后第一次),它 return 没有空值。美好的。随后的调用都没有空值,无论 header 是否存在。不太好。但是,如果我随后在没有 header 的情况下调用另一个相关的端点(在开始 API 后第一次),它 returns 主要 object 为空值(很好)但是对于两个响应共有的某些 sub-objects 没有空值(因为那些 object 的序列化程序已被缓存 - 不太好)。
我看到 object 映射器有一些视图概念。有没有办法用这些来解决这个?所以它每个 object 有两个缓存的序列化器,并选择了正确的一个? (我会尝试研究这个,还没有时间,但如果有人知道我在正确或错误的轨道上,那就太好了!)
你把它搞得太复杂了。
另外 ObjectMapper
不应像您那样根据请求进行初始化或重新配置。
注意:以下配置完全不依赖于您的 ApiContext 或 ApiScope,请在使用此代码之前删除那些 类 中的所有 ObjectMapper
自定义。您可以创建一个裸机 spring 启动应用程序来测试代码。
首先需要一种方法来检测您的请求是空包含还是排除
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
public class RequestUtil {
public static boolean isNullInclusionRequest() {
RequestAttributes requestAttrs = RequestContextHolder.currentRequestAttributes();
if (!(requestAttrs instanceof ServletRequestAttributes)) {
return false;
}
HttpServletRequest servletRequest = ((ServletRequestAttributes)requestAttrs).getRequest();
return "true".equalsIgnoreCase(servletRequest.getHeader(NULLMODE_HEADER));
}
private RequestUtil() {
}
}
其次,声明您的自定义消息序列化程序
import java.lang.reflect.Type;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Order(Ordered.HIGHEST_PRECEDENCE) // Need this to be in the first of the serializers
public class NullExclusionMessageConverter extends MappingJackson2HttpMessageConverter {
public NullExclusionMessageConverter(ObjectMapper nullExclusionMapper) {
super(nullExclusionMapper);
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// Do not use this for reading. You can try it if needed
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return super.canWrite(clazz, mediaType) && !RequestUtil.isNullInclusionRequest(); }
}
import java.lang.reflect.Type;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Order(Ordered.HIGHEST_PRECEDENCE)
public class NullInclusionMessageConverter extends MappingJackson2HttpMessageConverter {
public NullInclusionMessageConverter(ObjectMapper nullInclusionMapper) {
super(nullInclusionMapper);
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// Do not use this for reading. You can try it if needed
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return super.canWrite(clazz, mediaType) && RequestUtil.isNullInclusionRequest();
}
}
三、注册自定义消息转换器:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
public class JacksonConfiguration {
@Bean
public NullInclusionMessageConverter nullInclusionMessageConverter(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.build();
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
return new NullInclusionMessageConverter(objectMapper);
}
@Bean
public NullExclusionMessageConverter nullExclusionJacksonMessageConverter(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.build();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
objectMapper.disable(SerializationFeature.INDENT_OUTPUT);
return new NullExclusionMessageConverter(objectMapper);
}
}