@Bean注解在现代化Spring应用中得到了广泛的应用,在大多数场景下,@Bean
是配合@Configuration注解一起使用的,但这并不意味着@Bean
必须配合@Configuration
使用,反之,它可以与@Service、@Component等Bean声明注解一起使用,这种用法与配合@Configuration
使用有什么区别呢?什么是@Bean
的Lite Mode
呢?本文将给出答案。
概念
当
@Bean
定义在@Component
等Bean声明注解内或POJO
对象内部时称之为Lite Mode
,反之,定义在@Configuration
对象内部时称之为Full Mode
。
以上定义概括于@Bean
的API文档以及Spring Framework的官方文档,为方便大家理解,下面我们通过一个例子来详细说明。
举例
这里展示一个非常典型Lite Mode
场景,并包含内部Bean依赖(inter-bean dependencies)
。
1 | 4j |
简要解释一下,
- 定义了一个
LiteConfig
类,但注解为Service
,按照定义,其内部定义的@Bean
的行为都会处在Lite Mode
下; LiteService
是一个简单的类,包含了一个构造方法,一个初始化方法(init
),一个具体执行业务逻辑的方法(doSomething
),除日志输出外,构造方法会记录创建实例的个数;- 通过
@Bean
创建一个LiteService
的Bean; LiteService
Bean会被接下来的两个CommandLineRunner
Bean依赖,依赖的方式不同,withDependency
通过方法参数得到依赖,而withInterReference
通过内部方法调用得到依赖;- 最后一个
showCounter
用于打印在以上代码执行后,总共创建的LiteService
实例的个数。
让我们执行这段代码并查看输出,这里将略去不相关部分,
1 | Root WebApplicationContext: initialization completed in 1290 ms |
从运行结果上,我们不难看出,
- Spring容器初始化完成之后,
liteService()
方法被Spring执行,创建了一个LiteService
的Bean,并回调了init
方法; - 接下来
withDependency
的CommandLineRunner
运行,liteService
作为依赖被传入,并调用了doSomething
方法; - 接下来
withInterReference
的CommandLineRunner
运行,通过调用liteService()
方法来获取依赖,这时liteService()
方法本体又被执行了一遍,并且没有回调init
方法; - 接下来
showCounter
的CommandLineRunner
运行,打印了LiteService
实例的总数,共2个。
具体原因,我们稍后解释,这里我们先吧@Service
注解换成@Configuration
,看会发生什么,
1 | Root WebApplicationContext: initialization completed in 553 ms |
比较之前的运行结果,我们发现,
- 与之前相同:Spring容器初始化完成之后,
liteService()
方法被Spring执行,创建了一个LiteService
的Bean,并回调了init
方法; - 与之前相同:接下来
withDependency
的CommandLineRunner
运行,liteService
作为依赖被传入,并调用了doSomething
方法; - 与之前不同:接下来
withInterReference
的CommandLineRunner
运行,通过调用liteService()
方法来获取依赖,没有重新执行本体,直接调用了doSomething
方法; - 与之前不同:接下来
showCounter
的CommandLineRunner
运行,打印了LiteService
实例的总数,只有1个。
综上所述,两次运行结果的差异表现在当使用inter-bean references
获取内部依赖时的行为不同,Lite Mode
下,内部方法调用就是纯粹的执行了内部方法的逻辑,而在Full Mode
下,内部方法调用被Spring拦截且直接返回了已经创建好的Bean,并没有重新执行内部方法的逻辑。
揭秘
官方文档有大段的论述来解释Lite Mode
与Full Mode
,原文稍微有一些晦涩,这里我提炼一下官方文档中关于Lite Mode
与Full Mode
的异同,
相同点:
Lite Mode
定义下的Bean本质上与Full Mode
定义下的Bean没有本质区别,都可以被其他Bean依赖;Bean容器
仍然会管理Lite Mode
定义下的Bean的生命周期,@PostConstruct
、@PreDestroy
等注解依然生效;
不同点:
Lite Mode
无法兼容内部Bean依赖(inter-bean dependencies)
,究其本质,在Full Mode
下,Spring会使用类似CGLIB proxy
来拦截所有的方法调用,如果发现内部方法调用是为了获取内部Bean依赖(inter-bean dependencies)
,那么Spring将直接返回这个Bean。
从这些异同点我们可以看出,Spring的设计者对于使用类似CGLIB proxy
来拦截所有的方法这类操作还是比较慎重的,所以不惜用Full Mode
和Lite Mode
加以区分,也为使用者提供了精确控制与选择的机会,即使绝大部分使用者一般都会选择使用Full Mode
。
实践
有同学可能会问,如上面的例子所示,withDependency
的CommandLineRunner
是通过方法参数来获取依赖的,只不过这个依赖是在同一个类里面由另外一个@Bean
方法定义的内部Bean依赖
,这个也是Spring推荐的方式,那么不就可以避免通过内部方法调用来获取依赖了吗?
一般情况下,答案是肯定的,但也有例外情况,假设我们不能自定义一个使用依赖的方法的参数呢?这种场景存在吗?
看下面的例子,
1 |
|
在Spring Boot
应用中,我们经常会通过扩展WebMvcConfigurationSupport
来自定义一些MVC相关的设置,上面的例子就是在自定义拦截器,而拦截器恰好是在当前类里面通过@Bean
创建的一个Bean,而addInterceptors
方法却是要覆写父类的,无法通过修改参数列表来获取依赖,只能通过调用内部方法来获取,这时Full Mode
就有用武之地了,通过调用内部方法,就会得到一个完整的Spring容器管理的拦截器Bean。这样的场景在扩展Spring的各种Config里很常见,例如Spring Security
、Spring Data Couchbase
等。
总结
在Spring体系中,@Bean
是一个再常用不过的注解,但Lite Mode
与Full Mode
并不被人所熟知。当我们肆意使用@Bean
定义着Bean,通过方法参数传递传递着依赖时,我们并不清楚Spring的设计者有多少设计上的考量以及Spring的开发者通过哪些手段和技巧实现了设计者的理念。表面上看,这些无伤大雅,也无关紧要,甚至作为细节,使用者也无需关心,我随便baidu了一下spring @bean lite mode
这几个关键字,发现为数不多的文章里,大部分是机器翻译了Spring的官方文档,幸甚幸甚,至少还有人去看这部分的文档。
分享这个小技巧,并不是为了炫技,因为这本身也没有什么高深的,我甚至都没有贴一行Spring的源代码,我也不想去找,因为我觉得Spring官方文档上的论述就已经足够好了,不管我们是否关心,它都在那里。
后记
前面提到,Lite Mode
是可以用在POJO
对象里的,那会是怎样的行为呢?有兴趣的朋友可以自己试试看。