在spring这座大厦中,去除掉最底部的核心(core)组件,那么最重要的无疑是bean和bean工厂。
剩余是AOP、设计模式,更之上的就是各种组件:DATA,WEBMVC...
为了便于行文,这里把bean和bean工厂统称为bean。
bean英文的意思是豆子。为了符合它的实际作用,本人把bean翻译为“缓存对象实例”,但按照惯例,本文地方依然称为bean。
在本文中,bean主要指bean实例和bean工厂。
spring体系中的bean实际作用是创建和管理类实例,以便达成最核心的功能:提升创建效率,用空间换时间。
这也基本上是所有缓存的要义。
spring把bean玩出了花,基本上所有地方都会带上bean。
使用spring进行javaee开发,是无法绕过bean,也没有必要去绕过。
基于此,我们需要了解bean的一些基本知识,以便它能够为我们的应用开发服务。
注:由于我们现在已经基本不基于xml(或者非boot)的方式创建spring工程,所以以下内容不讨论古早的一些内容xml配置。
另外,本文的内容和例子是基于spring6.x+JDK17。
如果仅仅是做应用开发,那么了解以下的知识,个人认为足矣:
1.bean工厂
通过这个,我们知道bean放在什么地方,可以用什么工具访问它们。
2.创建bean
掌握常见的几种创建/定义bean的方式,以便选择需要的途径,并在必要的时候可以读懂代码
3.注入bean
掌握几种注入bean的方式,从而更加灵活编码,并在必要的时候达成一些其它目的
4.生命周期
在必要的时候调整bean;在系统初始化后或者关闭前做一些必要的事情
5.作用范围与单例
除了单例bean,有时候想象不到其它作用范围还有什么用处;谨慎使用非单例
6.常见的注解
这些注解主要涵盖bean的定义、创建、作用范围和生命周期。基本上掌握了这个就能掌握bean的大部分知识。
1.bean工厂
如果您学过常见的设计模式,那么必然知道工厂是什么意思。
没错,bean工厂就是那么设计的,只不过代码会稍微复杂一些些,但核心还是那样:利用java的map数据结构保存已经创建的对象实例,并有相关增删改查这些结构的
一些方法,以便程序可以访问这些数据结构中的bean。
以下是几个bean工厂的关键知识点:
1.1、没有必要创建多个bean工厂
spring没有强制阻止开发人员创建多个工厂,只不过在绝大部分的业务场景中,看不到创建多个工厂管理不同分类bean的必要性。
如前,spring的核心价值在javaEE开发,或者某种程度上就是作为一个web应用开发框架。
当我们的大部分对象都是web有关的时候,那么把和web有关的bean放在一个工厂中,总体上好处多于坏处。
如果硬要自己再创建默认工厂之外的工厂也可以,典型的作用就是通过创建额外的应用上下文来创建一个新的bean工厂。
1.2、默认bean工厂
默认情况下,spring会创建一个org.springframework.beans.factory.support.DefaultListableBeanFactory工厂。
虽然前文说过bean工厂是按照工厂模式进行设计,但为了工业上可用,这个类还是提供了非常多的数据结构用于存储各种信息,并提供了
非常多的方法用于实现各种功能,其中一个典型的功能就是getBean。
1.3、应用上下文和bean工厂
在spring中(如果没有特别说明指的是springweb应用),bean工厂包含在应用上下文中。
这个上下文是org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext(如果是webFlux,应该不是这个)
这个上下文就会包含默认的那个bean工厂DefaultListableBeanFactory。
如下图(AnnotationConfigServletWebServerApplicationContext继承于GenericApplicationContext):
1.4、Aware接口
spring的Aware接口的目的在于注入一个bean对象,典型地主要是注入应用上下文或者bean工厂。
Aware接口有众多的继承者:
2.创建bean
spring有多种方式创建bean。注意这里指的是“创建”,或者说定义,不包含注入。
什么是创建?就是根据java类创建对应的实例,并把这个类实例放入到bean工厂中。
什么是注入,就是把bean实例作为另外一个实例的属性(属性赋值)。
总体上,可以把bean的创建分为两类:自动和手动。
- 自动创建可以等同于通过注解创建
- 手动创建,即通过编写代码创建
手动创建的方式相当罕见,基本上找不到这么做的业务场景或者理由。
当然,如果愿意找,也可以找到大把交我们如何手动创建的文章,以及使用到手动创建的例子。
只不过,我个人基本上不怎么赞成那么做,因为手动创建,基本上等同于否定bean存在的理由:帮用户管理实例、并顺便提升速度、适当减低内存使用。
如何通过注解创建bean,见后文“常见的注解"有关部分。
具体到bean的代码
顺便说下,关于代码规范的问题。 现在有部分公司的规范或者网络上部分所谓的规范提倡一个类或者一个方法不要超过多少行的。
看看spring的源码,不知道有多少没有严格遵循。 所以所谓不能太长应该只能是一个大概的标准,依然需要具体情况具体分析。
我个人反对把一个方法写得过短,因为现有的大部分高级语言在调用其他函数的时候,都需要压栈等一系列操作,而这意味着牺牲性能。
当然,我们也非常理解为什么有的组织会那么规定,主要是程序员的水平参差不齐,责任心有高有低,一刀切有时候是一种折衷的措施。
所以一个bean应该有多少行代码,不是有特定的限定,看具体情况和具体组织的要求。
3.注入bean
如前,注入bean,顾名思义,就是把bean实例作为一个组件(属性)塞进另外一个实例中,专业一点的说法就是赋值。
3.1注入方式
注入的方式并不多,常见的有:
- 注解注入-常见的注解见后文
- 方法注入-可以细分为构造注入和赋值方法注入(即所谓的setter注入)
- 工具类注入-即通过工具类获取bean,并设置有关实例的bean属性
总体而言,注解注入和工具类注入是最常见的方式。
通过注解注入是最推荐的方式,因为它简单,能够节约时间,也基本没有什么副作用。
工具类的注入,主要是为了解决非自动创建类实例的情况下,对bean的使用。
方法注入也不是一无是处,例如有以下3个主要好处:
- 可以享受方法注入的副作用--在方法中写一些其它代码,实现额外的目录
- 让代码不要充斥着bean注解
- 还有一个好处:便于迁移到非spring体系。
为了最后一个好处而放弃注解注入,无疑是十分荒唐的理由。但是有那么一些文章在鼓吹放弃autowired改为方法注入,不是很理解。
说白了,绝大部分项目开始启用spring之后,就不会想着迁移到其它体系中,所以这个理由是没有什么说服力的。
所以,我从来不鼓励使用方法注入,盖因为我所做的系统以应用开发巨多,也从来不会想着迁移到其它ioc框架。
理论上来说,遵循规则来设计代码是不错的。但要考虑到工程具体化问题,就不会那么严谨了,这和表设计不遵循第三范式类似的道理。
说白了,那样会浪费比较多的时间,而时间就意味着成本,虽然成本很多时候也不光是写代码造成的,甚至写代码有时候都不是造成成本
升高的主要原因。但是能省一点是一点。
当我们做项目的时候,项目总是存在生命周期(一般是5年左右,长的在10年左右),此外做项目意味着基本不需要考虑通用性,基于此,不会对项目的可迁移性
做特别的要求。另外说一句托大的话,做个业务系统,不会使用什么了不起的技术,现在绝大部分的中国IT企业不会想着那么干。
IT企业在资本家眼里就好比是建筑业、制造业一样,资本家在乎的是赚快钱,而不是创造一个伟大的企业。
所以,如果程序员知道如何适当设计service,那么就没有必要使用方法注入,大胆地使用Autowired来注入bean,只要不滥用即可。
虽然,不鼓励方法注入,但是基本的设计原则、设计模式、代码规范还是需要遵守的,从职业道德和本职需求出发,那应该是总体上最好的一个选择。
最后补充一点:在spring6.x中,构造函数注入是不需要Autowired,但赋值方法还是需要的。所以,网上现在巨多已经过时的知识,工程师们应该自己辨别。
示例:spring6.x构造函数注入,不用autowired
@Service public class StudentService {private StudentDaoService stuDaoService;public StudentService(StudentDaoService stuDaoService) {this.stuDaoService = stuDaoService;}public Map<String,SchoolStudent> getAll(){return this.stuDaoService.get();} }
3.2注入限定
一个类有多个实现的注入问题
注入的时候,默认情况下,bean工厂会根据类类型来查找,这就会产生一个问题:如果一个类(接口)有多个实现类(bean实例),那么应该怎么选择?
出现这种情况的时候的时候,可能会导致错误:Parameter 0 of method xxx required a single bean, but *** were found(方法注入)
或者是autowired注入的时候。
比较典型的情况在于我们采用多数据源的时候:Parameter 0 of method sqlSessionFactory in com.ruoyi.framework.config.db.MyBatisConfig required a single bean, but 3 were found
这是因为现在很多组件喜欢通过@Bean注解定义bean,这个注解可以通过方法注入方式注入bean。
由于某些原因,组件设计者不会在参数中使用qualifier之类的注解,所以一旦有多个实现类(bean)就会导致报错。
遇到这种情况有几种解决方式:
- 定义bean的时候指定Primary注解从而定义一个主bean,这样bean工厂会选择主bean。这个是主要的解决方式,也是比较容易的方式。这种方式主要用于不可修改源头组件的情况。
- 利用qualifier注解结合其它注解的name属性来指定需要注入的bean的名称,这多用于我们能决定bean名称的场景
3.3注入顺序
注入顺序在实际应用中很少用到,或者说主要是spring自己用到。
和顺序有关的注解包含:order,priority,后者是JSR250标准。
用了order,意味着你为一个接口定义了多个实现类(bean),并且需要注入一个对象集合。而order中的数字决定了诸如时候,bean在集合中的顺序,
序号越小越前,这是一种符合绝大分人习惯的约定。
可以随意地,方便地使用的注解,某些时候有助于实现一些相对灵活的功能,order注解就有这个作用。
spring通过这个注解,可以决定某些事件的发生顺序、aop执行顺序。
总而言之,order这样的注解主要是spring自用,用于注入一个指定顺序的集合,并靠集合元素的顺序来确定其它顺序。
而一般的业务开发中,我们比较少用到。当然如果要可以去模仿也是可以的。
// 接口
package study.example.school.service; public interface LearnService {public String getName();public void learn(); }
//实现类1-LearnChineseServiceImpl
package study.example.school.service.impl; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import study.example.school.service.LearnService;@Order(1) @Service public class LearnChineseServiceImpl implements LearnService {@Overridepublic String getName() { return "中文";}@Overridepublic void learn() {System.out.println("少读诗书陋汉唐");} }
//实现类2-
LearnMathServiceImpl
package study.example.school.service.impl;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import study.example.school.service.LearnService;
@Order(2) @Service
public class LearnMathServiceImpl implements LearnService {
@Override
public String getName() { return "数学"; }
@Override public void learn() { System.out.println("1+1=2;1-1=0"); }
}
//注入List<LearnService>的bean
@Service
public class StudentService {
private List<LearnService> learnServices;
@Autowired
public void setLeanServices(List<LearnService> learnServices) {
//这里可以加一些代码,例如 sysout打印参数,可以证明learnServices是按照order顺序排列的
this.learnServices = learnServices;
}
}
3.4循环引用
在spring6.x中,并不鼓励对象的循环应用。事实上,循环引用对于编码等方面是一些挑战,所以大部分的程序、组件并不鼓励循环应用,例如spring的bean、对象属性对自身类型的引用,
后者往往给序列化照成一些问题,而前者主要给编码造成问题。
spring有参数可以解决这个问题,通过系统参数spring.main.allow-circular-references= true
但是毫无疑问,循环引用的存在,给工程师照成了一些麻烦,并通常导致启动会慢一点点之类的问题。
所以,我们日常编码的时候,也尽量不要循环引用,如果不可避免,那么也无所谓,spring也能解决。
4.生命周期
大部分时候,我们并不会关注bean的生命周期,通常认为程序启动后bean就创建了,程序关闭时bean会随着工厂一起消失。
我们通常把bean当圆珠笔一样用,用的时候拿来写,不用了就放一边,不会特意关注放哪里,还在不在。
如果不是对资源的影响特别大,不会去考虑延迟加载等等一些事情。
但有两件事还是会稍微关注下:bean什么时候创建好/什么时候全部bean就绪;bean什么时候销毁/什么时候全部bean销毁
因为,我们常常会希望在某些bean就绪之后做一些初始化的操作,或者在bean销毁前释放特定的资源。
虽然有postConstruct/preDestroy这样的注解可以使用,但是当一个系统比较复杂的时候,我个人还是更喜欢基于spring的事件进行初始化或者。
例如基于ApplicationReadyEvent来进行集中的初始化,这样做能更好地控制执行顺序,其次更容易维护,不至于到处找和维护初始化代码。
当然,如果不用在于初始化顺序,用类似postContruct这样的注解做一些初始化也是可以的,毕竟现在的idea能够解决一部分维护问题。
同样地,通过ContextClosedEvent进行程序关闭前的一些操作。
表-和生命周期有关的一些接口、注解、事件
序号 |
名称 |
类型 |
作用 |
调用时机 |
注意事项 |
1 |
InstantiationAwareBeanPostProcessor |
接口 |
可以做一些个性化初始化工作 有一些些作用 |
在bean初始化前后 |
bean的属性可能还没有初始化 |
2 |
BeanPostProcessor |
接口 |
可以对bean实例做一些修改 其实作用不大 |
在bean创建前后(bean的属性已经完成) |
不确认其中的两个方法是否接收的都是已经注入属性的bean,在测试用力中,是已经有属性值了。 |
3 |
ApplicationContextAwareProcessor |
类 |
把实现了org.springframework.beans.factory.Aware接口的类中注入ConfigurableApplicationContext或者它的属性 |
只要有关类实现了Aware接口,那么在bean实例(有属性)创建后,调用 |
|
4 |
DestructionAwareBeanPostProcessor |
接口 |
注销前做一些事情 |
在类注销前调用 |
|
5 |
postConstruct |
注解 |
|
初始化之后 |
|
6 |
preDestroy |
注解 |
|
销毁前 |
|
7 |
多个ware接口 |
接口 |
被ApplicationContextAwareProcessor处理,并注入一个上下文信息 |
大体是初始化后 |
|
8 |
org.springframework.context.ApplicationListener<E> |
接口 |
应用事件,在特定情况下被触发。 例如可以在应用启动完毕,或者应用关闭前做一些操作 把需要做的事情统一起来,以便更好编码/管理等,而不是通过自动配置等一些复杂的方式来实现。 |
特定事件完成后 |
5.作用范围与单例
作用范围,英文SCOPE,和bean工厂一样,基本不会有什么人特别关注它。
在org.springframework.beans.factory.config.ConfigurableBeanFactory中定义了两个经典的作用域:singleton,prototype。
singleton(单项物、单个人)--国内通常翻译为单例,实际就是某个类只会在bean工厂中创建一个实例
prototype(原型、雏形)-不太清楚外国人用这个是什么意思,也许可以理解为”本样(用一个建一个)“,但国内意译为多例,因为实际效果就是注入一次,创建一个实例,注入10次就创建10个
后来还有一些乱七八糟的,类似多例的作用范围:request,session,application,global session
最后,spring还很贴心地提供了自定义作用范围的机制,虽然这个功能基本没人用,但还是要说spring的设计某些时候还是很贴心的:它尽可能地提供扩展性,想工程师所想。
然而,在绝大部分业务中,我们不会使用单例之外的作用范围,我个人也不鼓励使用类似多例的范围来管理某些bean。
为什么选择单例? 因为这样让设计更加简单,程序更不容易出错,也更容易维护。
其次,绝大部分的应用系统都是以处理业务数据为主,且不是基于互联网设计的,所以基本不用考虑性能的问题(极端性能),所以可以把很多数据一致性等的任务直接交给数据库,
较大地减轻了程序设计的难度。
使用单例的第一注意事项:不要定义类变量(除非这个变量仅仅是工具),数据总是通过参数来传递或者通过dao组件来处理。
基于这样的原则,万花丛中过,片叶不沾身,可以非常放心大胆地使用单例bean。
6.常见的注解
下表列出了spring和bean有关的常见注解:
序号 |
编码 |
名称 |
类别1 |
可独立使用 |
用于类 |
用于方法 |
用于属性 |
用于参数 |
来源 |
说明 |
1 |
@Component |
组件 |
定义 |
是 |
✔ |
|
|
|
|
|
2 |
@Repository |
仓库 |
定义 |
是 |
✔ |
|
|
|
|
Component特例,表示仓库、存储(数据) |
3 |
@Controller |
控制器 |
定义 |
是 |
✔ |
|
|
|
|
Component特例,表示控制器(视图) |
4 |
@Service |
服务 |
定义 |
是 |
✔ |
|
|
|
|
Component特例,表示业务 |
5 |
@Bean |
组件实例 |
定义 |
是? |
✔ |
✔ |
|
|
|
用于方法上定义一个Bean,通常需要和Configuration配合 特别注意:根据定义,Bean可以用于注解类型,但很少那么用。 |
6 |
@Configuration |
配置 |
创建配置 |
是 |
✔ |
|
|
|
|
表明当前类是配置,其次也会生成bean |
7 |
@Import |
导入配置 |
导入配置 |
否 |
✔ |
|
|
|
|
通常和Configuration一起使用 |
8 |
@PropertySource |
属性资源 |
创建属性 |
否 |
✔ |
|
|
|
|
通常只能和Configuration一起使用 用于指定配置文件 |
9 |
@PropertySources |
属性资源集 |
创建属性 |
否 |
✔ |
|
|
|
|
通常只能和Configuration,PropertySources一起使用 用于指定配置文件 @PropertySources({ @PropertySource("classpath:application.properties"), @PropertySource("classpath:custom.properties") }) |
10 |
@ConfigurationProperties |
配置属性 |
创建属性 |
否 |
✔ |
|
|
|
|
通常只能和Configuration一起使用 用于指定配置文件的一个部分 |
11 |
@Conditional |
通用条件限定器 |
创建条件 |
否 |
✔ |
✔ |
|
|
|
根据条件生成bean,需要和Condition接口配合使用,后者提供条件的定义,当条件满足的时候,bean生成。 不能确认是否影响注入 |
12 |
@ConditionalOnMissingBean |
不存在限定器 |
创建条件 |
否 |
✔ |
✔ |
|
|
|
基本同上,只是条件是不存在特定bean |
13 |
@ConditionalOnxxxx |
特定条件限定器 |
创建条件 |
否 |
✔ |
✔ |
|
|
|
同上 |
|
@DependsOn |
依赖限定器 |
创建条件 |
否 |
✔ |
✔ |
|
|
|
确保当前Bean在其他Bean之后初始化,从而控制Bean的初始化顺序/加载顺序。 通常和生成bean的注解配合。 |
21 |
@Profile |
配置限定器 |
创建条件 |
否 |
✔ |
✔ |
|
|
|
只有特定配置文件加载的时候才会生成或者注入bean。 需要和spring.profiles.active配合 通常和Configuration,Bean一起使用 |
22 |
@AutoConfigureAfter |
后于自动配置识别 |
创建顺序 |
否 |
✔ |
|
|
|
|
|
23 |
@AutoConfigureBefore |
优先自动配置识别 |
创建顺序 |
否 |
✔ |
|
|
|
|
|
24 |
@AutoConfigureBefore |
|
创建顺序 |
否 |
✔ |
|
|
|
|
|
25 |
@Scope |
范围绑定标记 |
创建目标与范围 |
否 |
✔ |
✔ |
|
|
|
用于注解类上, |
26 |
@Target |
目标绑定标记 |
创建目标与范围 |
否 |
✔ |
|
|
|
JAVA |
用于注解类上,指定可用目标 |
27 |
@Lazy |
延后指定 |
创建时机 |
否 |
✔ |
✔ |
|
|
|
延迟加载 只有被显示注入(用各种注解),或者使用Bean工厂的方法获取bean的时候会被创建。
|
28 |
@ComponentScan |
组件扫描 |
创建扫描 |
否 |
✔ |
|
|
|
|
|
29 |
@ComponentScans |
组件集合扫描 |
创建扫描 |
否 |
✔ |
|
|
|
|
|
30 |
@MapperScan |
映射器扫描 |
创建扫描 |
是 |
✔ |
|
|
|
Mybatis |
|
31 |
@SpringBootApplication |
应用启动 |
创建扫描 |
是 |
✔ |
|
|
|
|
|
| ||||||||||
51 |
@Resource |
资源 |
注入 |
是 |
✔ |
✔ |
✔ |
|
JSR-250 |
Component特例,表示资源 |
52 |
@Resources |
资源 |
注入 |
是 |
✔ |
|
|
|
JSR-250 |
|
53 |
@Autowired |
自动绑定 |
注入 |
是 |
✔ |
✔ |
✔ |
✔ |
|
如果用于方法,则会在初始化时候执行setter,会尝试自动注入来自于bean的参数 所以有时候也可以用执行一些初始化操作。不过个人不推荐。 理论上可以用于注入方法参数,但是不推荐。 特别注意:根据定义,Autowired可以用于注解类型,但很少那么用。 |
54 |
@Qualifier |
名称限定器 |
注入限定 |
否 |
✔ |
✔ |
✔ |
✔ |
|
当一个接口有多个实现类(bean)的时候,用于指定特定的目标。可以通过bean名称来获取。 |
55 |
@Primary |
优先限定器 |
注入限定 |
否 |
✔ |
✔ |
|
|
|
当一个接口有多个实现类(bean)的时候,用于指定主bean。 这样在需要自动注入的场景可以选择默认bean。 常常和Comonent,Bean一起使用 |
56 |
@Order |
顺序指定 |
注入顺序 |
否 |
✔ |
✔ |
✔ |
|
|
注入排列顺序.注意,不是创建顺序 所谓排列顺序,指的是被注入的顺序。 |
57 |
@Priority |
顺序指定 |
注入顺序 |
否 |
✔ |
✔ |
✔ |
|
JSR-250 |
注入顺序,非创建顺序 有点起到Primary的作用。主要用于注入单个的情况下起到作用 |
| ||||||||||
91 |
@PostConstruct |
后置构建 |
弱相关-生命周期 |
是 |
|
✔ |
|
|
java标准 |
在bean初始化之后执行 |
92 |
@PreDestroy |
销毁前处理 |
弱相关-生命周期 |
是 |
|
✔ |
|
|
java标准 |
在bean被销毁前执行 |
93 |
@Retention |
持久性 |
弱相关-生命周期 |
否 |
✔ |
|
|
|
|
元注解。用于指定其他注解的保留策略。保留策略决定了注解在何时何地是有效的 |
当然,这是所有bean相关注解的大部分,但不是所有,此外有些注解和版本有关系,所以如果在spring的某些过时版本看不到,也不要奇怪。
、