简体   繁体   English

如何在 Spring Boot 项目中添加新的 ObjectMapper

[英]How to add new ObjectMapper in Spring Boot project

In my Spring Boot project I use a default Jackson ObjectMapper.在我的 Spring Boot 项目中,我使用默认的 Jackson ObjectMapper。 I'd like to add new ObjectMapper to the Spring Context and start using it at new places, but also keep the default one.我想将新的 ObjectMapper 添加到 Spring 上下文并开始在新地方使用它,但也保留默认值。 Adding new @Bean definition will override the default ObjectMapper.添加新的 @Bean 定义将覆盖默认的 ObjectMapper。 How can I add new ObjectMapper Bean without overriding the former one?如何在不覆盖前一个的情况下添加新的 ObjectMapper Bean?

Yes, @ConditionalOnMissingBean is [hard-impossible] to hack.是的, @ConditionalOnMissingBean [很难] 破解。 With a simple trick (asian philosophy), we can circumvent the problem/make it no problem at all:通过一个简单的技巧(亚洲哲学),我们可以规避问题/使其完全没有问题:

Wrap your (1+, auto configured, @ConditionalOnMissing... ) bean in something else/custom/a "wrapper".将您的(1+,自动配置, @ConditionalOnMissing... )bean 包装在其他/自定义/“包装器”中。 (at the costs of: referring to 1+/thinking about the difference/more complexity) (代价是:参考1+/思考差异/更复杂)

Mentioned MappingJackson2HttpMessageConverter ( auto-config here ) has this (built-in) capability (& purpose) to map to multiple object mappers in terms of "http conversion".提到的MappingJackson2HttpMessageConverter此处为自动配置)具有此(内置)功能(和目的)以根据“http 转换”映射到多个对象映射器。

So with a (generic, eg java.util.Map based) thing like:因此,使用(通用的,例如基于 java.util.Map 的)类似的东西:

class MyWrapper<K, V> {
  final Map<K, V> map;
  public MyWrapper(Map<K, V> map) {
    this.map = map;
  }
  public Map<K, V> getMap() {
    return map;
  }
}

We can go wire it:我们可以连接它:

@Bean
MyWrapper<String, ObjectMapper> myStr2OMWrapper(/*ObjectMapper jacksonOm*/) {
  return new MyWrapper() {
    {
      // map.put(DEFAULT, jacksonOm);
      getMap().put("foo", fooMapper());
      getMap().put("bar", barMapper());
    }
  };
}

..where fooMapper() and barMapper() can refer to (static/instance) no-bean methods: .. fooMapper()barMapper()可以引用(静态/实例) no-bean方法:

private static ObjectMapper fooMapper() {
  return new ObjectMapper()
      .configure(SerializationFeature.INDENT_OUTPUT, true) // just a demo...
      .configure(SerializationFeature.WRAP_ROOT_VALUE, true); // configure/set  as see fit...
}
private static ObjectMapper barMapper() {
  return new ObjectMapper()
      .configure(SerializationFeature.INDENT_OUTPUT, false) // just a demo...
      .configure(SerializationFeature.WRAP_ROOT_VALUE, false); // configure/set more...
}

(Already) testing/using time: (已经)测试/使用时间:

package com.example.demo;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoAppTests {

  @Autowired
  MyWrapper<String, ObjectMapper> my;
  @Autowired
  ObjectMapper jacksonOM;

  @Test
  void contextLoads() {
    System.err.println(jacksonOM);
    Assertions.assertNotNull(jacksonOM);
    my.getMap().entrySet().forEach(e -> {
      System.err.println(e);
      Assertions.assertNotNull(e.getValue());
    });
  }
}

Prints (eg)打印(例如)

...
com.fasterxml.jackson.databind.ObjectMapper@481b2f10
bar=com.fasterxml.jackson.databind.ObjectMapper@577bf0aa
foo=com.fasterxml.jackson.databind.ObjectMapper@7455dacb
...
Results:

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
...

Sorry this test dosn't verify (individual) configuration, but only: (visually different) not null object mappers.抱歉,此测试不验证(个人)配置,而仅:(视觉上不同)不是空对象映射器。


