Spring父子容器的应用: Bean冲突解决与上下文隔离

随着分布式和微服务部署的不断兴起。公司的工程模块和依赖变得越来越多,从而错综复杂。因此,通过通用 脚手架与通用模块等将各个项目通用的模块单独提炼出来,并形成依赖 jar,这类配置尤以 Spring 来的广泛,本文将介绍基于此形成的一种痛点及其解决方案。

1.痛点

你有没有这样的痛点?

  • 1.当前开发工程依赖的 spring-boot-starter 脚手架,配置了很多通用 bean,而部分无法满足自身需求,因此发现自己定义的 bean 和脚手架中的 某个 bean 出现冲突,导致出现 bean 重复的报错问题。

  • 2.脚手架的引入扰乱了当前业务线的 bean 依赖流程,有时候去捋顺这些依赖都煞费苦心,程序运行时,出现各类奇怪的运行冲突与报错。

  • 3.随着大家对 spring boot 使用的深入,大家对 @Condition* 之类的注解会越用越多。如果此时,无法控制。

本文尝试着通过 Sping 父子容器这一概念来对解决这些痛点提供一些思路与demo。

2.Spring父子容器

2.1.介绍

ApplicationContext 是 Spring 的高级容器,目前我们使用的 SpringBoot 和 SpringMvc 等容器,使用的都是 ApplicationContext 的子类。该上下文支持父子容器的概念,具体是定义可见 ConfigurableApplicationContext 类:

public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
  // 其他方法省略
  void setParent(@Nullable ApplicationContext parent);
}  

通过此类,我们可以在某一个 applicationContext 中 设置它的父容器 parent。

2.2.Spring 父子容器的使用场景

Spring中,父子容器不是继承关系,他们是通过组合关系完成的,即子容器通过 setParent()持有父容器的引用。

  • 父容器对子容器可见,子容器对父容器不可见。详细来说,就是 Spring 父子容器中,父容器不能访问子容器的 bean 。而子容器可以访问父容器的内容。
  • 如果父子容器中都存在某个 bean 的情况,子容器会使用自身上下文定义的 bean,从而覆盖父容器定义的相同的 bean。(这点很重要)。

总结:父子容器的主要用途是上下文隔离。

在传统的 SpringMVC + Spring 的架构中,Spring 负责 service 和 dao 层的 bean 管理,并支持事务,aop切面等功能。

而springMVC 为子容器,直接托管 controller 层等与 web 相关的代码,在使用 service 层的 bean时,直接从 父容器中获取即可。

而现今,在使用 springboot 的场景下,我们一般只有一个上下文。父子容器的使用和概念貌似已经被开发人员遗忘了。

但是,当出现文章开头出现的那些痛点时,我们应该怎么做呢?

其实我们就可以通过 Spring 父子容器的概念来实现 脚手架 与 当前工程的 bean 隔离,来达到和解决 bean 依赖冲突的各类问题。

3.Spring父子容器上下文隔离实战

3.1.通用脚手架与Bean冲突

假设我们开发了一个 Zookeeper 的 starter,引入这个 starter 包,就会自动注入zookeeper 相关的配置,下面代码是脚手架 starter 中的配置类。

以下是非常简单的代码模拟:

@Configuration
public class ZookeeperConfiguration {
  @Bean
  public ZookeeperClient zookeeperClient() {
    return new ZookeeperClient("From Starter.");
  }
}

通过 @Enable* 注解启用上面的配置 (spring有更完善的 通过 spring.factories 配置自动加载,这里不做赘述)。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(ZookeeperConfiguration.class)
public @interface EnableZookeeper {
}

我们的工程通过引入这个包之后,然后在启动类配置如下信息:

@EnableZookeeper
@SpringBootApplication
public class ChildSpringServer {
  public static void main(String[] args) {
    SpringApplication.run(ChildSpringServer.class, args);
  }
}

而如果我们的工程代码中也有一个自己的 zookeeper 的配置 bean:

@Slf4j
@Configuration
public class ChildConfiguration {
  @Bean
  ZookeeperClient zookeeperClient() {
    return new ZookeeperClient("From Current Project");
  }
} 

此时,启动项目,便会报如下错:

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'zookeeperClient', defined in com.maple.common.starter.ZookeeperConfiguration, could not be registered. A bean with that name has already been defined in class path resource [com/maple/spring/container/child/config/ChildConfiguration.class] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

