Ada*_*dam 4 java spring jackson spring-boot
介绍
我们在 Spring Boot 中实现了一个 REST API。目前它在序列化时返回所有字段。所以它返回类似
{
"foo": "A",
"bar": null,
"baz": "C",
}
Run Code Online (Sandbox Code Playgroud)
我们想要不返回空字段的选项,所以它只会返回
{
"foo": "A",
"baz": "C",
}
Run Code Online (Sandbox Code Playgroud)
对于那种情况 - 但仍然(如果bar有价值)
{
"foo": "A",
"bar": "B",
"baz": "C",
}
Run Code Online (Sandbox Code Playgroud)
我知道你可以控制它不通过应用程序属性返回空值,但这是一个现有的 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;
}
}
Run Code Online (Sandbox Code Playgroud)
委托是一个接口,其中每个端点都包含一个函数。这些返回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.
}
Run Code Online (Sandbox Code Playgroud)
我们有一个对象,我们称之为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;
}
}
Run Code Online (Sandbox Code Playgroud)
在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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
我试图把它放在注入ObjectMapper,通过Jackson2ObjectMapperBuilder并还经Jackson2ObjectMapperBuilder。我已经尝试过(我认为)所有不同的组合。发生的情况是美化部分对每个请求都有效,但空包含仅对第一个请求有效,之后它保持在该值。代码正在运行(美化工作,在调试器中通过并看到它)并且当我尝试设置包含属性但它没有使用它们时不会抛出错误。
然后我们有一个@Component实现委托接口的。getObjectMapper从我们的返回映射器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
}
Run Code Online (Sandbox Code Playgroud)
该ContextServiceImpl是@ApiCallScoped和@Component。通过所有其他方式获得的 ApiContext 是每个请求的,但映射器的行为不像我预期的那样。
它产生什么
例如,如果我的第一个请求将标头设置为 false(漂亮的打印,不包括空值),那么我会得到响应
{
"foo": "A",
"baz": "C"
}
Run Code Online (Sandbox Code Playgroud)
(哪个是正确的)。发送没有标头的后续请求(不要漂亮打印,包括空值)返回
{"foo": "A","baz": "C"}
Run Code Online (Sandbox Code Playgroud)
这是错误的 - 它没有空值 - 尽管漂亮的打印已被正确关闭。后续请求有/无头返回与上述两个示例相同,具体取决于头值。
另一方面,如果我的第一个请求不包含标题(不漂亮打印,请包含空值)我得到
{"foo": "A","bar":null,"baz": "C"}
Run Code Online (Sandbox Code Playgroud)
(哪个是正确的)。但随后的请求返回时带有标头
{
"foo": "A",
"bar": null,
"baz": "C"
}
Run Code Online (Sandbox Code Playgroud)
这是错误的-它确实有空-虽然漂亮印刷已被正确打开。带有/不带标头的后续请求将返回与上述相同的请求,具体取决于标头值。
我的问题
为什么它尊重漂亮的印刷品而不是属性包含,有没有办法让它像我想要的那样工作?
更新
我认为问题在于 Jackson 缓存了它为每个对象使用的序列化程序。我想这是设计使然 - 它们可能是使用反射生成的并且相当昂贵。如果我使用标头调用一个端点(启动 API 后的第一次),它返回而没有空值。美好的。无论是否存在标头,后续调用都没有空值。没那么好。但是,如果我然后在没有标头的情况下调用另一个相关的端点(在启动 API 后第一次),它会返回主对象(很好)的空值,但两个响应共有的一些子对象没有空值(如这些对象的序列化程序已被缓存 - 不太好)。
我看到对象映射器有一些视图的概念。有没有办法使用这些来解决这个问题?所以它每个对象有两个缓存的序列化程序,并选择了正确的一个?(我会尝试研究这个,还没有时间,但如果有人知道我在正确或错误的轨道上,那么知道它会很好!)
你把它弄得太复杂了。
还ObjectMapper应该初始化或像你这样每个请求重新配置。在这里查看原因
注意:以下配置完全不依赖于您的 ApiContext 或 ApiScope,请ObjectMapper在使用此代码之前删除这些类中的所有自定义。您可以创建一个裸露的 Spring Boot 应用程序来测试代码。
首先需要一种方法来检测您的请求是空包含还是排除
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() {
}
}
Run Code Online (Sandbox Code Playgroud)
其次,声明您的自定义消息序列化程序
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(); }
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
三、注册自定义消息转换器:
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);
}
}
Run Code Online (Sandbox Code Playgroud)