6

系统学习Spring源码-bean注册

 1 year ago
source link: https://dcbupt.github.io/2020/04/23/blog_article/%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0%E7%B3%BB%E5%88%97/%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0Spring%E6%BA%90%E7%A0%81-bean%E6%B3%A8%E5%86%8C/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

bean 定义分为两大类,一种是在资源文件里定义 bean,另一种是注解方式。
资源文件可以是 groovy 文件、properties 文件或者 xml 文件,本文介绍 Spring 是如何注册在 xml 里定义的 beanDefinition。

Resource

Spring 定义接口 Resource 来表示资源文件。通过 Resource 能拿到文件的输入流、File 对象等

public interface Resource extends InputStreamSource {



File getFile() throws IOException;

}

public interface InputStreamSource {

InputStream getInputStream() throws IOException;

}

ClassPathResource

ClassPathResource 表示从类路径 ClassPath 下加载的资源文件。看下它的构造函数。

构造函数 1:

  • 第一个参数是资源文件在 classpath 下的路径
    • 这里通常传入的就是 bean 定义的资源文件,例如 xml 文件
    • 不需要以”/“开头
  • 第二个参数是 CL,如果传 null,由 ClassUtils 取默认的线程上下文加载器
   public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
Assert.notNull(path, "Path must not be null");
String pathToUse = StringUtils.cleanPath(path);
if (pathToUse.startsWith("/")) {
pathToUse = pathToUse.substring(1);
}
this.path = pathToUse;
this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}

构造函数 2:

  • 如果构造函数传入 Class,则资源文件必须在传入 Class 所在包路径下,path 是资源文件在 Class 所在包下的相对路径,也不用以”/“开头
   public ClassPathResource(String path, @Nullable Class<?> clazz) {
Assert.notNull(path, "Path must not be null");
this.path = StringUtils.cleanPath(path);
this.clazz = clazz;
}

通过 CL 加载得到类路径下的资源文件输入流

  • 如果构造 ClassPathResource 时传入了 Class,则加载资源文件输入流的是加载 Class 的 ClassLoader
   public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
}

测试代码:
beandefinition.BeanDefinitionTests#testClassPathResource

BeanDefinitionReader

BeanDefinitionReader 是 beanDefinition 的解析注册器,负责读取资源文件 Resource 并解析得到 beanDefinition,然后注册到 bean 工厂

  • 每种资源文件(xml、groovy、properties 类型)都有对应的 BeanDefinitionReader 实现类
public interface BeanDefinitionReader {

BeanDefinitionRegistry getRegistry();

int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;



}

XmlBeanDefinitionReader

XmlBeanDefinitionReader 是 xml 方式定义 beanDefinition 的解析注册器,它会按 xml 格式解析 Resource 得到 beanDefinition

  • xml 文件一般放在 ClassPath 下,所以 Resource 的类型是 ClassPathResource
public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
super(registry);
}

调用 loadBeanDefinitions 方法开始 bd 的解析和注册流程。

解析 xml

解析 xml 直接使用 jdk 自带的工具类:

  • 把 xml 文件输入流包装为 InputSource 对象
  • 使用 DocumentBuilder 解析 InputSource 得到 Document 对象
    • 通过 Document 可以方便获取 xml 的标签元素
    • DTD 和 XSD 是两种常用的 XML 文档格式定义规范,在 xml 文件头部指定,目前主流的都使用 XSD 规范。构造 DocumentBuilder 时指定按哪种规范来校验和解析 xml
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
...
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isDebugEnabled()) {
logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}

拿到 Document 对象说明 xml 解析完成。

构建 beanDefinition

调用 BeanDefinitionDocumentReader 的 registerBeanDefinitions 方法完成 beanDefinition 的构建和注册流程

  • DefaultBeanDefinitionDocumentReader 是 BeanDefinitionDocumentReader 的默认实现
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
// 由DocumentReader负责读取Document并注册bd到beanFactory
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}

构建 beanDefiniton 前,需要先从 document 拿到根 root 元素

Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);

构建 beanDefinition 的过程拆解如下。

profile 环境隔离

