SpringBoot 的使用的中的一个奇怪问题:
Jackson中Mapper的注入冲突
0x1 问题
在工作中,使用的框架是SpringBoot,为了把一些对象转换为web使用的json格式的数据,就常常需要一些框架来完成,关于Object转换Json,常用的框架并不多,主要是这几个:
- Jackson: 这个框架基本成为了Spring的标配。
- FastJson : 这个是国内的一个框架,出自阿里,在某些时候据说效率会比较高。
- Gson : 没怎么用过。
我现在使用的框架就是Jackson。
SpringBoot开发的系统,有一个application.properties
文件,他可以配置很多关于Spring方方面面的基础设置,其中就有这Jackson。由于某种特定的需求,我需要在原有的系统中添加一个新的功能,而这个功能的实现,需要一个自己的ObjectMapper
– 哦,这个就是Jackson
转换Object
和Json
数据的东西。
但是,一个奇怪的问题就出现了,当这个新的ObjectMapper
作为Bean出现之后,原本SpingBoot中对于ObjectMapper
的配置全都失去了效果,后来向前辈请教,得知应该是新的Mapper覆盖了原有的Mapper。
项目有一个WebConfig类,其中一个方法是这样的:
@Bean
public JsonHandler jsonHandler(ObjectMapper objectMapper) {
JsonHandler jh = new JsonHandler();
jh.setMapper(objectMapper);
return jh;
}
这个项目在一个WEBConfig类中配置了一个用于转换Object到Json的类JsonHandler
,它就需要一个ObjectMapper,在这个配置方法中打印出这个ObjectMapper的class,正是新配置的Mapper。
而且各个配置中都没有看到对ObjectMapper的注入,那么这个方法要求的ObjectMapper,在定义这个新的Mapper之前,他又是从哪里来的呢?
0x2 新注解
我发现,在Spring的STS
(Spring官方提供的一个Eclipse版本)中,按住Ctrl的时候,可以点开application.properties配置,看到对应的源码,而Jackson的配置,是在spring-boot-autoconfigure
这个包里面的,名字是JacksonProperties
,而同一个包下,就有JacksonAutoConfiguration
。
首先我注意到的就是这个:
@Configuration
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration
很明显,这是两个注解,不过在Spring的使用中好像还没有见过他们。
而在这后面不远,就可以看到:
@Bean
@Primary
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
看起来我找到那个ObjectMapper的Bean了。
0x3 这些注解的含义和使用
注解名称 | 注解参数 | 含义 | 作用 |
---|---|---|---|
ConditionalOnBean | type,value | bean的class对象,Bean的名字 | 仅当Bean存在的时候,此注解修饰的内容才会起到作用 |
ConditionalOnMissingBean | type,value | bean的class对象,Bean的名字 | 仅当Bean不存在的时候,此注解修饰的内容才会起作用 |
ConditionalOnClass | name,value | 类名,类的class对象 | 仅当class存在的时候,此注解修饰内容才会起效 |
ConditionalOnMissingClass | value | 类名 | 仅当class不存在的时候,此注解修饰的内容才会起效 |
ConditionalOnJava | range,value | 描述java版本的枚举,定义在此注解中。 | 仅当java版本符合的时候才会起效 |
Conditional | value | Condition接口的实现类 | 根据指定类判别是否让修饰的内容起效 |
注解名称 | 作用 |
---|---|
Primary | 优先提供此注解修饰的Bean |
0x4 那么,问题就解决了
从上面的JacksonAutoConfiguration
中就可以知道,默认的ObjectMapper,是要在没有其他ObjectMapper的Bean出现的时候才会出现的,所以要解决这个问题,做法也很简单,将新的ObjectMapper添加@ConditionalOnBean
这个注解,让他在默认的ObjectMapper出现之后才能被注册,这样默认的那个ObjectMapper就不会自动消失了,而且由于默认的ObjectMapper带有@Primary
,所以会优先使用,到此为止,奇怪的Bean冲突问题就不存在了。
0x5 还是有一个后续
通过上面的操作后,可以发现虽然默认的ObjectMapper不会消失,但是CrudMapper却消失了,真是让人头大,,,
接下来我取消了@ConditionalOnBean
注解,改用@DependsOn
来控制Bean的初始化顺序,让他能够在jsonHandler加载之后在进行初始化,因为这个时候默认的ObjectMapper应该已经载入了,但是呢,想法是美好的,现实是残酷的:
Description:
There is a circular dependency between 6 beans in the application context:
好吧,出现了循环引用,应该是由于新的ObjectMapper是ObjectMapper的子类,而默认的ObjectMapper一旦发现有其他ObjectMapper的Bean,就不会注册,因此就导致了其他需要默认ObjectMapper的Bean对新的ObjectMapper出现了引用,这就出来了循环引用的问题。
我最终是这样做的,使用泛型作为返回类型,抹除本来的类型签名,这样就会导致config中不存在ObjectMapper的Bean定义,从而使得Jackson默认的那个可以得到加载,然后让他DependOn那个jsonHandler,确保它是在默认的ObjectMapepr之后才会出现。
注解名字 | 参数 | 含义 |
---|---|---|
DependOn | value(String[] Bean名字) | 此Bean依赖于另外的Bean,必须在那些Bean之后进行初始化 |
另外,Java的Annotation配置中,Bean默认的id就是注解了@Bean的方法的名字,而@Autowired注解默认装配是ByType而@Resource是ByName,这里抹去了类型就是为了避免这种ByType的装配,防止由此导致的循环依赖,不过抹除了类型就无法使用ByType装配了,这里我使用的时@Resource来注入这个新的ObjectMapepr。