[Spring] 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容