1. 如何使用IOC容器
- 在resource中创建app.xml(我是用maven构建的项目)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="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">
<bean id="dog" class="com.sanjin.blog04.Dog"></bean>
</beans>
- app.xml中加载了一个bean,所以我还需要创建一个Dog类
public class Dog {
public void show() {
System.out.println("I am a dog!");
}
}
- 将bean加载到Spring容器
public class Main {
public static void main(String[] args) {
// 1. 创建资源
ClassPathResource resource = new ClassPathResource("app.xml");
// 2. 创建一个factory,用户存储bean(factory内部有个hashMap存放)
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 3. 创建一个reader,用户读取资源,读取的bean会放在一个factory中
// 所以构造器中需要一个 factory
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
// 4. 加载资源
reader.loadBeanDefinitions(resource);
// 5. 从Spring容器中获取bean
Dog dog = factory.getBean(Dog.class);
dog.show();
}
}
通过上面一系列代码,我们可以发现初始化一个Spring容器有几个关键点
- resource:加载哪些Bean,需要资源来指定
- beanFactory:存放加载的Bean,取Bean也是从这里取
- reader:解析resource,并注册bean到beanFactory中
整个过程分为资源定位->装载->注册可以用这张图概括:
资源定位我们在 硬怼Spring-资源加载(三)探讨过。
文章第二部分就探讨Spring如何从一个xml文件解析出bean
2. BeanDefinition的加载
从reader.loadBeanDefinitions(resource);
出发
1. reader.loadBeanDefinitions(resource);
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
只是个代理方法,实际调用另一个重载方法。使用EncodedResource
对象作为参数,进入EncodedResource看看,这个类最主要的就是这个方法
/**
* 将resource转换为Reader,并使用指定的编码方式
* Open a {@code java.io.Reader} for the specified resource, using the specified
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}
* (if any).
* @throws IOException if opening the Reader failed
* @see #requiresReader()
* @see #getInputStream()
*/
public Reader getReader() throws IOException {
if (this.charset != null) {
return new InputStreamReader(this.resource.getInputStream(), this.charset);
}
else if (this.encoding != null) {
return new InputStreamReader(this.resource.getInputStream(), this.encoding);
}
else {
return new InputStreamReader(this.resource.getInputStream());
}
}
这个类的主要作用就是将 Resource 转换为 Reader。
2. loadBeanDefinitions(new EncodedResource(resource))
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}
// this.resourcesCurrentlyBeingLoaded 是一个ThreadLocal类型
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
// 判断encodedResource是否已经在currentResources中
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 继续调用 doLoadBeanDefinitions 方法,添加了 inputSource 参数
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
3. doLoadBeanDefinitions(inputSource, encodedResource.getResource());
相比这次就是真实干事情的方法了,毕竟加了个do。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
// Document 就是HTMl中的DOM树,可以用来解析XML或者HTML标签
Document doc = doLoadDocument(inputSource, resource);
// 将解析出来的bean注册到factory中
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
先说一下Document doc = doLoadDocument(inputSource, resource);这个方法主要作用就是解析我们classpath中的app.xml,抛开Spring不说,我们看看如何使用Java解析xml文件,我还是使用开头的app.xml举例,获取bean标签id="dog"的class值:
Java解析XMl有许多方式,但不管哪种方式,也都是调用不同包下的API。
- javax.xml包下API
- org.dom4j包下API
-
org.jdom包下API
xml文件:
我尝试了第一种包下API:
public class PraseXMLTest {
public static void main(String[] args) {
// 加载 resource 下的 app.xml 文件
// ClassPathResource 这个类是 Spring 提供的
ClassPathResource resource = new ClassPathResource("app.xml");
// 使用 javax.xml.* 下API
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = null;
try {
builder = factory.newDocumentBuilder();
// builder.parse()有许多重载的方法,返回 Document 对象
// 通过 Document 我们就可以获取DOM树种所有的元素信息了
Document document = builder.parse(resource.getInputStream());
// 下面我们来获取 bean标签的 id class 属性的值
// 1. 获取所有的bean标签
NodeList beanNodeList = document.getElementsByTagName("bean");
for (int i = 0; i < beanNodeList.getLength(); i++) {
Node beanNode = beanNodeList.item(i);
// 2.获取 bean 标签的所有属性
NamedNodeMap attributes = beanNode.getAttributes();
// 获取 id 属性的值
Node id = attributes.getNamedItem("id");
// 获取 class 属性的值
Node clazz = attributes.getNamedItem("class");
System.out.println("id 属性值:" + id.getNodeValue());
System.out.println("class 属性值:" + clazz.getNodeValue());
}
} catch (ParserConfigurationException | SAXException | IOException e) {
e.printStackTrace();
}
}
}
打印结果:
尝试了上面的例子后,我们就可以明白第一个函数
Document doc = doLoadDocument(inputSource, resource);
的作用:解析XMl文件生成一个Document对象,通过这个Document对象就可以获取DOM树中一切元素节点的信息(DOM是HTML中的一个概念)。以便于后面通过反射生成bean对象。Spring内部解析XML使用的API和例子里的相同。
3. BeanDefinition的注册
BeanDefinition的注册流程就包含在registerBeanDefinitions(doc, resource):
这个方法冲名字我们就可以猜出它的作用:从doc(Document)对象中获取bean的配置信息(id,class,lazyInit等等)注册到factory中。
从源码看内部细节:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// documentReader 对象用于解析bean的配置信息,如id,class等等
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 还记得开头我们的例子中
// XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
// factory 其实是个 DefaultListableBeanDefinition 对象,是Spring默认实现的一个容器
// getRegistry() 可以获取到我们传入的 factory 对象
// 获取没有注册bean之前 Spring 容器中的 bean 的个数
int countBefore = getRegistry().getBeanDefinitionCount();
// 获取注册信息,并加载到 factory 中,跟进
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 返回 factory 加载的 bean 的数量
return getRegistry().getBeanDefinitionCount() - countBefore;
}
跟进 documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}
想必其中玄机就在doRegisterBeanDefinitions中了,继续跟进:
doRegisterBeanDefinitions()源码种我们主要关注parseBeanDefinitions(root, this.delegate);:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 通过 Element 解析XML文件
// 注意此处的 if 判断是否为默认的 namespace
// 只有XML文件是默认的 namespace Spring才会正常解析
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// 解析 Element 获取 bean 的配置信息,跟进这个方法
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
XML文件 namespace
命名空间都是用来解决命名冲突问题,注意Spring XML 文件的命名空间,比如我们开头使用的app.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="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">
<bean id="dog" class="com.sanjin.blog04.Dog"></bean>
</beans>
xmlns="http://www.springframework.org/schema/beans"
xmlns
(XML namespace)属性用于指定命名空间,后面的url标识这个XML属于Spring bean的命名空间,我们的Spring xml只有指定这个命名空间才能被Spring正常解析。
跟进parseDefaultElement(ele, delegate);方法:
// 对于不同的标签进行不同的处理
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { // import 标签
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { // alias
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { // bean 标签,我们主要关注
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // beans 标签
// recurse
doRegisterBeanDefinitions(ele);
}
}
对于不同的XML标签需要用不同方法处理,我们只关心bean标签的处理,所以继续跟进processBeanDefinition(ele, delegate);
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
// 获取 beanDefinitionHolder
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// 将 beanDefinition 注册进 factory 中的 beanDefinitionMap 中,跟进该方法
// Register the final decorated instance.
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// 派发 注册 事件
// Send registration event.
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
这个方法中,我们主要关心这行代码:
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
这个方法的两个参数中
第一个参数
bdHolder
(beanDefinitionHolder)是一种Holder的设计模式,内部保存了完整的beanDefinition
对象和bean的名称以及bean的别名。
跟进delegate.parseBeanDefinitionElement(ele);方法,我们主要关心beanName的生成:
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
// 获取 bean 标签 id属性,只能设置一个
String id = ele.getAttribute(ID_ATTRIBUTE);
// 获取 bean 标签 name 属性,可以设置多个,name标识bean的别名
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
List<String> aliases = new ArrayList<>();
if (StringUtils.hasLength(nameAttr)) {
// MULTI_VALUE_ATTRIBUTE_DELIMITERS ->
// public static final String MULTI_VALUE_ATTRIBUTE_DELIMITERS = ",; ";
// 分隔 nameAttr 来获取别名
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
aliases.addAll(Arrays.asList(nameArr));
}
// 1. beanName 默认是 id
String beanName = id;
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
// 若 id 为空,并且 别名不为空,beanName为第一个别名
beanName = aliases.remove(0);
if (logger.isTraceEnabled()) {
logger.trace("No XML 'id' specified - using '" + beanName +
"' as bean name and " + aliases + " as aliases");
}
}
if (containingBean == null) {
// 验证beanName,别名是否唯一
checkNameUniqueness(beanName, aliases, ele);
}
// 根据 XML bean标签配置生成 beanDifinition 对象
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
// 这种情况是 id没有设置,别名也没设置
if (!StringUtils.hasText(beanName)) {
try {
// containingBean 表示这个 bean 标签是否嵌套 bean 标签,
if (containingBean != null) {
// 默认生成的 beanName 是 全类名+“#”+十六进制
// 如 com.sanjin.blog04.Dog#F3
beanName = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, this.readerContext.getRegistry(), true);
}
else {
// 若没有嵌套bean标签,采用这种方式生成beanName,生成的beanName就是
// 全类名+“#”+数字,如 com.sanjin.blog04.Dog#0
// 数字含义:0 表示 Dog 的第一个对象,1 表示第二对象
beanName = this.readerContext.generateBeanName(beanDefinition);
// Register an alias for the plain bean class name, if still possible,
// if the generator returned the class name plus a suffix.
// This is expected for Spring 1.2/2.0 backwards compatibility.
// 获取全类名,如 com.sanjin.blog04.Dog
String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName != null &&
beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
aliases.add(beanClassName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Neither XML 'id' nor 'name' specified - " +
"using generated bean name [" + beanName + "]");
}
}
catch (Exception ex) {
error(ex.getMessage(), ele);
return null;
}
}
String[] aliasesArray = StringUtils.toStringArray(aliases);
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}
return null;
}
代码很长,我们只需要记住这个关于beanDefinition 的 beanName结论就行:
- 若bean标签设置了 id,id即为beanName
- 若bean标签未设置id,设置了name,name第一个值为beanName,其他值为别名(alias)
- 若bean标签未设置id,未设置name,则beanName需要分情况:
3.1 若该bean标签为嵌套bean标签
beanName为 全类名+“#”+十六进制,如 com.sanjin.blog04.Dog#F3
3.2 若bean标签不是嵌套bean标签
beanName为 全类名+“#”+数字,如 com.sanjin.blog04.Dog#0,数字含义:0 表示 Dog 的第一个对象,1 表示第二对象
第二个参数
getReaderContext().getRegistry()
返回的就是我们开头 new 出来的 factory,这个factory 就是我们常常所说的 Spring 容器,BeanDefinition的注册就是指把 BeanDefinition注册进 factory中。
此处需要说明一下 BeanDefinition 是个傻子东西
Java是一种面向对象语言,一切皆对象,比如房子我们可以把它当成房子对象,并用宽,高,平方,地理位置等等信息来描述它。那么Java bean 我们如何来描述呢?Spring 使用 BeanDefinition 来描述,比如这个bean是否单例,是否懒加载等等。具体信息可以查看 BeanDefinition 接口源码。
离目标已经很近了,继续跟进BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {
// Register bean definition under primary name.
// 1. 获取 beanDifinition 的主名称,跟进
String beanName = definitionHolder.getBeanName();
// 2. 使用 主名称 注册 beanDefinition,跟进
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
// Register aliases for bean name, if any.
// 3. 注册 beanDefinition 的别名
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}
跟进第一个步骤definitionHolder.getBeanName(),来看看Spring 是如何判断一个bean的主名称:
/**
* Return the primary name of the bean, as specified for the bean definition.
*/
public String getBeanName() {
return this.beanName;
}
直接返回了beanName,说明在new BeanDefinitionHolder时候就已经定义了beanName,关于beanName这个问题很重要,前面我们以及讨论过了。下面看一下registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition())源码,指贴出主要代码:
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) { // 判断该 beanName 是否已经存在
// 判断是否允许 bean 重写。Spring 有2种情况:
// 1. 如果在同一个xml文件中有2个 bean 名字相同,Spring 会报错
// 2. 如果在不同xml文件种有2个 bean 名字相同,Spring 默认会覆盖先前的 bean,
// 在实际编码种我们也可以设置为 不允许重写
if (!isAllowBeanDefinitionOverriding()) {
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
}
// 覆盖之前beanName的BeanDefinition,
// beanDefinitionMap 保存了 beanDefinition
this.beanDefinitionMap.put(beanName, beanDefinition);
else {
if (hasBeanCreationStarted()) { // 若bean已经被创建过
// Cannot modify startup-time collection elements anymore (for stable iteration)
// 对beanDefinitionMap加锁
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
removeManualSingletonName(beanName);
}
}
else { // bean 没有被创建过
// Still in startup registration phase
this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);
removeManualSingletonName(beanName);
}
this.frozenBeanDefinitionNames = null;
}
if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
上面就是bean的注册过程
4. 本文总结:
- 如何XML中bean标签生成BeanDefinition
- BeanDefinition的beanName在不同情况如何生成
- BeanDefinition如何注册进 defaultListableBeanFactory