简体   繁体   中英

How to add new ObjectMapper in Spring Boot project

In my Spring Boot project I use a default 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. Adding new @Bean definition will override the default ObjectMapper. How can I add new ObjectMapper Bean without overriding the former one?

Yes, @ConditionalOnMissingBean is [hard-impossible] to hack. 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". (at the costs of: referring to 1+/thinking about the difference/more complexity)

Mentioned MappingJackson2HttpMessageConverter ( auto-config here ) has this (built-in) capability (& purpose) to map to multiple object mappers in terms of "http conversion".

So with a (generic, eg java.util.Map based) thing like:

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:

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;(

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? (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).

Instead the auto configuration provides (if none defined/ @ConditionalOnMissingBean ):

  • a prototype Jackson2ObjectMapperBuilder bean, which (everytime when requested):

So the the simplest approach seems (up-to-date) to:

  • steel/adopt the code (not-/implementing 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:

// ...
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:

@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.

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. You don't have to inject any beans. just use the Util. Here is Javadoc for the JsonUtils class. It comes with the java Open Source MgntUtils library written and maintained by me. You can get it as Maven artifacts or in 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). In the end, I gave up and simply made the second bean (mimicking the default one), myself. I chose to name it, hopefully to avoid any collision, and to declare it @Primary, to be chosen as would have the default.

In either case, making an ObjectMapper is quite easy, as such:

    @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!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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