如果根标签是 Spring 的 beans 标签,读取 profile 属性指定的环境标,如果与当前环境标匹配,则注册 beans 标签内定义的 bean,否则忽略这些 bean

if (this.delegate.isDefaultNamespace(root)) {
// 如果beans使用profile加载机制,则跳过不需要在当前环境加载的beans
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

acceptsProfiles 方法遍历所有 profile 属性指定的环境标,只要有一个 profile 环境标在当前环境标中,就认为匹配成功,可以注册 bean

当前环境标有两部分组成:activeProfiles 和 defaultProfiles

  • ActiveProfiles 集合是从系统变量或环境变量里,通过 key=spring.profiles.active拿到 value,按”,”分隔后的集合。默认的 ActiveProfiles 为空,可以通过几种方式指定环境标:
    • 显式添加到系统属性:System.setProperty("spring.profiles.active", "development");
    • web.xml 里配置 spring.profiles.active 到 context-param
    • 使用注解:@ActiveProfiles(“test”)
  • DefaultProfiles 集合类似,只不过 key=spring.profiles.default。Spring 默认有一个预留 reserved 的 DefaultProfile=”default”,如果用户有通过环境变量设置了,则替换

profile 环境标和当前环境标匹配规则为:优先匹配 activeProfiles,如果 activeProfiles 为空,再匹配 defaultProfiles

public boolean acceptsProfiles(String... profiles) {
Assert.notEmpty(profiles, "Must specify at least one profile");
for (String profile : profiles) {
if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
if (!isProfileActive(profile.substring(1))) {
return true;
}
}
else if (isProfileActive(profile)) {
return true;
}
}
return false;
}

protected boolean isProfileActive(String profile) {
validateProfile(profile);
Set<String> currentActiveProfiles = doGetActiveProfiles();
return (currentActiveProfiles.contains(profile) ||
(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
}

测试代码:beandefinition.BeanDefinitionTests#testProfile

前置和后置钩子

DefaultBeanDefinitionDocumentReader 提供了钩子函数,在 bd 注册前和注册后回调,用户可以继承DefaultBeanDefinitionDocumentReader来自定义逻辑

preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

测试代码:beandefinition.BeanDefinitionTests#customDefaultBeanDefinitionDocumentReader

处理子标签

如果是 beans 命名空间,遍历 beans 的所有子标签,读取并构建 beanDefinition,然后注册
如果不是 beans 命名空间,说明是用户自定义的 xml,走自定义标签处理流程。不过目标也是读取并构建 beanDefinition,然后注册

  • debug 发现,孩子节点是行维度的,空行或者注释的行,也是一个孩子。所以 xml 文件尽量写得紧凑些,避免产生一些无用的孩子节点
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
// beans->bean->property
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)) {
parseDefaultElement(ele, delegate);
}
else {
// 解析自定义标签,生成BeanDefinition并注册到beanFactory
// beans标签的解析由Spring完成,自定义标签的解析由用户自定义parser完成
// 例如<myname:user .../>,myname就是命名空间,Spring通过命名空间找到handler,通过handler找到标签user对应的parser完成自定义标签的解析和注册
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

beans 的子标签目前有四个:import、alias、bean、beans(嵌套方式),重点是解析 bean 标签

  • 对于 import 子标签,加载 resource 属性指定的资源文件,然后再重新调用 xmlBeanDefinitionReader#loadBeanDefinitions 方法,走一遍解析 xml 并注册 bean
  • 对于 alias 子标签,别名与 beanName 的映射关系注册到 bf
    • 测试代码:beandefinition.BeanDefinitionTests#testBeanNameAlias
  • 对于嵌套 beans 标签,当做根标签处理
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
// 加载import resource指定的bean定义文件,注册beanDefinition到beanFactory
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
// 别名与beanName的映射关系注册到beanFactory
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
// 注册beanDefinition到beanFactory
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
// 注册嵌套式定义的beans到beanFactory。嵌套式即beans标签里还嵌套了beans标签
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}

解析 bean 标签

对于 bean 标签,需要解析得到 beanDefinition 并注册

