如何在不影响原始 bean 的“客户”的情况下声明另一个 Jackson ObjectMapper?

Posted

技术标签:

【中文标题】如何在不影响原始 bean 的“客户”的情况下声明另一个 Jackson ObjectMapper?【英文标题】:How to declare another Jackson ObjectMapper without affecting 'clients' of the original bean? 【发布时间】:2017-10-26 23:02:34 【问题描述】:

我有一个公开 json REST API 的 spring-boot 应用程序。为了将对象映射到 json,它使用 spring-boot 配置的内置 jackson ObjectMapper。

现在我需要从 yaml 文件中读取一些数据,我发现一个简单的方法是使用 Jackson - 为此我需要声明一个不同的 ObjectMapper 来将 yaml 转换为对象。我用一个特定的名称声明了这个新的映射器 bean,以便能够将它注入到我的服务中,处理从 yaml 文件中读取:

@Bean(YAML_OBJECT_MAPPER_BEAN_ID)
public ObjectMapper yamlObjectMapper() 
    return new ObjectMapper(new YAMLFactory());

但我需要一种方法来告诉原始 json ObjectMapper 的所有其他“客户”继续使用该 bean。所以基本上我需要在原始 bean 上添加一个 @Primary 注释。有没有办法实现这一点,而不必在我自己的配置中重新声明原始 ObjectMapper(我必须通过 spring-boot 代码来查找并复制其配置)?

我发现的一个解决方案是为 ObjectMapper 声明一个 FactoryBean 并使其返回已声明的 bean,如 this answer 中所建议的那样。调试发现我原来的bean叫“_halObjectMapper”,所以我的factoryBean会搜索这个bean并返回:

public class ObjectMapperFactory implements FactoryBean<ObjectMapper> 

    ListableBeanFactory beanFactory;

    public ObjectMapper getObject() 
        return beanFactory.getBean("_halObjectMapper", ObjectMapper.class);
    
    ...

然后在我的 Configuration 类中,我将其声明为 @Primary bean,以确保它是自动装配的首选:

@Primary
@Bean
public ObjectMapperFactory objectMapperFactory(ListableBeanFactory beanFactory) 
    return new ObjectMapperFactory(beanFactory);

不过,我对这个解决方案并不是 100% 满意,因为它依赖于不受我控制的 bean 的名称,而且它看起来也像是一个 hack。有更清洁的解决方案吗?

谢谢!

【问题讨论】:

我还尝试使用YAMLMapper(扩展ObjectMapper)作为我的自定义bean 类型,但是spring 仍然会抱怨它找到了两个ObjectMapper 类型的bean。我没想到会这样,尽管在某种程度上它是合理的。所以我今天又学到了一些新东西。这意味着如果我自动装配List&lt;Object&gt; 类型的字段,我将获得上下文中所有可用的bean... 查看我的答案的更新.. 【参考方案1】:

您可以定义两个 ObjectMapper bean 并将一个声明为主,例如:

@Bean("Your_id")
public ObjectMapper yamlObjectMapper() 
    return new ObjectMapper(new YAMLFactory());


@Bean
@Primary
public ObjectMapper objectMapper() 
    return new ObjectMapper();

完成后,您可以使用带有 @Qualifier 注释的 objectmapper bean,例如:

@Autowired
@Qualifier("Your_id")
private ObjectMapper yamlMapper;

更新

您可以在运行时将 ObjectMapper 动态添加到 Spring 的 bean 工厂,例如:

@Configuration
public class ObjectMapperConfig 

    @Autowired
    private ConfigurableApplicationContext  context;

    @PostConstruct
    private void init()
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ObjectMapper.class);
        builder.addConstructorArgValue(new JsonFactory());
        DefaultListableBeanFactory factory = (DefaultListableBeanFactory) context.getBeanFactory();
        factory.registerBeanDefinition("yamlMapper", builder.getBeanDefinition());
        Map<String, ObjectMapper> beans = context.getBeansOfType(ObjectMapper.class);
        beans.entrySet().forEach(System.out::println);
    

上面的代码在context中添加了一个新的bean而不改变现有的bean(sysoutinit方法的末尾打印两个bean)。然后,您可以使用“yamlMapper”作为限定符在任何地方自动装配它。

更新 2(来自问题作者):

“更新”中建议的解决方案有效,这是一个简化版本:

@Autowired
private DefaultListableBeanFactory beanFactory;

@PostConstruct
private void init()
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(YAMLMapper.class);
    beanFactory.registerBeanDefinition("yamlMapper", builder.getBeanDefinition());

【讨论】:

这是我尝试的第一件事,但是new ObjectMapper() 的配置方式与spring-boot 定义的方式不同,所以我得到了很多失败的测试。这就是为什么我想继续使用完全相同的 bean,而不是影响当前的 API。 这与我之前创建的答案完全相同。你为什么要这么做? @luboskrnac 你和我的答案相差不到 2 分钟。您认为我当时阅读了您的答案、改写并输入了我的答案并附有解释吗? 其实5分钟 7:50:017:51:56,5 分钟?【参考方案2】:

我刚刚意识到我不需要使用 FactoryBean,我也可以将常规 bean 声明为 @Primary 并使其返回原始 bean,如下所示:

@Bean
@Primary
public ObjectMapper objectMapper(@Qualifier("_halObjectMapper") ObjectMapper objectMapper) 
    return objectMapper;

这使得配置更加简洁,但仍然需要原始 ObjectMapper 的确切名称。不过,我想我会继续使用这个解决方案。

【讨论】:

【参考方案3】:

其他选项是将自定义映射器包装到自定义对象中:

@Component
public class YamlObjectMapper 
    private final ObjectMapper objectMapper;

    public YamlObjectMapper() 
        objectMapper = new ObjectMapper(new YAMLFactory());
    

    public ObjectMapper getMapper() 
        return objectMapper;
    

不幸的是,这种方法需要在注入 YamlObjectMapper 后调用 getMapper

【讨论】:

【参考方案4】:

我相信为 MVC 层定义显式主要对象映射器应该这样工作:

 @Primary
 @Bean
 public ObjectMapper objectMapper() 
     return Jackson2ObjectMapperBuilder.json().build();
 

通过类型自动装配对象映射器的所有 bean 都将使用上面的 bean。您的 Yaml 逻辑可以通过 YAML_OBJECT_MAPPER_BEAN_ID 自动装配。

【讨论】:

我尝试了你的建议,但它与 new ObjectMapper() 相同 - 它不会为原始映射器重用 spring-boot 配置,所以我再次得到失败的测试。 顺便说一句,Darshan Mehta 创建的答案以糟糕的方式抄袭了我的,因为这创建了使用 Spring 默认值启动的更好的 bean。 你在测试中遇到什么样的异常? 测试失败,因为当我使用 new ObjectMapper() 而不是原始的 spring-boot 实例时,输出 json 与预期值不匹配。我想有一些额外的配置与它如何格式化某些值有关。

以上是关于如何在不影响原始 bean 的“客户”的情况下声明另一个 Jackson ObjectMapper?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不更改原始列表的情况下更改新列表?

如何在不影响 phpMyAdmin 的情况下从 mysql 中巧妙地删除 pma 表

如何在不更改原始内容的情况下操作 NSDictionary 内容的副本

组合:如何在不完成原始发布者的情况下替换/捕获错误?

如何在不修改原始drawable的情况下设置vectorDrawable的色调?

如何编写一个函数,在不使用返回的情况下更改原始整数列表?