How to enable (multiple!) my.custom.jackson.* auto configuration, is a more complex question... (it is not as easy as eg my.custom.datasource.* config;(如何启用(多个!) my.custom.jackson.*自动配置,是一个更复杂的问题......(它不像my.custom.datasource.*配置那么简单;(

With:和:

@Bean
@Primary // ! for auto config, we need one primary (whether it is "spring.jackson"  ... adjust;)
@ConfigurationProperties("spring.jackson")
JacksonProperties springJacksonProps() {
  return new JacksonProperties();
}

@Bean
@ConfigurationProperties("foo.jackson")
JacksonProperties fooProps() {
  return new JacksonProperties();
}

@Bean
@ConfigurationProperties("bar.jackson")
JacksonProperties barProps() {
  return new JacksonProperties();
}

we can already load and differentiate (full blown) config like:我们已经可以加载和区分(完全成熟的)配置,例如:

spring.jackson.locale=en_US
spring.jackson.time-zone=UTC
# ... all of spring.jackson @see org.springframework.boot.autoconfigure.jackson.JacksonProperties
foo.jackson.locale=en_US
foo.jackson.time-zone=PST
# ... just for demo purpose
bar.jackson.locale=de_DE
bar.jackson.time-zone=GMT+1

And also (no problem) pass them (props) to the according (static [foo|bar]Mapper ) methods.... but then?并且(没问题)将它们(道具)传递给相应的(静态[foo|bar]Mapper )方法......但是然后呢? (If you are good with it, you can stop reading here::) (如果你对它很好,你可以在这里停止阅读::)

Unfortunately the according ("state of art") code (to wire JacksonProperties with "om builder") is not public (ie not extendable/pluggable).不幸的是, 相应的(“最先进”)代码(将JacksonProperties与“om builder”连接起来)不是公开的(即不可扩展/可插入)。

Instead the auto configuration provides (if none defined/ @ConditionalOnMissingBean ):相反,自动配置提供(如果没有定义/ @ConditionalOnMissingBean ):

  • a prototype Jackson2ObjectMapperBuilder bean, which (everytime when requested):一个原型Jackson2ObjectMapperBuilder bean,它(每次请求时):

So the the simplest approach seems (up-to-date) to:因此,最简单的方法(最新)似乎是:

  • steel/adopt the code (not-/implementing Jackson2ObjectMapperBuilderCustomizer )钢铁/采用代码(不-/实施Jackson2ObjectMapperBuilderCustomizer
  • construct (from "stolen" + properties) according builders/mappers, as see fit.根据构建器/映射器构建(来自“被盗”+属性),视情况而定。

eg (review+TEST before PROD,) non-interface, returns a Jackson2ObjectMapperBuilder , mimic the auto-configured, without applying (other) customizers/-ation:例如(review+TEST before PROD,)非接口,返回一个Jackson2ObjectMapperBuilder ,模仿自动配置,不应用(其他)定制器/-ation:

// ...
import com.fasterxml.jackson.databind.Module; // !! not java.lang.Module ;)
// ...
private static class MyStolenCustomizer {

  private final JacksonProperties jacksonProperties;
  private final Collection<Module> modules;
  // additionally need/want this:
  private final ApplicationContext applicationContext;
  // copy/adopt from spring-boot:
  private static final Map<?, Boolean> FEATURE_DEFAULTS = Map.of(
      SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false,
      SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false
  );

  public MyStolenCustomizer(
    ApplicationContext applicationContext,
    JacksonProperties jacksonProperties,
    Collection<Module> modules
  ) {
      this.applicationContext = applicationContext;
      this.jacksonProperties = jacksonProperties;
      this.modules = modules;
  }
  // changed method signature!!
  public Jackson2ObjectMapperBuilder buildCustom() {
    // mimic original (spring-boot) bean:
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.applicationContext(applicationContext);
    // without (additional!) customizers:
    if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
      builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
    }
    if (this.jacksonProperties.getTimeZone() != null) {
      builder.timeZone(this.jacksonProperties.getTimeZone());
    }
    configureFeatures(builder, FEATURE_DEFAULTS);
    configureVisibility(builder, this.jacksonProperties.getVisibility());
    configureFeatures(builder, this.jacksonProperties.getDeserialization());
    configureFeatures(builder, this.jacksonProperties.getSerialization());
    configureFeatures(builder, this.jacksonProperties.getMapper());
    configureFeatures(builder, this.jacksonProperties.getParser());
    configureFeatures(builder, this.jacksonProperties.getGenerator());
    configureDateFormat(builder);
    configurePropertyNamingStrategy(builder);
    configureModules(builder);
    configureLocale(builder);
    configureDefaultLeniency(builder);
    configureConstructorDetector(builder);
    // custom api:
    return builder; // ..alternatively: builder.build();
  }
  // ... rest as in https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java#L223-L341

To wire modules , we can (hopefully, as originally) rely on:要连接modules ,我们可以(希望像最初那样)依赖:

@Autowired
ObjectProvider<com.fasterxml.jackson.databind.Module> modules

To initialize them like:像这样初始化它们:

@Bean
MyStolenCustomizer fooCustomizer(ApplicationContext context, @Qualifier("fooProps") JacksonProperties fooProperties, ObjectProvider<Module> modules) {
  return new MyStolenCustomizer(context, fooProperties, modules.stream().toList());
}

@Bean
MyStolenCustomizer barCustomizer(ApplicationContext context, @Qualifier("barProps") JacksonProperties barProperties, ObjectProvider<Module> modules) {
  return new MyStolenCustomizer(context, barProperties, modules.stream().toList());
}

..and use them like: ..并像这样使用它们:

@Bean
MyWrapper<String, Jackson2ObjectMapperBuilder> myStr2OMBuilderWrapper(
    @Qualifier("fooCustomizer") MyStolenCustomizer fooCustomizer,
    @Qualifier("barCustomizer") MyStolenCustomizer barCustomizer) {
  return new MyWrapper(
      Map.of(
          "foo", fooCustomizer.buildCustom(),
          "bar", barCustomizer.buildCustom()
      )
  );
}

...avoiding "double customization"/leaving JacksonAutoConfiguration enabled/intact/active. ...避免“双重定制”/启用JacksonAutoConfiguration /完整/活动。

Problem: time/updates(/external code)!问题:时间/更新(/外部代码)!

If you want just a default ObjectMapper to use, I wrote a small utility that has some static methods for serializing/deserializing JSON and it uses ObjectMapper inside.如果您只想使用默认的 ObjectMapper,我写了一个小实用程序,它有一些用于序列化/反序列化 JSON 的静态方法,它在内部使用 ObjectMapper。 You don't have to inject any beans.您不必注入任何 bean。 just use the Util.只需使用 Util。 Here is Javadoc for the JsonUtils class.这是JsonUtils类的 Javadoc。 It comes with the java Open Source MgntUtils library written and maintained by me.它带有由我编写和维护的 java 开源 MgntUtils 库。 You can get it as Maven artifacts or in Github .您可以将其作为Maven 工件或在Github中获取。

I, too, just faced a similar problem - I had already figured out how to make a new ObjectMapper bean, but I couldn't figure out, no matter what I did, how to keep that from Spring Boot's auto-configuration (so that it would continue to make the default one).我也遇到了类似的问题——我已经想出了如何制作一个新的 ObjectMapper bean,但无论我做了什么,我都无法弄清楚如何将它从 Spring Boot 的自动配置中保留下来(这样它将继续成为默认值)。 In the end, I gave up and simply made the second bean (mimicking the default one), myself.最后,我放弃了,自己做了第二个 bean(模仿默认的)。 I chose to name it, hopefully to avoid any collision, and to declare it @Primary, to be chosen as would have the default.我选择命名它,希望避免任何冲突,并将其声明为@Primary,以选择默认值。

In either case, making an ObjectMapper is quite easy, as such:在任何一种情况下,制作 ObjectMapper 都非常容易,如下所示:

    @Bean("standardJsonObjectMapper") // named, though not necessary
    @org.springframework.context.annotation.Primary // mimic default
    public com.fasterxml.jackson.databind.ObjectMapper standardJsonObjectMapper() {
        return
            org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
                .json()
                .build();
    }

That builder has MANY functions available for customization (like failOnUnknownProperties(boolean) and featuresToEnable(Object...) ) - just choose the ones you want, and off you go!该构建器有许多可用于自定义的函数(例如failOnUnknownProperties(boolean)featuresToEnable(Object...) )——只需选择您想要的函数,然后开始!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM