@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; LiteServiceBean会被接下来的两个CommandLineRunnerBean依赖,依赖的方式不同,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对象里的,那会是怎样的行为呢?有兴趣的朋友可以自己试试看。