[Spring] Spring父子容器的應(yīng)用: Bean沖突解決與上下文隔離

隨著分布式和微服務(wù)部署的不斷興起。公司的工程模塊和依賴變得越來越多,從而錯(cuò)綜復(fù)雜。因此,通過通用 腳手架與通用模塊等將各個(gè)項(xiàng)目通用的模塊單獨(dú)提煉出來,并形成依賴 jar,這類配置尤以 Spring 來的廣泛,本文將介紹基于此形成的一種痛點(diǎn)及其解決方案。

1.痛點(diǎn)

你有沒有這樣的痛點(diǎn)?

  • 1.當(dāng)前開發(fā)工程依賴的 spring-boot-starter 腳手架,配置了很多通用 bean,而部分無法滿足自身需求,因此發(fā)現(xiàn)自己定義的 bean 和腳手架中的 某個(gè) bean 出現(xiàn)沖突,導(dǎo)致出現(xiàn) bean 重復(fù)的報(bào)錯(cuò)問題。

  • 2.腳手架的引入擾亂了當(dāng)前業(yè)務(wù)線的 bean 依賴流程,有時(shí)候去捋順這些依賴都煞費(fèi)苦心,程序運(yùn)行時(shí),出現(xiàn)各類奇怪的運(yùn)行沖突與報(bào)錯(cuò)。

  • 3.隨著大家對(duì) spring boot 使用的深入,大家對(duì) @Condition* 之類的注解會(huì)越用越多。如果此時(shí),無法控制。

本文嘗試著通過 Sping 父子容器這一概念來對(duì)解決這些痛點(diǎn)提供一些思路與demo。

2.Spring父子容器

2.1.介紹

ApplicationContext 是 Spring 的高級(jí)容器,目前我們使用的 SpringBoot 和 SpringMvc 等容器,使用的都是 ApplicationContext 的子類。該上下文支持父子容器的概念,具體是定義可見 ConfigurableApplicationContext 類:

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

通過此類,我們可以在某一個(gè) applicationContext 中 設(shè)置它的父容器 parent。

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

Spring中,父子容器不是繼承關(guān)系,他們是通過組合關(guān)系完成的,即子容器通過 setParent()持有父容器的引用。

  • 父容器對(duì)子容器可見,子容器對(duì)父容器不可見。詳細(xì)來說,就是 Spring 父子容器中,父容器不能訪問子容器的 bean 。而子容器可以訪問父容器的內(nèi)容。
  • 如果父子容器中都存在某個(gè) bean 的情況,子容器會(huì)使用自身上下文定義的 bean,從而覆蓋父容器定義的相同的 bean。(這點(diǎn)很重要)。

總結(jié):父子容器的主要用途是上下文隔離。

在傳統(tǒng)的 SpringMVC + Spring 的架構(gòu)中,Spring 負(fù)責(zé) service 和 dao 層的 bean 管理,并支持事務(wù),aop切面等功能。

而springMVC 為子容器,直接托管 controller 層等與 web 相關(guān)的代碼,在使用 service 層的 bean時(shí),直接從 父容器中獲取即可。

而現(xiàn)今,在使用 springboot 的場景下,我們一般只有一個(gè)上下文。父子容器的使用和概念貌似已經(jīng)被開發(fā)人員遺忘了。

但是,當(dāng)出現(xiàn)文章開頭出現(xiàn)的那些痛點(diǎn)時(shí),我們應(yīng)該怎么做呢?

其實(shí)我們就可以通過 Spring 父子容器的概念來實(shí)現(xiàn) 腳手架 與 當(dāng)前工程的 bean 隔離,來達(dá)到和解決 bean 依賴沖突的各類問題。

3.Spring父子容器上下文隔離實(shí)戰(zhàn)

3.1.通用腳手架與Bean沖突

假設(shè)我們開發(fā)了一個(gè) Zookeeper 的 starter,引入這個(gè) starter 包,就會(huì)自動(dòng)注入zookeeper 相關(guān)的配置,下面代碼是腳手架 starter 中的配置類。

以下是非常簡單的代碼模擬:

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

通過 @Enable* 注解啟用上面的配置 (spring有更完善的 通過 spring.factories 配置自動(dòng)加載,這里不做贅述)。

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

我們的工程通過引入這個(gè)包之后,然后在啟動(dòng)類配置如下信息:

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

而如果我們的工程代碼中也有一個(gè)自己的 zookeeper 的配置 bean:

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

此時(shí),啟動(dòng)項(xiàng)目,便會(huì)報(bào)如下錯(cuò):

***************************
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

這個(gè)錯(cuò)誤直接原因就是:當(dāng)前工程上下文和依賴的組件上下文沒有隔離。

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,這里才會(huì)配置。如果 上下文已經(jīng)有相同的 bean 類型,那么這里就不會(huì)進(jìn)行配置。

本文我們將不采用這種做法,我們可以通過 Spring 父子容器來隔離工程代碼 和 starter 等依賴代碼。

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

將公共組件包(如 通用log、通用緩存)等里面的 Spring 配置信息通通由 父容器進(jìn)行加載。

將當(dāng)前工程上下文中的所有 Spring 配置由 子容器進(jìn)行加載。

父容器和子容器可以存在相同類型的 bean,并且如果子容器存在,則會(huì)優(yōu)先使用子容器的 bean,我們可以將上面代碼進(jìn)行如下改造:

在工程目錄下創(chuàng)建一個(gè) parent 包,并編寫 parent 父容器的配置類:

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

自定義實(shí)現(xiàn) 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;
  }

}
  • 主要作用是在啟動(dòng) Springboot 子容器時(shí),先根據(jù)父配置類 ParentSpringConfiguration 初始化父 容器 GenericApplicationContext。
  • 然后當(dāng)前 SpringApplicationBuilder 上下文將 父容器設(shè)置為初始化的父容器,這樣就完成了父子容器配置。
  • starter 中的 GsonBuilder 會(huì)在父容器中進(jìn)行初始化。

啟動(dòng) 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);
  }
}

此時(shí),可以正常啟動(dòng) 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);
    //當(dāng)前上下文
    log.info("zk name: {}", applicationContext.getBean(ZookeeperClient.class));

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

日志打印:

zk name: ZookeeperClient(name=From Current Project) //來自當(dāng)前工程,子容器
parent zk name: ZookeeperClient(name=From Starter.) //來自父容器

可以看到當(dāng)前上下文拿到的 bean 是當(dāng)前工程配置的 bean,然而我們還可以獲取到 父容器中配置的 bean,通過先 getParent() (注意NPE),然后再獲取bean,則會(huì)獲取到 父容器中的 bean。

4.總結(jié)

自從 Spring Boot 流行以后,Spring 父子容器的概念和使用就顯得很少了。目前在網(wǎng)上搜索相關(guān)內(nèi)容,大部分都會(huì)通過 SpringMVC + Spring 的關(guān)系來理解父子容器。

本文則通過在 SpringBoot 的基礎(chǔ)上通過 父子容器來實(shí)現(xiàn) 工程腳手架、starter 等 與 工程上下文的 bean 隔離,將父子容器的功能完美應(yīng)用于上下文的隔離,繼續(xù)發(fā)揮去潛在優(yōu)勢,避免不必要的 bean 沖突。

希望這篇文章能夠帶給讀者一定的收獲。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容