Spring 引导:控制每个请求的序列化响应中是否存在空字段



简介

我们在Spring Boot中实现了一个REST API。目前,它在序列化时返回所有字段。 所以它返回类似的东西

{
"foo": "A",
"bar": null,
"baz": "C",
}

我们希望该选项不返回空字段,因此它只会返回

{
"foo": "A",
"baz": "C",
}

对于这种情况 - 但仍然(如果bar有价值)

{
"foo": "A",
"bar": "B",
"baz": "C",
}

我知道您可以引导它不通过应用程序属性返回 null,但这是一个现有的 AI,如果缺少字段,针对它实现的某些应用程序可能会在反序列化时失败。 因此,我们希望让调用客户端来指导这一点。 我们的想法是有一个可以发送的标题:X-OurCompany-IncludeNulls; false。 这将允许客户端选择,我们最初默认为true,但随着时间的推移可能会以托管方式更改默认值。

我能找到的最接近的是这个,它通过查询参数引导漂亮的打印。 当我尝试做类似的事情时,它适用于漂亮的打印。 但是,对于包含,它适用于我启动 API 后的第一个请求,但之后每个其他请求都会从第一个请求中获取值。 我可以看到它正在通过断点设置它,并且我还针对同一参数添加了漂亮的打印,仅用于诊断目的。

我尝试过的细节

我们的API基于使用Swagger Codegen服务器存根生成的API。 我们使用委托模式,因此它会生成一个控制器,该控制器只有一个自动连线的委托和一个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;
}
}

委托是一个接口,其中包含每个终结点的函数。 这些返回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.
}

我们有一个对象,我们称之为ApiContext. 这的作用域是我们称之为ApiCallScoped的自定义范围 - 基本上是每个请求,但它处理异步并复制到创建的线程。 我们已经有一些实现HandlerInterceptorAdapter的东西(尽管我们的@Component而不是像上面的漂亮打印示例中那样@Bean)。 我们在preHandle中创建上下文,所以我想把它添加到那里以设置对象映射器属性。 撇开一些清理不谈,这看起来像:

@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中,我们查看标题。 我试过了

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

我尝试将其设置在注入的ObjectMapper上,通过Jackson2ObjectMapperBuilderJackson2ObjectMapperBuilder。 我已经尝试了(我认为)所有不同的组合。 发生的情况是,美化部分对每个请求有效,但 null 包含仅适用于第一个请求,之后它保持该值。 代码正在运行(美化工作,在调试器中浏览并看到它)并且在我尝试设置包含属性时不会抛出错误,但它没有使用它们。

然后,我们有一个实现委托接口的@ComponentgetObjectMapper从我们的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 以所有其他方式按请求进行,但映射器的行为不符合我的预期。

它产生什么

例如,如果我的第一个请求将标头设置为 false(漂亮打印,不包括空值),那么我会收到响应

{
"foo": "A",
"baz": "C"
}

(这是正确的)。发送不带标头的后续请求(不要漂亮打印,要包含空值)返回

{"foo": "A","baz": "C"}

这是错误的 - 它没有空值 - 尽管漂亮的打印已被正确关闭。 带有/不带标头的后续请求返回与上述两个示例相同的请求,具体取决于标头值。

另一方面,如果我的第一个请求不包含标头(不要漂亮打印,请包含空值),我得到

{"foo": "A","bar":null,"baz": "C"}

(这是正确的)。 但是在返回时带有标头的后续请求

{
"foo": "A",
"bar": null,
"baz": "C"
}

这是错误的 - 它确实有空值 - 尽管漂亮的打印正确打开。带/不带标头的后续请求返回与上述相同的请求,具体取决于标头值。

我的问题

为什么它尊重漂亮的印刷品而不是财产包含,有没有办法让它像我想要的那样工作?

更新

我认为问题是杰克逊缓存了它为每个对象使用的序列化器。我想这是设计使然 - 它们可能是使用反射生成的并且相当昂贵。 如果我使用标头调用一个端点(启动 API 后第一次),它会返回而不返回空值。好。后续调用都没有空值,无论标头是否存在。没那么好。但是,如果我随后在没有标头的情况下调用另一个相关的端点(启动 API 后第一次),它会返回主对象的 null(很好),但对于两个响应共有的某些子对象没有 null(因为这些对象的序列化器已被缓存 - 不是那么好)。

我看到对象映射器有一些视图概念。有没有办法使用这些来解决这个问题? 所以它每个对象有两个缓存的序列化器,并选择了正确的一个? (我会尝试调查这个问题,还没有时间,但如果有人知道我走在正确或错误的轨道上,那就太好了!

你把它弄得太复杂了。

此外,ObjectMapper不应像您那样根据请求进行初始化或重新配置。在这里查看原因

注意:以下配置根本不依赖于您的 ApiContext 或 ApiScope,请在使用此代码之前删除这些类中的所有ObjectMapper自定义。可以创建裸弹簧启动应用来测试代码。

首先需要一种方法来检测您的请求是空包含还是排除

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

最新更新