版本约定
本文内容若没做特殊说明,均基于以下版本:
- JDK:1.8
- Spring Framework:5.2.2.RELEASE
正文
上篇文章介绍了代理对象两个拦截器其中的前者,即BeanFactoryAwareMethodInterceptor,它会拦截setBeanFactory()方法从而完成给代理类指定属性赋值。通过第一个拦截器的讲解,你能够成功“忽悠”很多面试官了,但仍旧不能够解释我们最常使用中的这个疑惑:为何通过调用@Bean方法最终指向的仍旧是同一个Bean呢?
带着这个疑问,开始本文的陈诉。请系好安全带,准备发车了…
Spring配置类的使用误区
根据不同的配置方式,展示不同情况。从Lite模式的使用产生误区,到使用Full模式解决问题,最后引出解释为何有此效果的原因分析/源码解析。
Lite模式:错误姿势
配置类:
public class AppConfig { @Bean public Son son() { Son son = new Son(); System.out.println("son created..." + son.hashCode()); return son; } @Bean public Parent parent() { Son son = son(); System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } }
运行程序:
public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); AppConfig appConfig = context.getBean(AppConfig.class); System.out.println(appConfig); // bean情况 Son son = context.getBean(Son.class); Parent parent = context.getBean(Parent.class); System.out.println("容器内的Son实例:" + son.hashCode()); System.out.println("容器内Person持有的Son实例:" + parent.getSon().hashCode()); System.out.println(parent.getSon() == son); }
运行结果:
son created...624271064 son created...564742142 parent created...持有的Son是:564742142 com.yourbatman.fullliteconfig.config.AppConfig@1a38c59b 容器内的Son实例:624271064 容器内Person持有的Son实例:564742142 false
结果分析:
- Son实例被创建了2次。很明显这两个不是同一个实例
- 第一次是由Spring创建并放进容器里(624271064这个)
- 第二次是由构造parent时创建,只放进了parent里,并没放进容器里(564742142这个)
这样的话,就出问题了。问题表现在这两个方面:
- Son对象被创建了两次,单例模式被打破
- 对Parent实例而言,它依赖的Son不再是IoC容器内的那个Bean,而是一个非常普通的POJO对象而已。所以这个Son对象将不会享有Spring带来的任何“好处”,这在实际场景中一般都是会有问题的
这种情况在生产上是一定需要避免,那怎么破呢?下面给出Lite模式下使用的正确姿势。
Lite模式:正确姿势
其实这个问题,现在这么智能的IDE(如IDEA)已经能教你怎么做了:
按照“指示”,可以使用依赖注入的方式代替从而避免这种问题,如下:
// @Bean // public Parent parent() { // Son son = son(); // System.out.println("parent created...持有的Son是:" + son.hashCode()); // return new Parent(son); // } @Bean public Parent parent(Son son){ System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); }
再次运行程序,结果为:
son created...624271064 parent created...持有的Son是:624271064 com.yourbatman.fullliteconfig.config.AppConfig@667a738 容器内的Son实例:624271064 容器内Person持有的Son实例:624271064 true
Full模式:
Full模式是容错性最强的一种方式,你乱造都行,没啥顾虑。
当然喽,方法不能是private/final。但一般情况下谁会在配置里final掉一个方法呢?你说对吧~
@Configuration public class AppConfig { @Bean public Son son() { Son son = new Son(); System.out.println("son created..." + son.hashCode()); return son; } @Bean public Parent parent() { Son son = son(); System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } }
运行程序,结果输出:
son created...1797712197 parent created...持有的Son是:1797712197 com.yourbatman.fullliteconfig.config.AppConfig$$EnhancerBySpringCGLIB$$8ef51461@be64738 容器内的Son实例:1797712197 容器内Person持有的Son实例:1797712197 true
结果是完美的。它能够保证你通过调用标注有@Bean的方法得到的是IoC容器里面的实例对象,而非重新创建一个。相比较于Lite模式,它还有另外一个区别:它会为配置类生成一个CGLIB的代理子类对象放进容器,而Lite模式放进容器的是原生对象。
凡事皆有代价,一切皆在取舍。原生的才是效率最高的,是对Cloud Native最为友好的方式。但在实际“推荐使用”上,业务端开发一般只会使用Full模式,毕竟业务开发的同学水平是残参差不齐的,容错性就显得至关重要了。
如果你是容器开发者、中间件开发者…推荐使用Lite模式配置,为容器化、Cloud Native做好准备嘛~
Full模式既然是面向使用侧为常用的方式,那么接下来就趴一趴Spring到底是施了什么“魔法”,让调用@Bean方法竟然可以不进入方法体内而指向同一个实例。
BeanMethodInterceptor拦截器
终于到了今天的主菜。关于前面的流程分析本文就一步跳过,单刀直入分析BeanMethodInterceptor这个拦截器,也也就是所谓的两个拦截器的后者。
温馨提示:亲务必确保已经了解过了上篇文章的流程分析哈,不然下面内容很容易造成你脑力不适的
相较于上个拦截器,这个拦截器不可为不复杂。官方解释它的作用为:拦截任何标注有@Bean注解的方法的调用,以确保正确处理Bean语义,例如作用域(请别忽略它)和AOP代理。
复杂归复杂,但没啥好怕的,一步一步来呗。同样的,我会按如下两步去了解它:执行时机 + 做了何事。
执行时机
废话不多说,直接结合源码解释。
BeanMethodInterceptor: @Override public boolean isMatch(Method candidateMethod) { return (candidateMethod.getDeclaringClass() != Object.class && !BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) && BeanAnnotationHelper.isBeanAnnotated(candidateMethod)); }
三行代码,三个条件:
- 该方法不能是Object的方法(即使你Object的方法标注了@Bean,我也不认)
- 不能是setBeanFactory()方法。这很容易理解,它交给上个拦截器搞定即可
- 方法必须标注标注有@Bean注解
简而言之,标注有@Bean注解方法执行时会被拦截。
所以下面例子中的son()和parent()这两个,以及parent()里面调用的son()方法的执行它都会拦截(一共拦截3次)~
小细节:方法只要是个Method即可,无论是static方法还是普通方法,都会“参与”此判断逻辑哦