Bean加载方式
本文适合已经有了Spring基础的读者来阅读,如果你对Spring有一定了解但又没那么了解,或者你想要去了解SpringBoot的自动配置原理,读完这篇文章之后一定能对你有所帮助
1 环境准备
按照以下步骤搭建一个Demo,方便后续演示使用:
1)新建一个maven项目,在pom文件中导入spring的依赖
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.9</version></dependency>
2)新建一个com.itheima.bean包,这个包用来存放一些后续演示中会使用的Bean。在包中创建以下类:
publicclassCat{}publicclassDog{}publicclassMouse{}
3)在com.itheima.bean中新建一个service包,包中创建以下接口:
publicinterfaceBookSerivce{voidcheck();}
4)在service包中创建一个impl包,在包中创建以下类
publicclassBookServiceImpl1implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 1..");}}publicclassBookServiceImpl2implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 2....");}}publicclassBookServiceImpl3implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 3......");}}publicclassBookServiceImpl4implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 4........");}}
这样基础环境就搭建好了,整体结构如下:
2 Bean加载方式之XML方式
2.1 配置自定义Bean
在resources目录下新建一个名为applicationContext1.xml的文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"></beans>
如果希望在spring容器中加载某个类的Bean,那么只需要在标签中添加一个标签,并按照以下格式配置类的信息即可:
<!--定义一个id为cat的Bean,Bean的类型为Cat--><beanid="cat"class="com.itheima.bean.Cat"/>
如果我们希望使用这个Bean,只需要从容器获取即可,具体如下:
在com.itheima包下创建一个app包,这个包专门用来演示获取Bean,在app包下新建一个类,内容如下:
publicclassApp1{publicstaticvoidmain(String[] args){//获取容器对象ApplicationContext ctx =newClassPathXmlApplicationContext("applicationContext1.xml");//获取BeanObject cat = ctx.getBean("cat");System.out.println(cat);}}
我们在xml中声明Bean时,也可以不指定类型,例如:
<!--定义一个id为cat的Bean,Bean的类型为Cat--><beanid="cat"class="com.itheima.bean.Cat"/><!--创建一个Dog的Bean,不指定id--><beanclass="com.itheima.bean.Dog"/>
那么我们在获取Bean的时候就只能根据类型去获取
publicclassApp1{publicstaticvoidmain(String[] args){ApplicationContext ctx =newClassPathXmlApplicationContext("applicationCOntext1.xml");Object cat = ctx.getBean("cat");System.out.println(cat);//获取Dog类型的BeanDog dog = ctx.getBean(Dog.class);System.out.println(dog);}}
如果spring容器中存在两个以上Dog类型的Bean,我们按类型获取就会出现异常,这时就只能给不同的DogBean定义id然后根据id获取了
我们还可以通过以下方法获取容器中我们手动添加的所有Bean的定义信息
publicclassApp1{publicstaticvoidmain(String[] args){ApplicationContext ctx =newClassPathXmlApplicationContext("applicationCOntext1.xml");// Object cat = ctx.getBean("cat");// System.out.println(cat);// Dog dog = ctx.getBean(Dog.class);// System.out.println(dog);//获取所有Bean的DefinitionName,即定义名String[] names = ctx.getBeanDefinitionNames();//循环打印for(String name : names){System.out.println(name);}}}
打印信息如下:
当我们在定义Bean的时候,如果指定了id,那么Bean的DefinitionName就是我们指定的id,如果我们没有指定id,那么DefinitionName默认就是
类的全路径名#数字编号
,如果该类型的Bean是容器中第一个,那个数字编号就为0,第二个则为1,以此类推。
我们通过id获取Bean的方式实际上是通过DefinitionName来获取Bean,也就是说,如果我们希望获取Dog的Bean,也可以这样写:
publicclassApp1{publicstaticvoidmain(String[] args){ApplicationContext ctx =newClassPathXmlApplicationContext("applicationCOntext1.xml");//通过Bean的默认定义名来获取BeanObject cat = ctx.getBean("com.itheima.bean.Dog#0");System.out.println(cat);}}
虽然我并不认为有人会这样去获取
2.2 配置第三方Bean
除了配置我们自己定义的Bean之外,也可以在xml中配置一些第三方的Bean,例如数据库连接池等等,演示如下:
1)在pom文件中引入druid的依赖
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.16</version></dependency>
2)在xml文件中配置druid的Bean(这里只是演示一下,数据源啥的就不配了)
<beanid="dataSource"class="com.alibaba.druid.pool.DruidDataSource"/>
3)从容器中进行获取:
publicclassApp1{publicstaticvoidmain(String[] args){ApplicationContext ctx =newClassPathXmlApplicationContext("applicationCOntext1.xml");// Object cat = ctx.getBean("cat");// System.out.println(cat);// Dog dog = ctx.getBean(Dog.class);// System.out.println(dog);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}}}
打印结果如下:
如果我们没有指定id,也会按照
类的全路径名#数字编号
的格式为第三方Bean配置一个默认的DefinitionName
3 Bean加载方式之注解方式
3.1 配置自定义Bean
注解方式是为了简化xml方式的,如果我们需要将一个类加载到spring容器中,只需要在类上打上一个注解即可:
//将Cat类的一个Bean加载到容器中,Bean的名称为tom@Component("tom")publicclassCat{}
@Component
有三个衍生注解,分别是
@Controller
、
@Service
、
@Repository
,分别对应着Controller层,Service层和Dao层的Bean,这三个注解作用和
@Component
基本一致,更多的是起一个区分和标志作用
除此之外,我们还需要在xml中配置spring的包扫描,用来扫描对应包下的注解,并将包中打上注解的类加载到spring容器中
新建一个配置文件,名为applicationContext2.xml,内容如下:
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
"><!--配置包扫描--><context:component-scanbase-package="com.itheima.bean"/></beans>
如果要获取容器中的Bean,只需要采用相同的方式即可,这里为了不干扰,我们在app包下新建一个类来演示
publicclassApp2{publicstaticvoidmain(String[] args){ApplicationContext ctx =newClassPathXmlApplicationContext("applicationCOntext2.xml");String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}}}
打印结果如下:
我们发现除了tom以外,还有许多其他的Bean,这些是Spring底层的一些后处理器,不在本文的讲解范围内,大家忽略即可
3.2 配置第三方Bean
如果我们使用注解方式来加载Bean,那么第三方的Bean又该如何去加载呢?Spring为我们提供了@Bean注解来解决这个问题
1)新建一个config包,这个包专门用来放一些配置类,在包中新建一个类如下:
//表示当前类为spring的一个配置类,会被spring进行解析和加载@ConfigurationpublicclassDbConfig{}
2)在applicationContext2.xml的包扫描配置中加入对于config包的扫描
<context:component-scanbase-package="com.itheima.bean,com.itheima.config"/>
3)在DbConfig中配置第三方Bean,这里还是以druid为例:
@ConfigurationpublicclassDbConfig{//将方法的返回值加载到spring容器中,Bean的名字为方法名@BeanpublicDruidDataSourcedataSource(){DruidDataSource ds =newDruidDataSource();return ds;}}
@Bean是打在方法上的一个注解,它会将方法的返回值作为spring的一个Bean加载到spring容器中
4)再次启动App2进行测试:
publicclassApp2{publicstaticvoidmain(String[] args){ApplicationContext ctx =newClassPathXmlApplicationContext("applicationCOntext2.xml");String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}}}
运行结果如下:
跟之前运行的结果相比,容器中多出来了两个Bean,一个是dataSource,一个是dbConfig,这说明打上@Configuration的类也会作为spring的一个Bean被加载到容器中,实际上,@Configuration本身就打上了@Component注解,我们可以点开源码看看:
同时我们还发现了一个现象:如果一个类的Bean是通过注解形式被加载到容器中的,那么Bean默认的DefinitionName将不再是
类的全路径名#数字编号
的形式,而是类名小写。
3.3 全注解配置
将Bean使用注解方式之后,我们的配置文件中就只剩下包扫描的配置了,接下来我们需要做的就是使用配置类去代替配置文件。
1)在config包下新建一个类,如下:
//包扫描注解@ComponentScan({"com.itheima.bean","com.itheima.config"})publicclassSpringConfig3{}
加上这个类之后,配合上之前使用的注解,我们就可以完全废除掉xml文件了。有些读者可能会有疑惑,类上不用打
@Configuration
吗?其实是不用的,继续往下看
2)在app包中新建一个App3,编写代码如下:
publicclassApp3{publicstaticvoidmain(String[] args){//更换使用的容器,加载配置类ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig3.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}}}
打印结果如下
我们发现,仅仅只是在SpringConfig3上@ComponentScan注解,这个类的Bean就也被加载到容器中了,什么原因呢?难道@ComponentScan也使用了@Component注解吗?其实不是,关键在于我们使用的容器
AnnotationConfigApplicationContext
,这个容器本身就会将传入的配置类加载到容器中,无需我们再手动打上注解进行加载。
4 Bean加载方式之FactoryBean
如果你做过ssm的整合,那么你对以下代码应该不陌生
@BeanpublicSqlSessionFactoryBeansqlSessionFactoryBean(DataSource dataSource){SqlSessionFactoryBean sqlSessionFactoryBean =newSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.itheima.bean");return sqlSessionFactoryBean;}
这里也是用@Bean注解将第三方类的Bean加载到容器中,但是我们发现方法的返回值类型是SqlSessionFactoryBean,最后加载到容器中Bean类型却是SqlSessionFactory,这是怎么回事呢?
FactoryBean是一个工厂Bean,而我们都知道,工厂的作用就是拿来造对象的,而SqlSessionFactoryBean这个工厂造的对象就是SqlSessionFactory,那么Spring是怎么知道它是一个工厂Bean,又怎么知道它要造的是哪一个类型的Bean呢?这里我们可以模拟一下
1)在bean包下新建两个类,分别为:
publicclassMySqlSessionFactory{}publicclassMySqlSessionFactoryBean{}
2)让MySqlSessionFactoryBean实现接口FactoryBean,并重写抽象方法:
publicclassMySqlSessionFactoryBeanimplementsFactoryBean<MySqlSessionFactory>{@OverridepublicMySqlSessionFactorygetObject()throwsException{returnnewMySqlSessionFactory();}@OverridepublicClass<?>getObjectType(){returnMySqlSessionFactory.class;}@OverridepublicbooleanisSingleton(){returnFactoryBean.super.isSingleton();}}
这里我们实现了spring提供的一个接口:FactoryBean,看到这大家应该明白了,spring判断一个类是否为FactoryBean就是看这个类有没有实现FactoryBean接口而已,当我们实现FactoryBean时,需要指定泛型,泛型类型就是你这个FactoryBean需要生产的对象的类型
FactoryBean有三个方法需要我们重写:
getObject()
:该方法的返回值会作为FactoryBean所生产的对象被加载到容器中getObjectType()
:该FactoryBean所生产的对象的类型,可以和实现FactoryBean接口时指定的泛型保持一致,或者使用该泛型的子类或实现类isSingleton()
:工厂所生产的对象是否为单例,意思就是你这个工厂是只能生产一个对象还是可以生产多个对象,默认为单例
3)将MySqlSessionFactoryBean配置到配置类中:
@ComponentScan({"com.itheima.bean","com.itheima.config"})publicclassSpringConfig3{@BeanpublicMySqlSessionFactoryBeanmysqlSessionFactory(){returnnewMySqlSessionFactoryBean();}}
4)启动App3,进行测试,这里我们方便观察,就不再打印全部了:
publicclassApp3{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig3.class);// String[] names = ctx.getBeanDefinitionNames();// for (String name : names) {// System.out.println(name);// }System.out.println(ctx.getBean(MySqlSessionFactory.class));}}
测试结果如下:
很显然,MySqlSessionFactory已经被加载到容器中了。
那么为什么会有这种方式呢?我@Bean直接返回MySqlSessionFactory不香吗。实际上这种方式一般都是一些第三方类整合spring时使用的,例如Mybatis,我们再次回归以下代码:
@BeanpublicSqlSessionFactoryBeansqlSessionFactoryBean(DataSource dataSource){SqlSessionFactoryBean sqlSessionFactoryBean =newSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.itheima.bean");return sqlSessionFactoryBean;}
这里我们在配置时只指定了dataSource和需要设置别名的包,SqlSessionFactoryBean就帮我们造出一个SqlSessionFactory了,实际上造一个SqlSessionFactory需要的远远不止这些信息,我们之所以仅配置这两项信息就能得到一个可以正常使用的SqlSessionFactory,就是因为SqlSessionFactoryBean都已经帮我们把其他配置写好了,这也是框架的一个封装作用。
5 注解方式加载XML的Bean
假如现在我有一个系统需要进行二次开发,如果原有的系统使用的是xml声明Bean,但是二次开发我们又希望使用注解方式配置Bean,那该怎么做呢?把配置文件全部推到重来大刀阔斧的修改原有的代码吗?我想说的是可以,但没必要,spring为我们提供了更加优雅的解决方式。
先来看看我们之前写的applicationContext1.xml,内容如下:
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--xml方式声明自己开发的bean--><beanid="cat"class="com.itheima.bean.Cat"/><beanclass="com.itheima.bean.Dog"/><!--xml方式声明第三方开发的bean--><beanid="dataSource"class="com.alibaba.druid.pool.DruidDataSource"/></beans>
如果我们想让这个配置文件中的三个Bean通过注解方式也能使用,其实做起来很简单,只需要新建一个配置类,打上一个注解即可
在config包下新建一个配置类:
@ImportResource("applicationContext1.xml")publicclassSpringConfig32{}
在app包下新建一个类进行测试:
publicclassApp32{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig32.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}}}
测试结果如下:
我们发现这些Bean就已经顺利的被加载到容器中了
这里有一个问题需要注意,如果我们同时导入了多个配置文件,而这些配置文件中出现了一些同名的Bean,那么后导入的配置文件中的Bean会覆盖先导入的配置文件中的Bean。
6 proxyBeanMethod
之前我们说过,@Configuration上打上了@Component注解,那这两个注解的作用是否一样呢?实际上,二者最主要的区别在于,@Configuration比@Component多了一个名为proxyBeanMethod的属性。那这个属性有什么作用呢?
首先我们再在config包下新建一个配置类,并打上@Configuration注解
@ConfigurationpublicclassSpringConfig33{}
在app包下新建一个类名为App33,代码如下:
publicclassApp33{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig33.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}System.out.println("-------------------------");//打印springConfig33的类型System.out.println(ctx.getBean("springConfig33"));}}
运行结果如下:
横线之上的内容我们并不陌生,之前已经见过了,横线下面的内容是我们希望打印的springConfig33的类型,显然这个结果有点出乎意料,这个Bean的类型并不是我们想象的
com.itheima.config.SpringConfig33
,而是SpringConfig33的一个代理对象,那为什么我们需要知道这个东西呢?
我们修改一下SpringConfig33类,将@Configuration的proxyBeanMethods属性改为false(默认为true)
@Configuration(proxyBeanMethods =false)publicclassSpringConfig33{}
再重新启动一下App33,运行结果就发生了变化
这个时候,springConfig33就不再是一个代理对象了,而是确确实实的SpringConfig33类的一个对象了,那么这个又有什么作用呢
我们可以在SpringConfig33定义一个Bean,不管定义什么都可以:
@Configuration(proxyBeanMethods =false)publicclassSpringConfig33{@BeanpublicCatcat(){returnnewCat();}}
修改一下App33类,直接通过cat()方法来获取cat对象
publicclassApp33{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig33.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}System.out.println("-------------------------");SpringConfig33 springConfig33 = ctx.getBean("springConfig33",SpringConfig33.class);System.out.println(springConfig33.cat());System.out.println(springConfig33.cat());System.out.println(springConfig33.cat());}}
打印结果如下:
我们发现,每次调用cat()方法得到的cat都是不一样的,有些读者可能就会说,你这不是废话吗,你cat()方法return一个new Cat(),那对象能一样就有鬼了,欸,别急,我们不妨将SpringConfig33的proxyBeanMethods属性再修改为ture看看
@Configuration(proxyBeanMethods = ture)publicclassSpringConfig33{@BeanpublicCatcat(){returnnewCat();}}
再回到App33类中重新运行一下:
我们的发现,此时我们调用cat()方法得到的对象变成了同一个。神奇吗?还有更神奇的,此时容器中是有一个我们通过@Bean方式加载到容器中的Cat对象的,我们不妨看看这个对象的地址
publicclassApp33{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig33.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}System.out.println("-------------------------");SpringConfig33 springConfig33 = ctx.getBean("springConfig33",SpringConfig33.class);System.out.println(springConfig33.cat());System.out.println(springConfig33.cat());System.out.println(springConfig33.cat());System.out.println("-------------------------");//打印容器中Cat类型的Bean的地址System.out.println(ctx.getBean("cat"));}}
结果相信大家应该都已经猜到了,这四个地址都为同一个地址
为什么会这样呢?我们通过cat()方法每次返回的都应该是一个新的cat对象才对啊,其实这就是SpringConfig33代理对象所做的事,我们如果将proxyBeanMethods属性设置为ture了(不用设置,默认也就是true),容器中加载的对象就不再是SpringConfig33的对象了,而是SpringConfig33的代理对象,那么这个代理对象做了什么事呢?其实它就是对所有打上@Bean的方法的返回值进行了一个控制,如果容器中不存在返回值类型的Bean,则
return new Cat()
会正常创建一个cat类的对象,而如果容器中已经存在了返回值类型的Bean,那么返回值会被强制修改成容器中的Bean,这样刚好印证了我们上面的实验结果。
还需要注意一点,我们调用cat()方法时,使用的SpringConfig33对象是从容器中取出来的
SpringConfig33 springConfig33 = ctx.getBean("springConfig33",SpringConfig33.class);
如果这个对象是我们自己new的而不是从容器中拿的,或者springConfig33是一个Bean但是它不是单例的,那么上述理论就不适用了,原因很简单,无论是我们自己new的springConfig33还是多例的springConfig33,它本身就已经不属于spring容器管理的范畴了,spring自然也不会陪你玩下去了。
上面这样设计最主要的目的还是为了确保Bean的单例性,可能有很多读者认为这种设计没什么意义,单例多例有那么重要吗?实际上你如果去读了SpringBoot的源码,你就会发现上述设计是必不可少的。
7 Bean加载方式之Import导入
7.1 导入普通类
我们可以在配置类上通过@Import注解的方式,将指定类加载到spring容器中,具体演示如下:
在config包下新建一个配置类,在配置类上打上注解@Import,并指定值为Dog类
//@Import的值为数组,可以指定多个类型,这些类型的bean最终都会被加载到容器中@Import(Dog.class)publicclassSpringConfig5{}
在app包下新建一个App5,来打印spring容器中的对象
publicclassApp5{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig5.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}}}
打印结果如下:
我们发现Dog类的Bean已经被加载到容器中了,不过这个Bean的getBeanDefinitionName似乎有点奇怪,它是一个全类名,这个点我们先不用管,我们只需要知道Dog类现在确实已经被加载到容器中了
那这种方式加载Bean有什么好处呢?我不用这种方式,直接在Dog类上打上一个@Component不是一样的吗,最终效果确实是一样的,但是使用这种方式加载可以做到对代码无侵入,当我们需要加载一个Bean的时候,我们只需要通过配置类的@Import注解指定Bean的类型即可,无需对Bean所属的类本身进行修改(即打上注解),有点回归spring最初的xml形式的设计理念,利用这点,我们也可以通过@Import注解导入那些无法修改源码的第三方类的Bean,而不需要再使用@Bean的方式,当然如果我们使用的第三方Bean需要做初始化的话那就只能通过@Bean的方式得到了。
归纳上述优点,其实就是两个字:解耦,正是因为这一特性,在spring技术底层以及诸多框架的整合中,@Import被大量使用到。
7.2 导入配置类
其实对于@Import的使用,大家更熟悉的应该是另外一种,也就是使用@Import注解导入其他配置类
这里我们仍然使用配置类SpringConfig5,通过@Impor导入另一个配置类,这里就选择导入之前使用过的DbConfig
@ConfigurationpublicclassDbConfig{@BeanpublicDruidDataSourcedataSource(){DruidDataSource ds =newDruidDataSource();return ds;}}
@Import({Dog.class,DbConfig.class})publicclassSpringConfig5{}
再使用App5运行一下,观察打印结果
publicclassApp5{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig5.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}}}
观察运行结果可知,即便我们指定的类是配置类,@Import注解也能将其加载到容器中,而既然配置类都已经被加载到容器中了,那么配置类中打上了@Bean的方法所产生的Bean对象自然也就被加载到容器中了。
另外相信大家也观察到了,通过@Import方式加载的Bean在容器中的DefinitionName都是全路径名,这个点现在我们无需过多在意
再讲点大家可能不知道的,如果我们导入的配置类DbConfig上没有加@Configuration,那这个类还能被加载到容器中吗,这个相信难不倒大家,结果肯定是会被加载到容器中的,那大家不妨再思考一下,如果去掉了@Configuration注解,那么我们在DbConfig中使用@Bean获取的dateSource还会被加载到容器中吗?我们可以动手尝试一下:
修改DbConfig,将@Configuration注解注释掉
//@ConfigurationpublicclassDbConfig{@BeanpublicDruidDataSourcedataSource(){DruidDataSource ds =newDruidDataSource();return ds;}}
再次运行App5,结果是仍然会被加载到容器中
这是不是说明以后我们就不需要打@Configuration注解了呢?比如说我们可以将所有的配置类都交给一个公共配置类来导入,在获取容器的时候指定这个公共配置类来获取容器,那岂不是一个@Configuration都不用打了?
乍一看这种观点似乎好像也没问题,但是大家别忘了@Configuration可是有个名为proxyBeanMethod的属性,多的就不说了,总之在每个配置类上都打上一个@Configuration注解才是最规范的写法,也是最不容易出问题的写法
8 Bean加载方式之registerBean
我们可以在拿到容器后,再手动往容器中注册Bean,演示如下
在config包中新建一个SpringConfig6,在app包中新建一个App6,内容分别如下:
publicclassSpringConfig6{}
publicclassApp6{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig6.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}System.out.println("----------------------");}}
此时运行App6,打印结果如下
很显然目前容器中除了配置类和Spring一些处理器的Bean以外,容器中是什么都没有的
接下来让我们尝试在获取容器后,手动在容器中去注册Bean,代码如下:
publicclassApp6{publicstaticvoidmain(String[] args){//这里要将使用的容器更换为AnnotationConfigApplicationContext,因为registerBean是该容器特有的功能AnnotationConfigApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig6.class);//在容器中注册一个名为tom,类型为Cat的类
ctx.registerBean("tom",Cat.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}System.out.println("----------------------");}}
运行结果如下:
一个名为tom的Bean就这样被加载到容器中了。
看到这里,相信大家的第一反应是:切,这有什么用,还不如我自己在类上手动打注解香。首先我肯定你的想法,我想说确实是这样,但是这是从一个框架使用者的角度说的,但是如果你是一个框架的编写者,你希望将你的框架与spring做一个整合,那么这种方式还是可能会被用到的
这里关于registerBean方法的使用,我们可以再拓展一下,如果我们希望在创建Bean时为这个Bean的一些属性赋值,那又该怎么去做?
实际上这里的registerBean方法还有第三个参数,这个参数是一个可变参数,对应的是Bean的构造器参数。为了方便讲解,我们可以来演示一下:
首先修改以下Cat类信息,新增一个属性age,并提供构造器,为了方便后面打印,我们再加一个toString方法
//@Component("tom")publicclassCat{int age;//注意这里的无参构造器一定要添加publicCat(){}publicCat(int age){this.age = age;}@OverridepublicStringtoString(){return"Cat{"+"age="+ age +'}';}}
再回到App6,修改代码如下:
publicclassApp6{publicstaticvoidmain(String[] args){AnnotationConfigApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig6.class);//注册一个名为tom,类型为Cat的Bean,构造器参数传入1
ctx.registerBean("tom",Cat.class,1);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}System.out.println("----------------------");//打印tom的信息System.out.println(ctx.getBean("tom"));}}
测试结果如下,可以看到我们已经为tom的属性age设置上了值。
注意:registerBean的第三个可变参数列表一定要与Bean所属类的某个构造器参数列表对应上,不然就会直接报错。
这里我们再思考一下,如果我们先后注册两个相同名称,且类型相同的Bean(需要明确的是这两个Bean一定只会在容器中保留一个),那么是先注册的Bean最终保留在容器中,还是后注册的覆盖了先注册的Bean呢?
揭晓答案,其实Spring容器说到底就是一个Map集合,既然是Map集合,那就肯定是后添加的覆盖先添加的。以后碰到类似于这样的问题时,大家可以通过这个方向去思考。
最后再说一下,如果我们仅仅只是希望往容器中注册一个Bean,那么我们还可以用最简单的方式,例如:
ctx.registerBean(Cat.class);
这样就会往容器中注册一个Cat类型的Bean,Bean默认的定义名为类名小写,这里不再演示,大家可以自行尝试一下
9 Bean加载方式之ImportSelector
我们可以通过ImportSelector的方式,对Bean的加载进行条件控制。这种方式在我们日常开发中基本上不会使用到,但是在SpringBoot的源码中大量被使用到了,如果你想要去阅读SpringBoot的源码,那么这种方式就必须弄明白
首先我们在config包下新建一个类,名为MyImportSelector,这个类需要实现接口ImportSelector,并重写接口中一个名为selectImports的方法,具体如下:
publicclassMyImportSelectorimplementsImportSelector{@OverridepublicString[]selectImports(AnnotationMetadata annotationMetadata){returnnewString[0];}}
我们修改一下方法的返回值,返回值为我们需要加载到容器中的Bean的全限定名
@OverridepublicString[]selectImports(AnnotationMetadata annotationMetadata){//将Dog类和Cat类的一个Bean加载到容器中returnnewString[]{"com.itheima.bean.Dog","com.itheima.bean.cat"};}
在config包下新建一个配置类,名为SpringConfig7,并在配置类上导入MyImportSelector类
@Import(MyImportSelector.class)publicclassSpringConfig7{}
在app包下新建一个App7,内容如下:
publicclassApp7{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig7.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}System.out.println("----------------------");}}
运行代码,打印结果如下:
可以看到,此时Dog和Cat两个对象的Bean已经被加载到容器中了。
难道这种用法就这吗?接下来就让你看看高级的用法
我们的目光重新回到MyImportSelector中
publicclassMyImportSelectorimplementsImportSelector{@OverridepublicString[]selectImports(AnnotationMetadata annotationMetadata){returnnewString[]{"com.itheima.bean.Dog","com.itheima.bean.cat"};}}
我们发现selectImports方法有一个参数我们并未使用到,也就是annotationMetadata,这个参数有什么用呢?这里就不卖关子了,哪个类导入了MyImportSelector,那么annotationMetadata就能拿到关于这个类的基本上所有信息。
为了方便演示,我们这里先修改一下SpringConfig7,在类上打上注解@Configuration,并指定proxyBeanMethods为false
@Import(MyImportSelector.class)@Configuration(proxyBeanMethods =false)publicclassSpringConfig7{}
接下来我们就可以通过annotationMetadata拿到关于SpringConfig7的信息来看看,当然这里只演示annotationMetadata的一小部分方法,除了以下信息以外我们能拿到的还有很多很多
@OverridepublicString[]selectImports(AnnotationMetadata annotationMetadata){//查看全类名信息System.out.println("全类名信息:"+annotationMetadata.getClassName());//查看是否打上了注解@ConfigurationSystem.out.println("是否打上了注解@Configuration:"+
annotationMetadata.hasAnnotation("org.springframework.context.annotation.Configuration"));//查看@Configuration里的属性信息System.out.println("查看@Configuration的属性值:"+
annotationMetadata.getAnnotationAttributes("org.springframework.context.annotation.Configuration"));System.out.println("================");returnnewString[]{"com.itheima.bean.Dog","com.itheima.bean.cat"};}
我们可以再次通过启动App7来查看,代码无需做任何修改
这些信息我们就已经获取到了,那么获取到这些信息有什么作用呢?我们想想,既然最终加载到容器中的Bean是通过selectImports方法的返回值来确定的,那我们是不是就可以在selectImports中做一些条件判断,根据不同的条件返回不同的Bean到容器中呢?
例如,如果SpringConfig7上添加了@Configuration注解,我们就加载Cat类到容器中,如果没有添加@Configuration注解,那我们就加载Dog类到容器中,代码编写如下:
publicString[]selectImports(AnnotationMetadata annotationMetadata){// 根据条件进行判定,判定完毕后,决定是否装在指定的beanboolean flag = annotationMetadata.hasAnnotation("org.springframework.context.annotation.Configuration");if(flag){returnnewString[]{"com.itheima.bean.Cat"};}returnnewString[]{"com.itheima.bean.Dog"};}
这时我们的SpringConfig7上是打上了@Configuration注解的,我们再运行App7看看结果
结果正如我们所料,Cat类的Bean被加载到容器中了,而Dog类的Bean却没有
正式因为ImportSelector可以根据不同的条件对加载的Bean进行控制的特性,所以在源码中大量使用到了它
10 Bean加载方式之ImportBeanDefinitionRegistrar
10.1 加载Bean
这种方式是比ImportSelector更加高端的加载方式,话不多说,直接上代码
首先我们在config目录下创建一个类,名为MyRegistrar,然后实现ImportBeanDefinitionRegistrar接口
publicclassMyRegistrarimplementsImportBeanDefinitionRegistrar{}
ImportBeanDefinitionRegistrar并没有抽象方法,我们需要手动选择其中一个方法进行覆盖
这两个方法其实选择哪个差别不大,这里我们选择第二个方法,也就是registerBeanDefinitions方法进行覆盖:
publicclassMyRegistrarimplementsImportBeanDefinitionRegistrar{@OverridepublicvoidregisterBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry
){}}
registerBeanDefinitions方法的第一个参数和我们在上一种加载方式中使用过的annotationMetadata的作用是一样的,因此在这里我就不再过多介绍了,我们重点看第二个参数。
第二个参数的类型是BeanDefinitionRegistry,光看名字我们知道了这是一个关于BeanDefinition的注册表,那么什么是BeanDefinition呢?这个其实涉及到spring比较底层的东西了,BeanDefinition实际上记录的是关于一个Bean的一系列信息,它包括但不限于:
- Bean 的类名
- 设置父 bean 名称、是否为 primary、
- Bean 行为配置信息,作用域、自动绑定模式、生命周期回调、延迟加载、初始方法、销毁方法等
- Bean 之间的依赖设置,dependencies
- 构造参数、属性设置
BeanDefinition与Bean其实有点像类与对象的关系,BeanDefinition定义了一个Bean的基本结构。
ImportBeanDefinitionRegistrar比ImportSelector高端的地方就在于,它不仅能根据条件来控制需要加载到容器中的类型(通过第一个参数importingClassMetadata,使用方法与上一种加载方式中的参数annotationMetadata完全一致),它还能直接在方法中对一个Bean的BeanDefinition进行修改,然后在方法内把这个Bean加载到容器中。通过这种方式,我们可以无需修改任何代码,就可以完成对一个Bean的覆盖。
关于后者的使用方式如下:
publicclassMyRegistrarimplementsImportBeanDefinitionRegistrar{@OverridepublicvoidregisterBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry
){//构建一个beanDefinition,类型指定为Dog,构建beanDefinition的方式并不唯一,这种方式仅供参考BeanDefinition beanDefinition =BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();//这里我们拿到BeanDefinition后还可以对其进行一系列设置,这里不再演示//根据指定的beanDefinition,生产一个名为"wangcai"的bean并将其加载到容器中
registry.registerBeanDefinition("wangcai",beanDefinition);}}
在config包中新建一个配置类,名为SpringConfig8,并导入MyRegistrar
@Import(MyRegistrar.class)publicclassSpringConfig8{}
在app包中新建一个App8,代码还是老样子
publicclassApp8{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig8.class);String[] names = ctx.getBeanDefinitionNames();for(String name : names){System.out.println(name);}}}
观察打印结果,发现已经有一个名为wangcai的Bean被加载到容器中了
10.2 覆盖Bean
通过ImportBeanDefinitionRegistrar,我们可以在无需修改现有代码的基础上完成对一个Bean的覆盖,接下来我们来演示一下:
这里需要用到咱们之前已经定义了,但还没有用使用过的几个类,如下:
publicinterfaceBookSerivce{voidcheck();}publicclassBookServiceImpl1implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 1..");}}publicclassBookServiceImpl2implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 2....");}}publicclassBookServiceImpl3implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 3......");}}publicclassBookServiceImpl4implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 4........");}}
1)在BookServiceImpl1上打上@Service注解,并将Bean命名为bookService
@Service("bookService")publicclassBookServiceImpl1implementsBookSerivce{@Overridepublicvoidcheck(){System.out.println("book service 1..");}}
2)将MyRegistrar原有的内容清空
publicclassMyRegistrarimplementsImportBeanDefinitionRegistrar{@OverridepublicvoidregisterBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry){}}
3)在SpringConfig8中添加对BookServiceImpl1.class的导入
@Import({MyRegistrar.class,BookServiceImpl1.class})publicclassSpringConfig8{}
4)修改App8,改为从容器中获取一个名为bookService的Bean,并调用这个Bean得check方法
publicclassApp8{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig8.class);BookSerivce bookService = ctx.getBean("bookService",BookSerivce.class);
bookService.check();}}
打印结果如下:
打印的结果如我们所料。
那么如果我们现在要对名为bookService这个Bean进行覆盖,我们希望底层使用的实现类是BookServiceImpl2而不是BookServiceImpl1,该怎么实现呢?这里我们只需要在MyRegistrar中添加两行代码即可,其他所有代码都不用动:
publicclassMyRegistrarimplementsImportBeanDefinitionRegistrar{@OverridepublicvoidregisterBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry){//构建beanDefinition,将类型定义为BookServiceImpl2BeanDefinition beanDefinition =BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();//对名为bookService的Bean的BeanDefinition进行覆盖
registry.registerBeanDefinition("bookService",beanDefinition);}}
直接再次启动App8,发现打印的结果已经变成了BookServiceImpl2的check方法所输出的内容
通过ImportBeanDefinitionRegistrar对Bean的加载是在容器启动之后的,这一点其实与之前介绍的registerBean的方式比较类似,但是ImportBeanDefinitionRegistrar相对来说功能更强大,因为这种方式是能直接对BeanDefinition进行修改的,同时又由于其加载Bean在容器启动之后,因此它是能够确保在容器中已经存在同名Bean的时候对该Bean进行覆盖。
而且这种方式覆盖Bean对原有代码的改动极小,我们只需要在MyRegistrar中添加两行代码即可完成覆盖,原有代码不需要做任何改动。
如果我们在配置类中导入了多个ImportBeanDefinitionRegistrar,例如:
@Import({MyRegistrar.class,MyRegistrar1.class,MyRegistrar2.class})publicclassSpringConfig8{}
老规矩,后导入的覆盖先导入的。
11 Bean加载方式之BeanDefinitionRegistryPostProcessor
BeanDefinitionRegistryPostProcessor的作用其实跟ImportBeanDefinitionRegistrar作用是比较相似的,我们可以来演示一下:
定义一个名为MyPostProcessor的类,然后实现BeanDefinitionRegistryPostProcessor接口,类中有两个抽象方法需要实现
publicclassMyPostProcessorimplementsBeanDefinitionRegistryPostProcessor{@OverridepublicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)throwsBeansException{}@OverridepublicvoidpostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)throwsBeansException{}}
第二个方法我们先不管,我们仔细看第一个方法的参数:BeanDefinitionRegistry,怎么样,是不是有点相似?这个参数的作用其实就跟ImportBeanDefinitionRegistrar中注册BeanDefinition的参数的作用一模一样,到这里其实大家应该也就知道该怎么使用了,直接把之前使用的代码搬过来不就可以了嘛
publicclassMyPostProcessorimplementsBeanDefinitionRegistryPostProcessor{@OverridepublicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)throwsBeansException{//构建beanDefinition,将类型定义为BookServiceImpl4BeanDefinition beanDefinition =BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();//对名为bookService的Bean的BeanDefinition进行覆盖
registry.registerBeanDefinition("bookService",beanDefinition);}@OverridepublicvoidpostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)throwsBeansException{}}
这里再新建一个SpringConfig9和App9,内容分别如下:
@Import({MyPostProcessor.class})publicclassSpringConfig9{}publicclassApp9{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringConfig9.class);BookSerivce bookService = ctx.getBean("bookService",BookSerivce.class);
bookService.check();}}
运行App9,打印结果如下:
那么这种方式有什么意义呢?
这种方式的意义在于,它对Bean的覆盖是一定是最后一个完成的,如果说ImportBeanDefinitionRegistrar可以完成对容器中已有的Bean进行覆盖,那么这种方式就可以对ImportBeanDefinitionRegistrar注册的Bean再次进行覆盖,并且这个覆盖一定是最后进行的。
如果你不信,我们可以来试试,测试的方式很简单,在SpringConfig9中同时导入
MyRegistrar.class
、
BookServiceImpl1.class
和
MyPostProcessor.class
,再运行一下App9,看看最终打印结果是哪一个就可以了
@Import({BookServiceImpl1.class,MyPostProcessor.class,MyRegistrar.class})publicclassSpringConfig9{}
结果已经很明显了,最终生效的就是我们通过BeanDefinitionRegistryPostProcessor注册的Bean
Bean常见的加载方式了解到这里,如果你的目标是研究SpringBoot的自动配置原理,那么了解这些就已经够用了,当然spring底层还有许多其他的加载方式,这里笔者水平有限,大家感兴趣可以自行去了解一下
版权归原作者 左右盲 所有, 如有侵权,请联系我们删除。