为什么 Spring Boot 不使用 @Primary Jackson ObjectMapper 在 rest 控制器上进行 JSON 序列化?

Why is Spring Boot not using a @Primary Jackson ObjectMapper for JSON serialization on a rest controller?

我有一个 class 设置为 return 一个自定义的 ObjectMapper。据我所知,Spring Boot 使用此 ObjectMapper 的正确方法是将其声明为 @Primary,它是。

@Configuration
public class MyJacksonConfiguration {

    @Bean
    @Primary
    public ObjectMapper objectMapper() {
        return Jackson2ObjectMapperBuilder
            .json()
            .findModulesViaServiceLoader(true)
            .mixIn(Throwable.class, ThrowableMixin.class)
            .featuresToDisable(
                    WRITE_DATES_AS_TIMESTAMPS)
            .serializationInclusion(
                    Include.NON_ABSENT)
            .build();
    }
}

然而,当我 return 来自控制器方法的对象时,它使用默认的 Jackson ObjectMapper 配置进行序列化。

如果我向我的控制器添加一个明确的 ObjectMapper 并对其调用 writeValueAsString,我可以看到这个 ObjectMapper 是我希望 Spring 启动时使用的自定义对象。

@RestController
public class TestController {

    @Autowired
    private TestService service;

    @Autowired
    private ObjectMapper mapper;

    @GetMapping(value = "/test", produces = "application/json")
    public TestResult getResult() {

        final TestResult ret = service.getResult();

        String test = "";
        try {
            test = mapper.writeValueAsString(ret);
            // test now contains the value I'd like returned by the controller!
        } catch (final JsonProcessingException e) {
            e.printStackTrace();
        }

        return ret;
    }
}

当我 运行 在我的控制器上进行测试时,测试 class 也使用自动装配的 ObjectMapper。同样,提供给测试的 ObjectMapper 是自定义的。

所以 Spring 在某种程度上知道自定义的 ObjectMapper,但我的其余控制器没有使用它 classes。

我已尝试为 Spring 打开调试日志记录,但在日志中看不到任何有用的信息。

知道可能会发生什么,或者我还应该去哪里寻找问题?

编辑:似乎有多种方法可以做到这一点,但我尝试这样做的方式似乎是一种推荐的方法,我想得到它以这种方式工作 - 参见 https://docs.spring.io/spring-boot/docs/1.4.7.RELEASE/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper 的 71.3 - 我是不是误解了什么?

Spring 使用 HttpMessageConverters 呈现@ResponseBody(或来自@RestController 的响应)。我认为您需要覆盖 HttpMessageConverter。 您可以通过扩展 WebMvcConfigurerAdapter 并覆盖以下内容来做到这一点。

 @Override
public void configureMessageConverters(
  List<HttpMessageConverter<?>> converters) {     
    messageConverters.add(new MappingJackson2HttpMessageConverter());
    super.configureMessageConverters(converters);
}

Spring documentation

因为您所做的是创建一个 bean,您可以 Autowired 供以后使用。 (就像你在代码中写的那样)Spring boot 使用 auto-configured Jackson2ObjectMapperBuilder 获取默认的 ObjectMapper 进行序列化。

所以如果你想在你的rest controller上自定义一个ObjectMapper用于序列化,你需要想办法配置Jackson2ObjectMapperBuilder。有两种方法可以做到这一点:

为 spring

声明您自己的 Jackson2ObjectMapperBuilder
@Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.serializationInclusion(JsonInclude.Include.NON_NULL);
    // other settings you want.
    return builder;
}

通过属性设置

可以通过application.properties设置。例如,如果要启用漂亮打印,请设置 spring.jackson.serialization.indent_output=true.

更多详细信息,请查看这里: Customize the Jackson ObjectMapper

虽然其他答案显示了实现相同结果的替代方法,但这个问题的实际答案是我定义了一个扩展 WebMvcConfigurationSupport 的单独 class。通过这样做,WebMvcAutoConfiguration bean 已被禁用,因此 @Primary ObjectMapper 未被 Spring 拾取。 (在 WebMvcAutoConfiguration source 中寻找 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)。)

暂时删除扩展 WebMvcConfigurationSupport 的 class 允许 @Primary ObjectMapper 按预期被 Spring 拾取和使用。

由于无法永久删除扩展 class 的 WebMvcConfigurationSupport,因此我向其中添加了以下内容:

@Autowired
private ObjectMapper mapper;

@Override
public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) {
    converters.add(new MappingJackson2HttpMessageConverter(mapper));
    addDefaultHttpMessageConverters(converters);
    super.configureMessageConverters(converters);
}

不会破坏字符串序列化的正确方法(保持顺序):

public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    addDefaultHttpMessageConverters(converters);

    HttpMessageConverter<?> jacksonConverter =
        converters.stream()
            .filter(converter -> converter.getClass().equals(MappingJackson2HttpMessageConverter.class))
            .findFirst().orElseThrow(RuntimeException::new);

    converters.add(converters.indexOf(jacksonConverter), new MappingJackson2HttpMessageConverter(objectMapper));
}