@Bean注解在现代化Spring应用中得到了广泛的应用,在大多数场景下,@Bean是配合@Configuration注解一起使用的,但这并不意味着@Bean必须配合@Configuration使用,反之,它可以与@Service@Component等Bean声明注解一起使用,这种用法与配合@Configuration使用有什么区别呢?什么是@BeanLite Mode呢?本文将给出答案。

概念

@Bean定义在@Component等Bean声明注解内或POJO对象内部时称之为Lite Mode,反之,定义在@Configuration对象内部时称之为Full Mode

Lite Mode定义

以上定义概括于@BeanAPI文档以及Spring Framework的官方文档,为方便大家理解,下面我们通过一个例子来详细说明。

举例

这里展示一个非常典型Lite Mode场景,并包含内部Bean依赖(inter-bean dependencies)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Slf4j
@Service
public class LiteConfig {

static class LiteService {

static AtomicLong COUNTER = new AtomicLong();

LiteService() {
log.info("creating...");
COUNTER.incrementAndGet();
}

@PostConstruct
void init() {
log.info("initializing...");
}

void doSomething() {
log.info("doing...");
}
}

@Bean
LiteService liteService() {
return new LiteService();
}

@Bean
@Order(0)
public CommandLineRunner withDependency(LiteService liteService) {
return args -> {
log.info("running with dependency");
liteService.doSomething();
};
}

@Bean
@Order(1)
public CommandLineRunner withInterReference() {
return args -> {
log.info("running with InterReference");
this.liteService().doSomething();
};
}

@Bean
@Order(100)
public CommandLineRunner showCounter() {
return args -> log.info("total instance count: {}",
LiteService.COUNTER.get());
}

}

简要解释一下,

  • 定义了一个LiteConfig类,但注解为Service,按照定义,其内部定义的@Bean的行为都会处在Lite Mode下;
  • LiteService是一个简单的类,包含了一个构造方法,一个初始化方法(init),一个具体执行业务逻辑的方法(doSomething),除日志输出外,构造方法会记录创建实例的个数;
  • 通过@Bean创建一个LiteService的Bean;
  • LiteServiceBean会被接下来的两个CommandLineRunnerBean依赖,依赖的方式不同,withDependency通过方法参数得到依赖,而withInterReference通过内部方法调用得到依赖;
  • 最后一个showCounter用于打印在以上代码执行后,总共创建的LiteService实例的个数。

让我们执行这段代码并查看输出,这里将略去不相关部分,

1
2
3
4
5
6
7
8
9
Root WebApplicationContext: initialization completed in 1290 ms
creating...
initializing...
running with dependency
doing...
running with InterReference
creating...
doing...
total instance count: 2

从运行结果上,我们不难看出,

  1. Spring容器初始化完成之后,liteService()方法被Spring执行,创建了一个LiteService的Bean,并回调了init方法;
  2. 接下来withDependencyCommandLineRunner运行,liteService作为依赖被传入,并调用了doSomething方法;
  3. 接下来withInterReferenceCommandLineRunner运行,通过调用liteService()方法来获取依赖,这时liteService()方法本体又被执行了一遍,并且没有回调init方法
  4. 接下来showCounterCommandLineRunner运行,打印了LiteService实例的总数,共2个。

具体原因,我们稍后解释,这里我们先吧@Service注解换成@Configuration,看会发生什么,

1
2
3
4
5
6
7
8
Root WebApplicationContext: initialization completed in 553 ms
creating...
initializing...
running with dependency
doing...
running with InterReference
doing...
total instance count: 1

比较之前的运行结果,我们发现,

  1. 与之前相同:Spring容器初始化完成之后,liteService()方法被Spring执行,创建了一个LiteService的Bean,并回调了init方法;
  2. 与之前相同:接下来withDependencyCommandLineRunner运行,liteService作为依赖被传入,并调用了doSomething方法;
  3. 与之前不同:接下来withInterReferenceCommandLineRunner运行,通过调用liteService()方法来获取依赖,没有重新执行本体,直接调用了doSomething方法
  4. 与之前不同:接下来showCounterCommandLineRunner运行,打印了LiteService实例的总数,只有1个。

综上所述,两次运行结果的差异表现在当使用inter-bean references获取内部依赖时的行为不同,Lite Mode下,内部方法调用就是纯粹的执行了内部方法的逻辑,而在Full Mode下,内部方法调用被Spring拦截且直接返回了已经创建好的Bean,并没有重新执行内部方法的逻辑。

揭秘

官方文档有大段的论述来解释Lite ModeFull Mode,原文稍微有一些晦涩,这里我提炼一下官方文档中关于Lite ModeFull 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 ModeLite Mode加以区分,也为使用者提供了精确控制与选择的机会,即使绝大部分使用者一般都会选择使用Full Mode

实践

有同学可能会问,如上面的例子所示,withDependencyCommandLineRunner是通过方法参数来获取依赖的,只不过这个依赖是在同一个类里面由另外一个@Bean方法定义的内部Bean依赖,这个也是Spring推荐的方式,那么不就可以避免通过内部方法调用来获取依赖了吗?

一般情况下,答案是肯定的,但也有例外情况,假设我们不能自定义一个使用依赖的方法的参数呢?这种场景存在吗?
看下面的例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

public static class DummyInterceptor implements HandlerInterceptor {

@PostConstruct
public void init() {
log.info("initializing dummy interceptor...");
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

//handling logic
return true;
}
}

@Bean
public DummyInterceptor dummyInterceptor() {
return new DummyInterceptor();
}

@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.dummyInterceptor()).addPathPatterns("/**");
super.addInterceptors(registry);
}
}

Spring Boot应用中,我们经常会通过扩展WebMvcConfigurationSupport来自定义一些MVC相关的设置,上面的例子就是在自定义拦截器,而拦截器恰好是在当前类里面通过@Bean创建的一个Bean,而addInterceptors方法却是要覆写父类的,无法通过修改参数列表来获取依赖,只能通过调用内部方法来获取,这时Full Mode就有用武之地了,通过调用内部方法,就会得到一个完整的Spring容器管理的拦截器Bean。这样的场景在扩展Spring的各种Config里很常见,例如Spring SecuritySpring Data Couchbase等。

总结

在Spring体系中,@Bean是一个再常用不过的注解,但Lite ModeFull Mode并不被人所熟知。当我们肆意使用@Bean定义着Bean,通过方法参数传递传递着依赖时,我们并不清楚Spring的设计者有多少设计上的考量以及Spring的开发者通过哪些手段和技巧实现了设计者的理念。表面上看,这些无伤大雅,也无关紧要,甚至作为细节,使用者也无需关心,我随便baidu了一下spring @bean lite mode这几个关键字,发现为数不多的文章里,大部分是机器翻译了Spring的官方文档,幸甚幸甚,至少还有人去看这部分的文档。

分享这个小技巧,并不是为了炫技,因为这本身也没有什么高深的,我甚至都没有贴一行Spring的源代码,我也不想去找,因为我觉得Spring官方文档上的论述就已经足够好了,不管我们是否关心,它都在那里。

后记

前面提到,Lite Mode是可以用在POJO对象里的,那会是怎样的行为呢?有兴趣的朋友可以自己试试看。