这个错误直接原因就是:当前工程上下文和依赖的组件上下文没有隔离。

3.2.问题跟踪

常用解决办法一般是在 starter 脚手架组件的bean 配置类上面加 @Condition* 类的注解,如我们改造上面 starter 的代码:

@Slf4j
@Configuration
public class ZookeeperConfiguration {
  @Bean
  //这是新加的
  @ConditionalOnMissingBean
  public ZookeeperClient zookeeperClient() {
    return new ZookeeperClient("From Starter.");
  }
}

@ConditionalOnMissingBean 注解表示的意思是:

如果在 spring 上下文中找不到 GsonBuilder的 bean,这里才会配置。如果 上下文已经有相同的 bean 类型,那么这里就不会进行配置。

本文我们将不采用这种做法,我们可以通过 Spring 父子容器来隔离工程代码 和 starter 等依赖代码。

3.3.Spring 父子容器隔离上下文

将公共组件包(如 通用log、通用缓存)等里面的 Spring 配置信息通通由 父容器进行加载。

将当前工程上下文中的所有 Spring 配置由 子容器进行加载。

父容器和子容器可以存在相同类型的 bean,并且如果子容器存在,则会优先使用子容器的 bean,我们可以将上面代码进行如下改造:

在工程目录下创建一个 parent 包,并编写 parent 父容器的配置类:

@Slf4j
@Configuration
//将 starter 中的 enable 注解放在父容器的 配置中
@EnableZookeeper
public class ParentSpringConfiguration {
}

自定义实现 SpringApplicationBuilder 类:

public class ChildSpringApplicationBuilder extends SpringApplicationBuilder {


  public ChildSpringApplicationBuilder(Class<?>... sources) {
    super(sources);
  }

  public ChildSpringApplicationBuilder functions() {
    //初始化父容器,class类为刚写的父配置文件 ParentSpringConfiguration
    GenericApplicationContext parent = new AnnotationConfigApplicationContext(ParentSpringConfiguration.class);
    this.parent(parent);
    return this;
  }

}
  • 主要作用是在启动 Springboot 子容器时,先根据父配置类 ParentSpringConfiguration 初始化父 容器 GenericApplicationContext。
  • 然后当前 SpringApplicationBuilder 上下文将 父容器设置为初始化的父容器,这样就完成了父子容器配置。
  • starter 中的 GsonBuilder 会在父容器中进行初始化。

启动 Spring 容器:

@Slf4j
//@EnableZookeeper 此注解放到了 ParentConfiguration中。
@SpringBootApplication
public class ChildSpringServer {

  public static void main(String[] args) {
    ConfigurableApplicationContext applicationContext = new ChildSpringApplicationBuilder(ChildSpringServer.class)
        .functions()
        .run(args);

    log.info("applicationContext: {}", applicationContext);
  }
}

此时,可以正常启动 spring 容器,我们通过 applicationContext.getBean() 的形式获取 ZookeeperClinet。

public static void main(String[] args) {
    ConfigurableApplicationContext applicationContext = new ChildSpringApplicationBuilder(ChildSpringServer.class)
        .functions()
        .registerShutdownHook(false)
        .run(args);

    log.info("applicationContext: {}", applicationContext);
    //当前上下文
    log.info("zk name: {}", applicationContext.getBean(ZookeeperClient.class));

    //当前上下文的父容器 get
    log.info("parent zk name: {}", applicationContext.getParent().getBean(ZookeeperClient.class));
  }

日志打印:

zk name: ZookeeperClient(name=From Current Project) //来自当前工程,子容器
parent zk name: ZookeeperClient(name=From Starter.) //来自父容器

可以看到当前上下文拿到的 bean 是当前工程配置的 bean,然而我们还可以获取到 父容器中配置的 bean,通过先 getParent() (注意NPE),然后再获取bean,则会获取到 父容器中的 bean。

4.总结

自从 Spring Boot 流行以后,Spring 父子容器的概念和使用就显得很少了。目前在网上搜索相关内容,大部分都会通过 SpringMVC + Spring 的关系来理解父子容器。

本文则通过在 SpringBoot 的基础上通过 父子容器来实现 工程脚手架、starter 等 与 工程上下文的 bean 隔离,将父子容器的功能完美应用于上下文的隔离,继续发挥去潜在优势,避免不必要的 bean 冲突。

希望这篇文章能够带给读者一定的收获。

本文工程源码:parent-and-children

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容