Spring核心——@Configuration与混合使用

@Configuration

在介绍Spring核心容器的系列文章中已经多次出现这个注解,从使用的角度来说可以把他理解为XML配置中的<beans>标签,但是两者肯定是不等价的。

在<beans>标签中除了使用<bean>声名Bean以外,还有各种<context>标签来扩展功能,比如<context:component-scan/>、<context:annotation-config />以及<import>等,这些扩展的功能并不是@Configuration注解的参数,而是通过另外一个注解来实现——@ComponentScan、@Import。

@Configuration的基本使用方法已经在纯Java运行与@Bean的“@Bean注解”部分介绍了使用方法,本篇在此基础上进一步进行说明。

@Configuration添加依赖

除了在纯Java运行与@Bean文中介绍的使用方法,我们还可以直接通过使用Java代码来添加依赖关系:

@ConfigurationpublicclassMyConfig{@BeanpublicAlicealice(){//直接使用方法注入数据。//从表面上看这里调用bob()并没有经过容器处理。而是直接使用了。returnnewAlice(bob());    }@BeanpublicBobbob(){returnnewBob();    }}

看到这里,思维敏捷的码友通过以下逻辑肯定就发现问题了:

通过@Bean注解是向容器添加一个BeanDefinition

在所有的BeanDefinition创建之后容器开始创建Bean之前会执行预设的后置处理器BeanFactoryPostProcessor

最后容器根据BeanDefinition的内容创建Bean。

 return new Alice(bob()); 这段代码中MyConfig::bob方法的调用看起来完全和容器无关,这样就违反了依赖注入的原则!

所以是不是Alice类中被注入的Bob实例根本就不是IoC容器中的Bob?

首先可以很负责的告诉码友们Spring并没有限制这个方式去添加Bean,所以例子中Alice类中的Bob实例就是IoC容器中的实例。即使是这样去注入Bean同样实现了依赖注入的功能。至于怎么解决的看完本文自然就能得到答案了。

@Component添加依赖

之前在Stereotype组件与Bean扫描这篇文章已经提到过,除了在@Configuration中的方法使用@Bean,还可以在@Component及其派生类中的方法使用@Bean。例如下面的例子:

packagechkui.springcore.example.javabase.configuration.bean;@ComponentpublicclassBeanManager{@BeanpublicCytuscytus(){returnnewCytus();}@BeanpublicDvadva(){returnnewDva();}@BeanpublicGamegame(Dva dva){returnnewGame(cytus(), dva);}}

BeanManager中的三个方法都会向容器添加Bean。注意第三个方法:public Game game(Dva dva)。这里即采用了通过方法参数注入依赖,也像前面的例子一样直接调用了方法。但是这里与前面介绍的使用@Configuration注解不同,Game中的Cytus实例不是IoC容器中的Cytus。

通过下面的例子来说明@Configuration和@Component中注入Bean的差异。

//package chkui.springcore.example.javabase.configuration;//使用@Configuration注解@ConfigurationclassConfig{@BeanpublicAlicealice(){returnnewAlice(bob());}@BeanpublicBobbob(){returnnewBob();}}//package chkui.springcore.example.javabase.configuration.bean;//使用@Component注解@ComponentpublicclassBeanManager{@BeanpublicCytuscytus(){returnnewCytus();}@BeanpublicDvadva(){returnnewDva();}@BeanpublicGamegame(Dva dva){returnnewGame(cytus(), dva);}}//运行publicclassConfigurationApp{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(Config.class, BeanManager.class);Bob bob = ctx.getBean(Bob.class);Alice alice = ctx.getBean(Alice.class);System.out.println("Bob instance of IoC hash: "+ bob.hashCode());System.out.println("Bob instance of Alice hash: "+ alice.getBob().hashCode());System.out.println("Compare:"+ (bob == alice.getBob()));System.out.println("Config instance:"+ ctx.getBean(Config.class));Game game = ctx.getBean(Game.class);Cytus cytus = ctx.getBean(Cytus.class);Dva dva = ctx.getBean(Dva.class);System.out.println("IoC Cytus: "+ cytus.hashCode());System.out.println("Game Cytus: "+ game.getCytus().hashCode());System.out.println("IoC Dva: "+ dva.hashCode());System.out.println("Game Dva: "+ game.getDva().hashCode());System.out.println("Cytus:"+ (cytus == game.getCytus()));System.out.println("Dva:"+ (dva == game.getDva()));System.out.println("BeanManager Instance:"+ ctx.getBean(BeanManager.class));}}

在最后的main方法中我们对容器中以及Alice、Game中包含的实例进行了hash以及实例对比,在我的电脑上输出结果如下:

1.Bob instance of IoChash: 12420275252.Bob instance of Alicehash: 12420275253.Compare:true4.Config instance:5.chkui.springcore.example.javabase.configuration.Config$$EnhancerBySpringCGLIB$$acdbeb32@74287ea36.IoC Cytus: 21049735027.Game Cytus: 7359374288.IoC Dva: 16042473169.Game Dva: 160424731610.Cytus:false11.Dva:true12.BeanManager Instance:chkui.springcore.example.javabase.configuration.bean.BeanManager@68746f22

例子中分别在@Configuration和@Component标记的类中使用@Bean来向容器添加Bean。最后通过输出实例的hash以及地址匹配(使用“==”比对)来确定是否都是同一个单例。

很明显IoC容器中的Cytus以Game中的Cytus并不是一个实例,其他都是同一个单例。仔细看看第4行和第12行的Config instanceBeanManager instance的输出内容就会得到答案。

BeanManager是一个常规的类,而在JVM中运行的Config是一个通过CGLIB实现的字节码级别的代理类(如果不知道CGLIB是什么就自己网上找找吧,这玩意在Java界已经红得发紫了)。Spring实际上是使用CGLIB为Config类添加了一个“代理壳”,当我们在任何地方直接调用@Configuration标注的类中的的方法时,代理壳都会将其整理为一个BeanDefinition的转换过程。

知道两者的差异后我们选择何种方式来添加Bean就很清晰了:

使用@Configuration能保证不会出现例子中Cytus这样的例外。也能清晰的明确@Configuration等价于一个<beans>统一管理。

而在@Component或其他组建中使用@Bean好处是不会启动CGLIB这种重量级工具(不过在Spring中即使这里不使用,其他很多地方也在使用)。并且@Component及其相关的Stereotype组件自身就有摸框级别的功能,在这里使用@Bean注解能很好的表明一个Bean的从属和结构关系,但是需要注意直接调用方法的“副作用”。

个人建议如果没什么特别的要求就使用@Configuration,引入CGLIB并不会影响多少性能,然而坑会少很多。在spring官网将用@Configuration创建的@Bean称呼为"Full"模式、将@Component创建的@Bean称呼为"'lite"模式,从字面上也能略知他们的差异。

多种方式混合使用

从XML配置到纯Java配置,Spring变得越来越简便好用,对应的功能也越来越多样化。如果对他的脉络没有清晰的认识,往往会陷入迷惑中。无论功能再复杂我们都要记住本系列文章开篇提到的IoC容器的初衷:

处理容器与Bean、Bean与Bean的关系。Bean是最小的工作单元,一切功能都是在Bean基础上扩展而来的。

所以无论是XML配置还是纯Java配置基本目标就是解决三个问题:向容器添加Bean,确定Bean的功能,确定Bean与Bean之间的依赖关系。

既然XML和纯Java配置都是解决同样的问题,那么混合使用当然没问题。比如在XML中配置了<context:component-scan/>,那么指定路径下的@Component以及派生注解(@Service、@Comfiguration等)都会被扫描并添加到容器中成为一个Bean。然后IoC容器会根据注解的类型来确定这个Bean是什么功能。

下面是一个使用AnnotationConfigApplicationContext启动容器混合使用Java配置与XML配置的例子。

首先我们使用AnnotationConfigApplicationContext启动IoC容器:

packagechkui.springcore.example.javabase.multiconfiguration;@Configuration@ComponentScans({@ComponentScan("chkui.springcore.example.javabase.multiconfiguration.config"),@ComponentScan("chkui.springcore.example.javabase.multiconfiguration.service") })publicclassMultiConfigurationApp{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(MultiConfigurationApp.class);}}

在Main方法中直接指定了当前的类,所以MultiConfigurationApp类会成为一个Bean。由于是一个Stereotype模式的@Configuration标记类(@Configuration继承自@Component,提供了配置相关的分层功能,关于Stereotype模式的内容相见Stereotype组件与Bean扫描),所以容器会用CGLIB来代理它实现配置相关的功能。@ComponentScans是一个辅助注解,他的作用就是整合多个@ComponentScan一起使用。

在config包中有2个@Configuration类:

packagechkui.springcore.example.javabase.multiconfiguration.config;@Configuration@Import({ClubConfiguration.class})@ImportResource("javabase/multiconfiguration/config.xml")publicclassMainConfiguration{}

packagechkui.springcore.example.javabase.multiconfiguration.config;publicclassClubConfiguration{@BeanpublicMilmil(){returnnewMil();}@BeanpublicMaumau(){returnnewMau();}}

MainConfiguration类被标记了@Configuration注解,所以他会被扫描并添加到容器中。

@Import注解的作用是引入其他类成为一个Bean,我们可以看到ClubConfiguration类并没有任何注解,但是他通过@Import注解在其他类添加到容器中。

而@ImportResource等价于XML配置中的<import>标签,作用就是引入一个XML配置文件。对应的XML文件如下:

这样XML配置中的2个类也会被添加到容器中。案例中对应的实体类如下:

packagechkui.springcore.example.javabase.multiconfiguration.bean;classMau{publicStringtoString(){return"Manchester United[MAU]";}}classCfc{publicStringtoString(){return"Chelsea Football Club[CFC]";}}classMil{publicStringtoString(){return"A.C Milan [MIL]";}}classJav{publicStringtoString(){return"Juventus [JAV]";}}

Conditionally

最后在使用@Configuration时可以使用Conditionally特性来确定是否添加Bean。大致用法就是实现Condition接口,然后通过@Conditional注解和@Bean绑定在一起进行条件判断。

实现Condition:

packagechkui.springcore.example.javabase.multiconfiguration.config;publicclassSoySauceConditionimplementsCondition{@Overridepublicbooleanmatches(ConditionContext context, AnnotatedTypeMetadata metadata){returnfalse;//返回false则不会对应的Bean。}}

然后使用@Conditional注解绑定到一个@Bean上:

packagechkui.springcore.example.javabase.multiconfiguration.config;publicclassClubConfiguration{@Bean@Conditional(SoySauceCondition.class)publicSoySaucesoySauce(){returnnewSoySauce();}}

这样,如果SoySauceCondition中的matches方法返回ture则添加SoySauce到IoC容器中,否则不会存在这个Bean。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容