分布式和微服务的兴起催生了工程模块化的方向,从而会出现各种通用模块与通用脚手架。
这类通用模块尤以 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 类:
1 | public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable { |
通过此类,我们可以在某一个 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 中的配置类。
以下是非常简单的代码模拟:
1 |
|
通过 @Enable* 注解启用上面的配置 (spring有更完善的 通过 spring.factories 配置自动加载,这里不做赘述)。
1 |
|
我们的工程通过引入这个包之后,然后在启动类配置如下信息:
1 |
|
而如果我们的工程代码中也有一个自己的 zookeeper 的配置 bean:
1 |
|
此时,启动项目,便会报如下错:
1 | *************************** |
这个错误直接原因就是:当前工程上下文和依赖的组件上下文没有隔离。
3.2.问题跟踪
常用解决办法一般是在 starter 脚手架组件的bean 配置类上面加 @Condition* 类的注解,如我们改造上面 starter 的代码:
1 |
|
@ConditionalOnMissingBean 注解表示的意思是:
如果在 spring 上下文中找不到 GsonBuilder的 bean,这里才会配置。如果 上下文已经有相同的 bean 类型,那么这里就不会进行配置。
本文我们将不采用这种做法,我们可以通过 Spring 父子容器来隔离工程代码 和 starter 等依赖代码。
3.3.Spring 父子容器隔离上下文
将公共组件包(如 通用log、通用缓存)等里面的 Spring 配置信息通通由 父容器进行加载。
将当前工程上下文中的所有 Spring 配置由 子容器进行加载。
父容器和子容器可以存在相同类型的 bean,并且如果子容器存在,则会优先使用子容器的 bean,我们可以将上面代码进行如下改造:
在工程目录下创建一个 parent 包,并编写 parent 父容器的配置类:
1 |
|
自定义实现 SpringApplicationBuilder 类:
1 | public class ChildSpringApplicationBuilder extends SpringApplicationBuilder { |
- 主要作用是在启动 Springboot 子容器时,先根据父配置类 ParentSpringConfiguration 初始化父 容器 GenericApplicationContext。
- 然后当前 SpringApplicationBuilder 上下文将 父容器设置为初始化的父容器,这样就完成了父子容器配置。
- starter 中的 GsonBuilder 会在父容器中进行初始化。
启动 Spring 容器:
1 |
|
此时,可以正常启动 spring 容器,我们通过 applicationContext.getBean() 的形式获取 ZookeeperClinet。
1 | public static void main(String[] args) { |
日志打印:
1 | zk name: ZookeeperClient(name=From Current Project) //来自当前工程,子容器 |
可以看到当前上下文拿到的 bean 是当前工程配置的 bean,然而我们还可以获取到 父容器中配置的 bean,通过先 getParent() (注意NPE),然后再获取bean,则会获取到 父容器中的 bean。
4.总结
自从 Spring Boot 流行以后,Spring 父子容器的概念和使用就显得很少了。目前在网上搜索相关内容,大部分都会通过 SpringMVC + Spring 的关系来理解父子容器。
本文则通过在 SpringBoot 的基础上通过 父子容器来实现 工程脚手架、starter 等 与 工程上下文的 bean 隔离,将父子容器的功能完美应用于上下文的隔离,继续发挥去潜在优势,避免不必要的 bean 冲突。
希望这篇文章能够带给读者一定的收获。
本文工程源码:parent-and-children。
原文链接: https://hzways.gitee.io/p/ea1a0d11.html
版权声明: 转载请注明出处.