先确定 beanName 和别名

  • beanName 优先取 id 属性,为空再取 name 属性按”,”或”;”分隔后的第一个字符串。如果 name 属性也空,由 spring 生成 BeanName,格式为:{类的全路径名}#0,别名为:类的全路径名
  • 别名取 name 属性按”,”或”;”分隔后的多个字符串,如果 beanName 用了第一个字符串,则别名要把它排掉

注意,getBean 方法可以传入别名,但 getBeanDefinition 方法只能传入 beanName,不认别名。测试代码:beandefinition.BeanDefinitionTests#testBeanName

String id = ele.getAttribute(ID_ATTRIBUTE);
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);

List<String> aliases = new ArrayList<>();
if (StringUtils.hasLength(nameAttr)) {
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
aliases.addAll(Arrays.asList(nameArr));
}

String beanName = id;
// 如果bean的id为空,取第一个name作为beanName
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
beanName = aliases.remove(0);
if (logger.isDebugEnabled()) {
logger.debug("No XML 'id' specified - using '" + beanName +
"' as bean name and " + aliases + " as aliases");
}
}

解析 bean 标签的属性和子标签,封装到 BeanDefinition

// bd最基本的属性是CL加载的bean类对象
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
// bean的各种框架属性封装到bd
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
// 解析bean的meta子标签属性(bean属性之外的键值对)并添加到bd。通过bd的getMetadataAttribute可以获取meta属性
parseMetaElements(ele, bd);
// 解析lookup-method。bean方法声明一个父类返回结果,实际返回bean在lookup-method中声明
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
// 解析replaced-method。replaced-method用于完全替换bean方法的实现
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
// 解析bean的constructor-arg子标签得到构造函数参数,添加到bd.constructorArgumentValues
parseConstructorArgElements(ele, bd);
// 解析bean的property子标签得到业务属性,添加到bd.propertyValues。如果属性值是一个ref,封装成RuntimeBeanReference,如果是一个value,封装成TypedStringValue
parsePropertyElements(ele, bd);
// 解析bean的qualifier子标签,添加到bd.qualifiers。qualifier的作用是按byName声明要注入的实际bean
parseQualifierElements(ele, bd);

bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));

return bd;

一些比较重要的 bean 标签属性:

  • autowire:如果 bean 的引用类型的属性没有用@Autowire 或@Resource 修饰,可以在 xml 里使用 autowire 属性统一指定依赖注入方式,一般为 byName 或 byType
  • scope:bean 的作用域,一般为 singleton 或者 prototype
  • lazy-init:如果想要 bean 懒加载,lazy-init 设为 true
  • depends-on:指定当前 bean 前置依赖的一些 beanNames,用,或者;分割
  • primary:为 true 表示当前 bean 是 primary 的,byType 加载到多个 bean 时,取 primary bean
  • init-method:设置 bean 初始化阶段回调的 init 方法名
  • destroy-method:设置 bean 销毁前回调的 destroy 方法名

一些比较重要的 bean 子标签的:

  • meta:额外定义一些 bean 的元属性 KV,保存到 BD。只能通过 BD#getAttribute 方法传入 K 获得 V,不会作为 bean 实例的属性,即元属性的含义
    • 测试代码:beandefinition.BeanDefinitionTests#testMeta
  • lookup-method:指定 lookup-method 标签声明的方法的返回值(bean)
    • 配置 lookup-method 的 bean,会被 CgLIB 代理,增强逻辑是从 bd.methodOverrides 获取 lookup-method 方法返回的 beanName,加载 bean 作为方法的返回值
    • 注意不是属性的依赖注入,只是用动态代理强制指定了方法返回的 bean
    • 测试代码:instantiation.before.lookup.LookupTest
  • property:指定依赖注入的值。如果是一个 ref,封装成 RuntimeBeanReference,加载得到注入的 bean。如果是一个 value,封装成 TypedStringValue,当做字符串注入

解析自定义标签

所谓自定义标签,即标签的命名空间不是 Spring 的 beans 命名空间
Spring 提供了基于自定义命名空间获取对应标签处理器NamespaceHandler的机制,由 NamespaceHandler 负责解析自定义标签,生成 BD 并注册

