前言
这篇博客主要是细读spring-core的文档,其中主要列出了文档中比较重要的点,以及一些自己的思考,并不是事无巨细都列出来的。
文档对应的链接:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans
对应的spring版本:5.3.5
第一章 IoC容器
本章主要介绍了spring的IoC容器。
介绍IoC容器和Beans
IoC和DI(dependency injection)之间的关系是什么?
IoC is also known as dependency injection (DI).
所以可以说IoC 就是DI,但是也有人说IoC 是一种思想,而DI是它的实现方法,我觉得都有道理。
如果我们要创建一个对象,我们需要调用它的构造函数、使用工厂,或者在创建完一个对象之后使用对应的setter。容器的作用就是在创建bean的时候,注入这些依赖。也就是把控制权从bean自己控制实例这种控制权交给了spring框架(IoC容器)。
org.springframework.beans和 org.springframework.context 这两个包是IoC容器的包。在这里面有BeanFactory和ApplicationContext两个重要的接口。其中ApplicationContext是子接口,它新增了一些方法。
在spring中,构成你的应用的主干的对象,并且被spring IoC容器管理的对象,称为bean。
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans.
bean由spring IoC容器进行实例化、组装和管理。
容器总览
容器通过读取对应的说明,来进行bean的实例化、配置和组装(instantiate, configure, and assemble)。对应的配置文件可以是XML文件、java的注解或者是java代码。
applicationcontext有很多实现,对于独立的那些应用,我们推荐使用ClassPathXmlApplicationContext or FileSystemXmlApplicationContext。

总体来说spring整体就长这样。
配置元数据
本章大多还是介绍XML形式的元数据配置,但是spring的IoC容器和配置文件的数据格式之间是解耦的。
在spring2.5之后,可以使用java注解来配置元数据;在spring3.0之后,核心框架吸收了Javaconfig这个项目。
在xml中使用的是<beans><bean>标签,而注解的话用的是@Configuration标注的类中的@Bean。
当然也并不是所有的对象都推荐让spring管理的,一些比较细粒度的对象,还是不要让spring管理。
实例化容器
通过一个字符串指定元数据的配置文件,容器就可以被实例化了。
基于XML配置元数据
bean的定义可以跨越多个xml文件,只需要在构造applicationcontext的时候一起指定就可以了。
在beans标签中可以使用import标签对别的文件进行导入,路径都是相对于当前的文件而言的。可以使用../来代表上级目录,但是不推荐这么做。因为这样会创造一个对上级目录进行依赖的应用,同理,也不是很推荐使用绝对路径来表示,而是使用${}这种环境变量。
基于Groovy的bean定义DSL
跳过。
使用容器
就是指定配置文件,创建容器。然后从容器中获取bean,利用这个bean进行一些操作。
GenericApplicationContext更加灵活一点,可以利用一些reader去读取一些新的xml文件并进行更新。
Bean总览
IoC 容器可以不包含bean吗?
A Spring IoC container manages one or more beans.
但是实际中是可以创建一个没有任何bean的context的。
在容器内部,这些xml中的一个一个Bean的定义,是以BeanDefinition接口的对象存在的,它有至少以下的元数据:
- 这个对象所属类的全限定类名。
- 这个对象的一些行为
- 这个对象还需要哪些别的bean
- 其它的一些配置。
除了由bean definition以外,IoC 容器还可以自己注册一个对象进去。具体方法是获取ApplicationContext的beanFactory,然后利用对应的registerSingleton方法可以注册一个对象。
但是!!文档里是这样的说的:
This is done by accessing the ApplicationContext’s BeanFactory through the
getBeanFactory()method, which returns the BeanFactoryDefaultListableBeanFactoryimplementation.
然而我自己并没有找到application context有对应的方法。真正的方法是,需要把ApplicationContext强转成ConfigurableApplicationContext就可以了。
如果使用手动的方法注册一个单例的bean,那么最好尽早注册。而且如果在运行的时候去覆盖一个单例的bean,那么由于这种注册的方法没有被正式支持,所以可能会导致问题的发生。
命名beans
我们一般使用id来区别beans,但是也可以使用name来区别。由于历史原因,在spring3.1之前,id是被声明成xsd:ID,这个限制了其内部的字符;而目前已经被声明成了xsd:string,所以可以包含任何字符。但是id的独一无二仍然是由IoC 容器来保证的,不是XML解释器。
当然可以不提供id和name属性,这种情况会在inner beans和autowiring collaborators的情况下发生。
spring为那些没有名字的组件创建名字,使用下面的规则:
1 | public static String decapitalize(String name) { |
一般情况下首字母小写;如果前两个字母都是大写的,那么就不改变。
bean的别名
在bean标签中,可以使用最多一个id和多个name(这里的意思不是直接让你使用多个name,这里似乎会被xml的规则限制)。
在大型的系统中,bean的定义分散在各处,可以使用<alias>来进行统一。
假设一个大系统由两个子系统组成A和B。A使用了 subsystemA-dataSource,同理B也使用了另外一个dataSource,那么当把它们合成一个大系统的时候, 主系统只需要
1 | <alias name="myApp-dataSource" alias="subsystemA-dataSource"/> |
这三个引用的是同一个对象了。也就是不论是子系统A还是B,或者是主系统,最终都会去访问myApp-dataSource。
实例化beans
绝大部分情况下,class这个属性是必须的。
通常容器会通过反射调用类对应的构造函数,本质上就是使用了new这个运算符。
如果使用了内嵌的类,可以使用$或者是.来进行分割。
利用构造器来实例化
这种方式是spring兼容最好的方法。所有的普通类都可以通过这种方法来实例化对象。
静态工厂方法实例化
如果使用静态工厂方法来创建一个对象,那么class属性指定静态工厂方法所在的类,然后 factory-method 指定这个类中对应的静态工厂方法。
实例工厂方法实例化
不同于静态工厂方法,由于它不能直接调用方法,所以需要首先创建出一个类的对象(对应到xml文件中是声明一个bean),然后利用这个bean的方法来创建对象。
确定bean的运行时类型
虽然在xml中声明了bean所属的类,但是这个只是初始化类,可能会被AOP代理修改.
实际中只需要使用容器(容器当然知道是什么实际类型了)的getType来获取一个对象的实际Class。
依赖
实际中肯定是各种beans组合在一起,组成一个应用给用户使用。本章主要介绍一下如何把这些stand alone的beans组合在一起。
依赖注入
依赖注入指的是,对象通过构造函数参数、工厂方法参数、setter方法参数(在创建完对象之后设置)来定义它们的依赖的这么一个过程。见下面英文原文:
Dependency injection (DI) is a process whereby objects define their dependencies (that is, the other objects with which they work) only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method.
用了依赖注入,你的代码可以更加干净,更加解耦,更加容易测试(比如你的代码依赖某个抽象类或者接口)
构造器依赖注入
静态工厂和构造器非常相似,所以可以合在一起说。简单来说就是你的构造器函数的参数由容器来提供。
构造器参数解析
根据上面我们也知道,由容器来提供构造器的参数的对象,那么容器就需要知道,去哪里找,怎么找,找谁。
如果使用参数名字来注入,那么就必须要在编译的时候开启debug,或者是使用@ConstructorProperties这个JVM提供的。
这里稍微解释下(其实我自己还有点疑惑),主要是因为构造函数中的参数名在运行的时候,其实是无所谓的,所以基于这一点,可能会导致spring无法真正找到一一对应的参数进行注入,所以需要开启debug标志来保留这个参数名,或者是利用@ConstructorProperties注解来显式标明参数的名字,这样spring才能使用。
我自己做了一个实验是,spring中的xml文件中只需要声明和@ConstructorProperties中声明的相同即可,spring和真实的参数名字就没有关系了。
setter注入
其实和构造器注入也几乎一样,只不过是对象创建完成之后使用setter方法来注入依赖。
其实不论是我们在xml配置,还是使用注解进行配置,本质上依赖对应的是BeanDefinition,依靠它完成最终的操作。
一般实际中,推荐强制依赖用构造器注入、一般可选的依赖用setter。同时可以在setter方法上使用@Require来让它变成强制的,但是不推荐这么做,不如直接用构造器。
依赖解决过程
首先根据一些XML配置文件、一些注解来构造applicationcontext。对于每一个bean,它的一些构造器参数、一些静态工厂参数等会被提供。这些属性都是要被定义的、或者是对容器中别的一种引用。
spring容器会在创建的时候,确认每一个bean的配置信息。只有当bean被真正创建的时候,它的属性才会被设置。单例的bean(默认)在容器创建的时候创建,否则只有当bean被使用的时候才会被创建。
面试中会被问到的一个经典问题:如何解决循环依赖?
循环依赖如果是构造器依赖注入造成的,那么spring在运行的时候是可以检测出来,并且抛出BeanCurrentlyInCreationException,所以这也是setter的一个优势。
由于applicationcontext是在实际需要的时候才会去创建对象(非单例的情况下),所以可能会造成运行一段时间之后才发生这种循环依赖的问题。
如果没有循环依赖,那么在A依赖B的时候,容器会完全创建好一个B之后才送来给A使用。
依赖和配置的细节
直接值
可以通过配置一个java.util.Properties,比如有一些属性放在配置文件中,然后可以把它们映射成一个java.util.Properties。
idref
有一个属性需要引用别的字符串(引用别的bean的id,注意只有字符串的时候才有效!),那么可以使用<idref bean="..."/>。当然你也可以直接在value中直接指定。这两种在运行时是完全等价的。但是idref标签可以在部署的时候就立刻发现对应的bean存不存在,这样可以减少拼写错误的几率。
还有一个有用的地方是在AOP拦截器,这个之后会提到。
引用别的beans
利用ref的bean属性来引用其他的bean,只要当前的容器或者父容器中有对应的bean即可,并不要求在同一个xml文件中。bean可以利用id或者名字进行指定。
当使用ref的parent的时候,就必须是在父容器中的bean了。这主要是为了解决子容器中和父容器中的bean有相同的名字,这样如果使用parent就不会有歧义了。
内部bean
内部bean并不要求指定id或者名字(就算你指定了容器也会忽视掉),因为只有外部的bean可以用到它。
有一个例外:在一个单例的bean中有一个request范围的bean,当单例创建的时候,自然内部的这个request的bean也创建了。但是在销毁的时候,由于内部是一个request的bean,所以会参加一个request的生命周期。
集合的继承
这里需要读了bean定义的继承之后才可以看懂。
空和null
如果想在spring框架中使用null,一定要使用<null/>这个明确说明。
Depends-on
这个其实和ref有点类似,不过ref用到的是有明确依赖关系,即一个对象中有一个属性是另外一个对象。而depends-on是没有那么强的依赖关系,但是还是希望能够在当前的bean之前创建,就可以用depends-on
懒初始化
默认情况下,ApplicationContext急切地(eagerly,这个词我觉得很形象)创建并且配置好所有的单例bean。
autowire的四种模式
这个值是在bean标签中的。
no。默认值,对于大型系统,其实是非常推荐使用no,这样可以让整个系统非常明确。byName。spring会为你的属性去寻找对应名字的属性并且尝试自动注入。byType。如果有多个类型匹配,容器会抛出一个异常。但是如果没有匹配的,那就会无事发生哦。constructor。和上面的类型类似,不过只能用在构造器参数上。因为是在构造器上,所以如果没有匹配,那会抛出异常。即容器中有且仅有对应的
byType还可以用在List这种集合中,但是自己试验了没有实现。
实际中推荐不要使用这个,因为它缺点不少:明确的依赖注入会覆盖掉自动注入、自动注入不够精确,可能会导致一些奇奇怪怪的事情、一些别的工具(比如为你的代码生成文档的工具)可能会无法识别这个自动注入。
不让bean使用自动注入
在bean属性中,让autowire-candidate=false,这样这个bean就不会被autowire所使用了(包括@Autowired注解也对其无效)。这个属性设计用来针对类型的,所以如果用了name注入,自动装配仍然会生效。
那如果我就是想连name注入都不让呢?可以在beans标签上(注意,有s哦)使用default-autowire-candidates,这个属性可以指定正则表达式。但是在单独bean中定义的autowire-candidate的属性永远优先。
强调一下:关闭autowire的意思是,当前的这个bean不能被别人用,而不是它自己不能使用别人。
方法注入
spring的IoC 到目前为止看上去都很美好,但是当多个对象它们的生命周期不一样的时候,就会出现有意思的问题。
比如A是一个单例对象,所以在容器创建的时候它就会被创建;它依赖一个B,而B是prototype,根据规则,A会在容器初始化的时候创建,而它依赖的B自然也会被创建。但是A只会被创建一次,也就是它只有一次机会设置B。如果实际中需要多次设置B怎么办呢?
解决这个的其中一个方法是使用控制反转。可以反过来,让bean持有容器的引用,只要这个类实现了ApplicationContextAware接口,
查找方法注入
容器有能力覆盖它管理的bean的方法,并且返回另外一个bean的结果。spring通过CGLIB库来动态修改子类,通过覆盖方法完成的。
当然由于是通过子类完成的,所以类不可以是final,方法也不能是final。
尤其需要注意的是,查找方法并不适用于工厂方法,尤其使用了@Bean这个注解,容器是无法查找方法的,
如果一个方法是无参数,且返回类型确定,修饰符是public或者protected,那么这个方法的返回类型就可以被注入。说到这么一个方法,有没有马上想到接口,接口的抽象方法不就是符合要求的方法么?
这样,就算一个类是单例,但是由于我们把它的一个方法给替换了,每次它自己调用那个方法,就会由容器给他一个新的对象,这样就解决了问题。
如果使用的是注解,请注意spring默认的组件扫描规则是不会去扫描抽象类的,所以可能会导致漏掉,请注意。当然显示声明就没有关系。
任意方法替换
这是另外一种方法注入。如果你不是真的需要,直接跳过这一章。我不需要。但是看看又没关系。
注意!这种方法它侵入性特别强,因为编写代码的人是完全意识不到日后代码会被这种替换,所以谨慎使用。
如果你希望能够替换掉任意的方法,那么你的类需要实现MethodReplacer接口,重写方法。
Bean作用域
spring默认提供了6种作用域,其中4种仅仅在web环境有意义。当然你还可以自己定义作用域。
request:把单个bean的定义限定到单个HTTP请求上。也就是每个request都会有一个单个的bean。
application:等同于ServletContext的生命周期。
单例
当对象是单例的时候,创建完成之后会放到缓存中,接下来就直接在缓存中提取这个。
spring中所说的单例,和设计模式中的单例是不同的。设计模式中定义的单例是,单个classloader只会生成一个对象,而在spring中则是单个容器只有一个对象。所以如果容器不同,那么两个对象必然不可能是同一个对象。
Prototype
保存状态的对象适合使用Prototype,而不保存状态的对象适合使用单例。DAO并不适合配置成Prototype,因为典型的DAO本身并不保存任何状态。
spring并不管理Prototype的完整生命周期。spring仅仅初始化、配置并且装配一个Prototype对象,交给客户端时候就不管了。所以客户端需要自己处理一些资源的释放,或者是使用自定义的bean post-processor,而spring的configured destruction lifecycle callbacks将不会生效。
所以从某种角度上说,在对于Prototype对象上,spring的作用仅仅就是一个new标识符。
其余的web作用域
如果你在常规的IoC 中使用(仅仅声明不使用是没关系的)了web作用域的bean,是会抛出异常的。
如果希望使用这些作用域的bean,需要首先设置一下servlet环境——如果用了spring mvc,那么这个工作由DispatcherServlet来完成。
本质上,DispatcherServlet, RequestContextListener和 RequestContextFilter 做的事情都一样:把HTTP request object绑定到处理这个request的线程上去。
application范围
如果bean声明了application作用域,那么其实可以把它看成是ServletContext的属性。对于每一个ServletContext而言,它独一无二;但是对于容器来说,就不是。
作用域和依赖
如果希望把request作用域的一个bean注入到一个作用域更久的bean中,你可能需要使用AOP代理