Spring扫描一个Java Bean到内部容器,可以有两种方式。
- 第一种在xml里面配置<bean/>标签,指定扫描路径下的类文件。
<bean class="com.wintig.bean.Student" id="student" scope="singleton" primary="true"/>
- 第二种使用一个特殊的标签,指定要扫描路径下的所有文件
<context:component-scan base-package="com.wintig"/>
第二种明显更试用与我们的工程,毕竟类文件那么多,我们不可能一个个去配置吧。底层究竟是如何实现的,今天我们就来揭开这里面的奥妙。
Java Bean和Spring Bean
Java Bean简单来说就是运行在JVM平台上的一个个对象,我们平常New User()会经过一系列加载验证,然后从方法区中获取类的模板信息,然后基于类模板信息在堆中为其分配内存。所以Class就是JVM建模对象。
Spring对Class又进一步的增强,因为Class里面的基础信息并不能满足Spring的业务需要,所以Spring引入BeanDegingion,里面不仅仅包含了类的Class信息,还包括比如bean的作用域,是否懒加载,是否是个抽象类等信息。
所以Spring中的Bean就是基于DeanDefinition抽象出来的信息,然后经历一系列生命周期而生成类,就叫Spring的Bean对象。
自定义标签讲解
首先启动Spring容器使用ClassPathXmlApplicationContext去加载spring.xml(配置如上)
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
}
顺着ClassPathXmlApplicationContext的refresh()方法,就来到了我们今天的核心方法obtainFreshBeanFactory()
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
这个方法主要做的事情很好理解
- 创建一个BeanFactory工厂对象
- 之后根据我们传入的"spring.xml"对里面的配置类进行解析
- 把解析出来的xml标签封装成BeanDefinition对象
也很好理解,spring也没有什么神秘的。其实是这样的,前2步虽然描述很简单,但是具体实现也只是多了很多校验和为了扩展性做的设计在里面,都很好想到。但是第3步就要费点脑筋了,对标签的解析我们最开始也说道了,Spring有两类标签形如<Bean>的默认标签,我们可以直接解析;还有一类<context:component-scan>自定义标签。
这两种标签的解析最终是由parseBeanDefinitions方法去完成的,我们接下来分情况进行讲解。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
// 从xml的根节点开始,循环的解析所有的变迁
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
// 是否有namespace,如果拿不到说明是默认标签
if (delegate.isDefaultNamespace(ele)) {
// 默认标签解析 <bean>
parseDefaultElement(ele, delegate);
}
else {
// 自定义标签解析 有前缀的 <context:component-scan base-package="com.wintig"/>
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
接下来,我们分别对这两类标签进行展开解析。
默认标签
默认标签标签除了上面所说的<bean>,还有<import>、<alias>、<beans>核心的其实就是bean标签。默认标签其实没啥好讲的,因为标签里面已经包含了所有的信息。
自定义标签
默认标签很简单吧,但是我们请看spring是如何判断一个标签是默认标签或自定义标签的?它通过了一个isDefaultNamespace
方法来判断,意思就是说如果这个标签没有对应的namespace他就属于默认标签。namespace是什么?他就是xml文件最上面的xmlns约束。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
<context:component-scan base-package="com.wintig"/>
</beans>
这时候我们来到了<context>标签,我们扫描到了<context>标签有对应的namespace所以会走自定义标签解析,就会进入parseCustomElement
方法。
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// <context:component-scan/>
// 根据标签拿到namespaceUri,根据namespaceUri拿到对应的NamespaceHandler处理类
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 解析默认标签的时候调用的decorate(装饰),而这里调用的是parse(解析)
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
parseCustomElement
parseCustomElement方法首先调用了一个getNamespaceURI方法,这个方法是根据<context>标签拿到跟后根据xml上面的xmlns获取到namespaceUri。<context>标签对应的namespaceUri是http://www.springframework.org/schema/context
resolve
根据标签然后拿到namespaceUri,接下来就进入了解析过程,这个方法很复杂我对代码进行了简化如下:
public NamespaceHandler resolve(String namespaceUri) {
// 获取spring中所有jar包里面的"/META-INF/spring.handlers"文件
// 并且建立uri-class的映射关系
Map<String, Object> handlerMappings = getHandlerMappings();
// 根据namespaceUri=http://www.springframework.org/schema/p,获取这个命名空间的处理类
// 通过建立的uri-class的映射关系,通过一个namespaceUri就可以唯一的确定一个处理类
Object handlerOrClassName = handlerMappings.get(namespaceUri);
String className = (String) handlerOrClassName;
// 反射获取Class
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
// 然后实例化
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 调用处理类的init方法,在init方法中完成标签元素解析类的注册
namespaceHandler.init();
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
getHandlerMappings()
首先会调用getHandlerMappings方法,接下来根据根据namespaceUri拿到对应的处理类的映射关系,这里使用了SPI的思想。spring会加载所有jar包下面的resource/META-INF/spring.handlers
文件,我们打开spring.handlers
文件发现里面存着一系列的url-class的映射关系结构的信息如下,根据如下信息建立映射关系。
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler
然后我们namespaceUri拿到对应的NamespaceHandler处理类。然后根据http://www.springframework.org/schema/context
就可以找到一个对应的处理类org.springframework.context.config.ContextNamespaceHandler
public class ContextNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}
}
接下来的操作就很简单了
- 反射生成类
- 调用init()方法
- 把标签对应的处理类注册到spring容器里面
- 建立namespaceUri和处理类的映射关系
我们有了NamespaceHandler处理类,接下来就根据子标签<context:component-scan/>
获取子标签component-scan
获取子标签处理类ComponentScanBeanDefinitionParser然后调用它的parse方法
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 获取basePackage属性:要扫描的路径
String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
// 可以用逗号分开
String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
// Actually scan for bean definitions and register them.
// 创建注解扫描器
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
// + 扫描并把扫描的类封装成beanDefinition对象
// 1 根据basePackage路径,递归扫描文件夹
// 2 使用ASM技术,把找到的类封装成metadataReader,
// 3 遍历metadataReader,如果类属性上有注解则完成bd注册
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
// + 基本组件注册 beanPostProcess
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
return null;
}
到这里其实已经很清楚了,自定义标签的实质其实就是配置namespace告诉spring你要处理的这个标签我来处理,然后根据spi找到对应的处理类。不同的子标签对应不同的处理逻辑,这也就是策略模式。