public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取自定义标签的命名空间,Spring默认命名空间是beans
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 找到该命名空间的自定义Handler
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 通过handler找到自定义parser,解析自定义标签,生成beanDefinition注册到beanFactory
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

第一次解析自定义标签时,会加载描述命名空间和NamespaceHandler映射关系的资源文件,默认是类路径下的META-INF/spring.handlers文件,然后反射实例化 handler,再调用它的 init 方法注册该命名空间下的所有标签解析器BeanDefinitionParser,最后将命名空间和NamespaceHandler的映射关系放入 map 缓存

   // handlerMappingsLocation默认为:META-INF/spring.handlers,classLoader默认为ClassUtils.getDefaultClassLoader方法返回的线程上下文加载器
Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
... ...
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 注册用于解析自定义标签的用户实现的BeanDefinitionParser
namespaceHandler.init();
handlerMappings.put(namespaceUri, namespaceHandler);

示例:
SimpleConstructorNamespaceHandler支持所有”<c:xxx>”标签的解析,c 是命名空间,xxx 是标签名

http\://www.springframework.org/schema/c=org.springframework.beans.factory.xml.SimpleConstructorNamespaceHandler

NamespaceHandler的 parse 方法,会通过标签名获取标签解析器BeanDefinitionParser,调用 parse 方法解析标签,生成 BeanDefinition。通过 ParserContext 能拿到 beanFactory,至于是否注册 beanDefinition 到 beanFactory,由 parse 方法的实现决定

   private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 获取命名空间后面的标签名,例如<mybean:user xxx/>,标签名就是user
String localName = parserContext.getDelegate().getLocalName(element);
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}

如果使用自定义标签,需要:

  • 写一个自定义命名空间下的 XML 文档协议规范 XSD
  • 实现 NamespaceHandler 和标签解析器 BeanDefinitionParser
  • 将命名空间和 NamespaceHandler 的映射关系写入META-INF/spring.handlers资源文件
  • 在 xml 文件中声明自定义的命名空间

BD 注册到 bean 工厂

注册即 beanDefinition 添加到 beanFactory.beanDefinitionMap,key=beanName,别名注册到 beanFactory.aliasMap,key=alias,value=beanName

   public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {

// Register bean definition under primary name.
String beanName = definitionHolder.getBeanName();
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

// Register aliases for bean name, if any.
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}

bean 工厂

bean 工厂的主要职责是管理注册的 beanDefinition 以及加载 bean,即实现所谓的 IoC 控制反转

DefaultListableBeanFactory是 Spring 最常用的 bean 工厂,XmlBeanFactory是它的子类,可以理解为XML配置方式得到的bean工厂,使用XmlBeanDefinitionReader解析 XML 配置文件、生成 beanDefinition 并注册到 bean 工厂

bean 工厂本身并不负责解析各种方式(注解、XML)的 bean 定义并生成 beanDefinition,它只是拿到 beanDefinition 执行后续 bean 注册、加载流程。bean 定义的解析注册由各种BeanDefinitionReader完成,它持有 bean 工厂,生成 beanDefinition 后回调 bean 工厂的注册方法注册 bean

注意,XmlBeanFactory 现在已经被 Spring 废弃,替代它的是ClassPathXmlApplicationContext

ClassPathXmlApplicationContext本身是一个 Spring 容器ApplicationContext,在构造函数中创建好DefaultListableBeanFactory,其中 xml 解析和注册 beanDefinition 仍然通过XmlBeanDefinitionReader

Spring 容器ApplicationContext具备 bean 工厂的全部能力,并提供了对于 bean 工厂更多的定制和扩展能力,后文会详细介绍 Spring 容器

xml%E6%96%B9%E5%BC%8Fbean%E8%A7%A3%E6%9E%90%E6%B3%A8%E5%86%8C%E6%B5%81%E7%A8%8B.png

UML 类图

xml%E6%96%B9%E5%BC%8Fbean%E6%B3%A8%E5%86%8C%E7%B1%BB%E5%9B%BE.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK