Spring Bean简介
简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
1 | <!-- Constructor-arg with 'value' attribute --> |
下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。
org.springframework.beans
和 org.springframework.context
这两个包是 IoC 实现的基础。
一、bean与spring容器的关系
Bean配置信息定义了Bean的实现及依赖关系,Spring容器根据各种形式的Bean配置信息在容器内部建立Bean定义注册表,然后根据注册表加载、实例化Bean,并建立Bean和Bean的依赖关系,最后将这些准备就绪的Bean放到Bean缓存池中,以供外层的应用程序进行调用。
二、Spring注解
从广义上Spring注解可以分为两类:
一类注解是用于注册Bean:即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean;
假如IOC容器就是一间空屋子,首先这间空屋子啥都没有,我们要吃大餐,我们就要从外部搬运食材和餐具进来。这里把某一样食材或者某一样餐具搬进空屋子的操作就相当于每个注册Bean的注解作用类似。注册Bean的注解作用就是往IOC容器中放(注册)东西! 用于注册Bean的注解: 比如@Component , @Repository , @ Controller , @Service , @Configration这些注解就是用于注册Bean,放进IOC容器中,一来交给spring管理方便解耦,二来还可以进行二次使用,啥是二次使用呢?这里的二次使用可以理解为:在你开始从外部搬运食材和餐具进空屋子的时候,一次性搬运了猪肉、羊肉、铁勺、筷子四样东西,这个时候你要开始吃大餐,首先你吃东西的时候肯定要用筷子或者铁勺,别说你手抓,只要你需要,你就会去找,这个时候发现你已经把筷子或者铁勺放进了屋子,你就不用再去外部拿筷子进屋子了,意思就是IOC容器中已经存在,就可以只要拿去用,而不必再去注册!而拿屋子里已有的东西的操作就是下面要讲的用于使用Bean的注解!
一类注解是用于使用Bean:@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的@Autowired , @Resource配合到一起,把对象、属性、方法完美组装。
用于使用Bean的注解:比如@Autowired , @Resource注解,这些注解就是把屋子里的东西自己拿来用,如果你要拿,前提一定是屋子(IOC)里有的,不然就会报错,比如你要做一道牛肉拼盘需要五头牛做原材料才行,你现在锅里只有四头牛,这个时候你知道,自己往屋子里搬过五头牛,这个时候就直接把屋子里的那头牛直接放进锅里,完成牛肉拼盘的组装。是的这些注解就是需要啥想要啥,只要容器中有就往容器中拿!而这些注解又有各自的区别,比如@Autowired用在筷子上,这筷子你可能只想用木质的,或许只想用铁质的,@Autowired作用在什么属性的筷子就那什么筷子,而@Resource如果用在安格斯牛肉上面,就指定要名字就是安格斯牛肉的牛肉。
@Component
:通用的注解,可标注任意类为Spring
组件。如果一个 Bean 不知道属于哪个层,可以使用@Component
注解标注。@Repository
: 对应持久层即 Dao 层,主要用于数据库相关操作。@Service
: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@Controller
: 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
三、bean的配置方式
bean配置有三种方法:
- 基于xml配置Bean
- 使用注解定义Bean
- 基于java类提供Bean定义信息
1、传统XML配置方式
直接填写 bean标签属性和子标签,根据构造函数实现,可能还会有ref引用其他bean,或者基本属性,集合属性等等
1 |
|
创建对象时,默认执行无参构造函数创建对象。
- 默认命名空间:
- 它没有空间名,用于Spring Bean的定义;
- xsi命名空间:这个命名空间用于为每个文档中命名空间指定相应的Schema样式文件,是标准组织定义的标准命名空间;
- aop命名空间:这个命名空间是Spring配置AOP的命名空间,是用户自定义的命名空间。
命名空间的定义分为两个步骤:第一步指定命名空间的名称;第二步指定命名空间的Schema文档样式文件的位置,用空格或回车换行进行分分隔。
2、Bean基本配置
在Spring容器的配置文件中定义一个简要Bean的配置片段如下所示:
1 | <bean id="" class=""></bean> |
一般情况下,Spring IOC容器中的一个Bean即对应配置文件中的一个<bean>,这种镜像对应关系应该容易理解。
- id为这个Bean的名称,通过容器的getBean(“foo”)即可获取对应的Bean,在容器中起到定位查找的作用,是外部程序和Spring IOC容器进行交互的桥梁。
- class属性指定了Bean对应的实现类。
3、依赖注入
- 属性注入
- 构造函数注入
- 工厂方式注入
使用注解定义bean——@Component
Spring容器成功启动的三大要件分别是:Bean定义信息、Bean实现类以及Spring本身。如果采用基于XML的配置,Bean定义信息和Bean实现类本身是分离的,而采用基于注解的配置方式时,Bean定义信息即通过在Bean实现类上标注注解实现。
下面是使用注解定义一个DAO的Bean:
1 | package com.baobaotao.anno; |
在(1)处,我们使用@Component
注解在UserDao类声明处对类进行标注,它可以被Spring容器识别,Spring容器自动将POJO转换为容器管理的Bean。
它和以下的XML配置是等效的:
1 | <bean id="userDao" class="com.baobaotao.anno.UserDao"/> |
除了@Component
以外,Spring提供了3个功能基本和@Component等效的注解,它们分别用于对DAO、Service及Web层的Controller进行注解,所以也称这些注解为Bean的衍型注解:(类似于xml文件中定义Bean<bean id=” “ class=” “/>
@Repository
:用于对DAO实现类进行标注;==> Dao层@Service
:用于对Service实现类进行标注;==>业务逻辑层@Controller
:用于对Controller实现类进行标注;==>web控制层
之所以要在@Component之外提供这三个特殊的注解,是为了让注解类本身的用途清晰化,此外Spring将赋予它们一些特殊的功能。
使用注解配置信息启动spring容器
Spring提供了一个context的命名空间,它提供了通过扫描类包以应用注解定义Bean的方式:
1 |
|
在①处声明context命名空间,在②处即可通过context命名空间的component-scan的base-package属性指定一个需要扫描的基类包,Spring容器将会扫描这个基类包里的所有类,并从类的注解信息中获取Bean的定义信息。
如果仅希望扫描特定的类而非基包下的所有类,你们可以使用resource-pattern属性过滤特定的类,如下所示:
1 | < context:component-scan base-package="com.baobaotao" resource-pattern="anno/*.class"/ > |
这里我们将基类包设置为com.baobaotao,默认情况下resource-pattern属性的值为”**/.class”,即基类包里的所有类。这里我们设置为”anno/.class”,则Spring仅会扫描基包里anno子包中的类。
4、基于java类提供Bean定义——@Configuration
在普通的POJO类中只要标注@Configuration注解,就可以为spring容器提供Bean定义的信息了,每个标注了@Bean的类方法都相当于提供了一个Bean的定义信息。
1 | package com.baobaotao.conf; |
①处在APPConf类的定义处标注了@Configuration注解,说明这个类可用于为Spring提供Bean的定义信息。类的方法处可以标注@Bean注解,Bean的类型由方法返回值类型决定,名称默认和方法名相同,也可以通过入参显示指定Bean名称,如@Bean(name=”userDao”).直接在@Bean所标注的方法中提供Bean的实例化逻辑。
在②处userDao()和logDao()方法定义了一个UserDao和一个LogDao的Bean,它们的Bean名称分别是userDao和logDao。在③处,又定义了一个logonService Bean,并且在④处注入②处所定义的两个Bean。
因此,以上的配置和以下XML配置时等效的:
1 | <bean id="userDao" class="com.baobaotao.anno.UserDao"/> |
基于java类的配置方式和基于XML或基于注解的配置方式相比,前者通过代码的方式更加灵活地实现了Bean的实例化及Bean之间的装配,但后面两者都是通过配置声明的方式,在灵活性上要稍逊一些,但是配置上要更简单一些。
5、@Component 和 @Bean 的区别是什么?
@Component
注解作用于类,而@Bean
注解作用于方法。@Component
通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan
注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean
告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。@Bean
注解比@Component
注解的自定义性更强,而且很多地方我们只能通过@Bean
注解来注册 bean。比如当我们引用第三方库中的类需要装配到Spring
容器时,则只能通过@Bean
来实现。
@Bean
注解使用示例:
1 |
|
上面的代码相当于下面的 xml 配置
1 | <beans> |
下面这个例子是通过 @Component
无法实现的。
1 |
|
四、Bean注入
Bean注入的方式有两种,一种是在XML中配置,此时分别有属性注入、构造函数注入和工厂方法注入;另一种则是使用注解的方式注入 @Autowired,@Resource,@Required。
1、在xml文件中配置依赖注入
a.属性注入
属性注入即通过setXxx()方法注入Bean的属性值或依赖对象,由于属性注入方式具有可选择性和灵活性高的优点,因此属性注入是实际应用中最常采用的注入方式。
属性注入要求Bean提供一个默认的构造函数,并为需要注入的属性提供对应的Setter方法。Spring先调用Bean的默认构造函数实例化Bean对象,然后通过反射的方式调用Setter方法注入属性值。
1 | package com.baobaotao.anno; |
bean.xml配置
1 |
|
- ref引用一个已经存在的对象
- property:是通过setter方法注入
b.构造方法注入
使用构造函数注入的前提是Bean必须提供带参数的构造函数。例如
1 | package com.baobaotao.anno; |
bean.xml配置
1 |
|
- constructor-arg:通过构造方法注入
constructor-arg标签属性:
- name属性:通过参数名找到参数列表中对应参数
- index属性:通过参数在参数列表中的索引找到参数列表中对应参数,index从0开始:
- type属性:通过参数数据类型找到参数列表中对应参数
- value属性:设置参数列表参数对应的值,用于设定基本数据类型和String类型的数据
- ref属性:如果参数值为非基本数据类型,则可通过ref为参数注入值,其值为另一个bean标签id或name属性的属性值
constructor-arg子标签:
- ref子标签:对应ref属性,该标签name属性的属性值为另一个bean标签id或name属性的属性值;
- value子标签:对应value属性,用于设置基本数据类型或String类型的参数值;
- list子标签:为数组或List类型的参数赋值
- set子标签:为Set集合类型参数赋值
- map子标签:为Map集合类型参数赋值
- props子标签:为Properties类型的参数赋值
参考文章:
c.工厂方法注入
非静态工厂方法:
有些工厂方法是非静态的,即必须实例化工厂类后才能调用工厂放。
1 | package com.baobaotao.ditype; |
工厂类负责创建一个或多个目标类实例,工厂类方法一般以接口或抽象类变量的形式返回目标类实例,工厂类对外屏蔽了目标类的实例化步骤,调用者甚至不用知道具体的目标类是什么。
1 |
|
静态工厂方法:
很多工厂类都是静态的,这意味着用户在无须创建工厂类实例的情况下就可以调用工厂类方法,因此,静态工厂方法比非静态工厂方法的调用更加方便。
1 |
|
2、使用注解的方式注入
a.使用@Autowired进行自动注入
Spring通过@Autowired注解实现Bean的依赖注入,下面是一个例子:
1 | package com.baobaotao.anno; |
在①处,我们使用@Service将LogonService标注为一个Bean,在②处,通过@Autowired注入LogDao及UserDao的Bean。
@Autowired默认按类型匹配的方式,在容器查找匹配的Bean,当有且仅有一个匹配的Bean时,Spring将其注入到@Autowired标注的变量中。
使用@Autowired的required属性
如果容器中没有一个和标注变量类型匹配的Bean,Spring容器启动时将报NoSuchBeanDefinitionException的异常。如果希望Spring即使找不到匹配的Bean完成注入也不用抛出异常,那么可以使用@Autowired(required=false)进行标注:
1 |
|
默认情况下,@Autowired的required属性的值为true,即要求一定要找到匹配的Bean,否则将报异常。
b.使用@Qualifier指定注入Bean的名称
如果容器中有一个以上匹配的Bean时,则可以通过@Qualifier注解限定Bean的名称,如下所示:
1 |
|
这里假设容器有两个类型为UserDao的Bean,一个名为userDao,另一个名为otherUserDao,则①处会注入名为userDao的Bean。
c.对类方法进行标注
@Autowired可以对类成员变量及方法的入参进行标注,下面我们在类的方法上使用@Autowired注解:
1 | package com.baobaotao.anno; |
如果一个方法拥有多个入参,在默认情况下,Spring自动选择匹配入参类型的Bean进行注入。Spring允许对方法入参标注@Qualifier以指定注入Bean的名称,如下所示:
1 |
|
在以上例子中,UserDao的入参注入名为userDao的Bean,而LogDao的入参注入LogDao类型的Bean。
一般情况下,在Spring容器中大部分的Bean都是单实例的,所以我们一般都无须通过@Repository、@Service等注解的value属性为Bean指定名称,也无须使用@Qualifier按名称进行注入。
d.对标准注解的支持
此外,Spring还支持@Resource和@Inject注解,这两个标准注解和@Autowired注解的功能类型,都是对类变量及方法入参提供自动注入的功能。
@Resource
要求提供一个Bean名称的属性,如果属性为空,则自动采用标注处的变量名或方法名作为Bean的名称。
1 | package com.baobaotao.anno; |
这时,如果@Resource未指定”car”属性,则也可以根据属性方法得到需要注入的Bean名称。可见**@Autowired
默认按类型匹配注入Bean,@Resource
则按名称匹配注入Bean**。而@Inject和@Autowired一样也是按类型匹配注入的Bean的,只不过它没有required属性。可见不管是@Resource还是@Inject注解,其功能都没有@Autowired丰富,因此除非必须,大可不必在乎这两个注解。
(类似于Xml中使用
1 | <constructor-arg ref="logDao"></constructor-arg> |
进行注入,如果使用了@Autowired或者Resource等,这不需要在定义Bean时使用属性注入和构造方法注入了)
e.@Autowired 和 @Resource 的区别
@Autowired
注入是按照类型注入的,只要配置文件中的bean类型和需要的bean类型是一致的,这时候注入就没问题。但是如果相同类型的bean不止一个,此时注入就会出现问题,Spring容器无法启动。Autowired
属于 Spring 内置的注解,默认的注入方式为byType
(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。这会有什么问题呢? 当一个接口存在多个实现类的话,
byType
这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。这种情况下,注入方式会变为
byName
(根据名称进行匹配),这个名称通常就是类名(首字母小写)。@Resourced
标签是按照bean的名字来进行注入的,如果我们没有在使用@Resource时指定bean的名字,同时Spring容器中又没有该名字的bean,这时候@Resource就会退化为@Autowired即按照类型注入,这样就有可能违背了使用@Resource的初衷。所以建议在使用@Resource时都显示指定一下bean的名字@Resource(name=”xxx”)
就比如说下面代码中的 smsService
。
1 | // smsService 就是我们上面所说的名称 |
举个例子,SmsService
接口有两个实现类: SmsServiceImpl1
和 SmsServiceImpl2
,且它们都已经被 Spring 容器所管理。
1 | // 报错,byName 和 byType 都无法匹配到 bean |
我们还是建议通过 @Qualifier
注解来显示指定名称而不是依赖变量的名称。
@Resource
属于 JDK 提供的注解,默认注入方式为 byName
。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType
。
@Resource
有两个比较重要且日常开发常用的属性:name
(名称)、type
(类型)。
1 | public Resource { |
如果仅指定 name
属性则注入方式为byName
,如果仅指定type
属性则注入方式为byType
,如果同时指定name
和type
属性(不建议这么做)则注入方式为byType
+byName
。
1 | // 报错,byName 和 byType 都无法匹配到 bean |
简单总结一下:
@Autowired
是 Spring 提供的注解,@Resource
是 JDK 提供的注解。Autowired
默认的注入方式为byType
(根据类型进行匹配),@Resource
默认注入方式为byName
(根据名称进行匹配)。- 当一个接口存在多个实现类的情况下,
@Autowired
和@Resource
都需要通过名称才能正确匹配到对应的 Bean。Autowired
可以通过@Qualifier
注解来显示指定名称,@Resource
可以通过name
属性来显示指定名称。
f.让@Resource和@Autowired生效的几种方式
1.在xml配置文件中显式指定
1 | <!-- 为了使用Autowired标签,我们必须在这里配置一个bean的后置处理器 --> |
2.在xml配置文件中使用context:annotation-config
1 | <context:annotation-config /> |
3.在xml配置文件中使用context:component-scan
1 | <context:component-scan base-package="com.baobaotao.anno"/> |
4.重写Spring容器的Context,在自定义BeanFactory时调用AnnotationConfigUtils.registerAnnotationConfigProcessors()把这两个注解处理器增加到容器中。
g.自动注入
此时对象已经存放在了容器中,等着被用。那我们怎么从容器中取到我们想用的对象呢?
两种方式,一种是通过类型,一种是通过名字。(容器中的一个对象必然携带两个信息,一个是自己是哪个类的对象,即类型;一个是自己叫什么名字)
最先想到的就是get,对,get。从哪里get呢?从容器中。容器在哪里呢?
容器 最根是个BeanFactory(开始设计的时候定义的),有个子类叫ApplicationContext(容器管理的一些方法,就像Object和Map),专门做这事情。从 ApplicationContext中get出来我们需要的对象。
ApplicationContext 有很多实现,主要针对不同场景下
- AnnotationConfigApplicationContext : 从一个或多个基于Java的配置类中加载上下文定义,适用于Java注解的方式;
- ClassPathXmlApplicationContext : 从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式;
- FilesSystemXmlApplicationContext : 从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载xml配置文件
- AnnotationConfigWebApplicationContext : 专门为web应用准备的,适用于注解方式;
- XmlWebApplicationContext : 从web应用下的一个或多个xml配置文件加载上下文定义,适用于xml配置方式
五、Bean的作用域
Spring 中 Bean 的作用域通常有下面几种:
- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的,对单例设计模式的应用。
- prototype : 每次请求都会创建一个新的 bean 实例。
- request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
- session : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
- global-session : 全局 session 作用域,仅仅在基于 portlet 的 web 应用中才有意义,Spring5 已经没有了。Portlet 是能够生成语义代码(例如:HTML)片段的小型 Java Web 插件。它们基于 portlet 容器,可以像 servlet 一样处理 HTTP 请求。但是,与 servlet 不同,每个 portlet 都有不同的会话。
如何配置 bean 的作用域呢?
xml 方式:
1 | <bean id="..." class="..." scope="singleton"></bean> |
注解方式:
1 |
|
六、Bean的生命周期
下面的内容整理自:https://yemengying.com/2016/07/14/spring-bean-life-cycle/ ,除了这篇文章,再推荐一篇很不错的文章 :https://www.cnblogs.com/zrtqsk/p/3735273.html 。
- Bean 容器找到配置文件中 Spring Bean 的定义。
- Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。由BeanFactory读取Bean定义文件,并生成各个实例。
- 如果涉及到一些属性值 利用
set()
方法设置一些属性值。 - 如果 Bean 实现了
BeanNameAware
接口,调用setBeanName()
方法,传入 Bean 的名字。 - 如果 Bean 实现了
BeanClassLoaderAware
接口,调用setBeanClassLoader()
方法,传入ClassLoader
对象的实例。 - 如果 Bean 实现了
BeanFactoryAware
接口,调用setBeanFactory()
方法,传入BeanFactory
对象的实例。 - 与上面的类似,如果实现了其他
*.Aware
接口,就调用相应的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessBeforeInitialization()
方法 - 如果 Bean 实现了
InitializingBean
接口,执行afterPropertiesSet()
方法。 - 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
- 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法 - 当要销毁 Bean 的时候,如果 Bean 实现了
DisposableBean
接口,执行destroy()
方法。 - 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。
图示:
与之比较类似的中文版本:
七、Bean的实例化
1、 懒汉式:BeanFactory
只有当客户端调用BeanFactory的getBean()方法来请求某个实例对象的时候,才会触发相应bean的实例化进程( 当然对于 BeanFactory 容器而言并不是所有的 getBean() 方法都会触发实例化进程,比如 signleton 类型的 bean,该类型的 bean 只会在第一次调用 getBean() 的时候才会触发,而后续的调用则会直接返回容器缓存中的实例对象)
2、 饿汉式:ApplicationContext
使用ApplicationContext容器启动的时候立刻调用注册到该容器所有bean定义的实例化方法
Spring提供了两种类型的IOC容器实现(两种类型的配置方式是一样)
- BeanFactory:是Spring框架的基础设施,面向Spring本身
- ApplicationContext: 面向使用 Spring 框架的开发者,几乎所有的应用场合都直接使用 ApplicationContext 而非底层的
Spring单例Bean与单例模式的区别
单例模式是指在一个JVM进程中仅有一个实例,而Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。
首先看单例模式,在一个JVM进程中(理论上,一个运行的JAVA程序就必定有自己一个独立的JVM)仅有一个实例,于是无论在程序中的何处获取实例,始终都返回同一个对象。
与此相比,Spring的单例Bean是与其容器(ApplicationContext)密切相关的,所以在一个JVM进程中,如果有多个Spring容器,即使是单例bean,也一定会创建多个实例。
单例 Bean 的线程安全问题
大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。
常见的有两种解决办法:
- 在 Bean 中尽量避免定义可变的成员变量。
- 在类中定义一个
ThreadLocal
成员变量,将需要的可变成员变量保存在ThreadLocal
中(推荐的一种方式)。
不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。