在很多情况下,我们需要为系统提供可配置化支持。因此 spring 提供了可扩展 schema 的支持,扩展spring 自定义标签配置大致需要以下几个步骤:
- 创建一个需要扩展的组件
- 定义一个 XSD 文件描述组件内容
- 创建一个文件,实现 BeanDefinitionParser 接口,用于解析 XSD 文件中的定义和组件定义
- 创建一个 Handler 文件,扩展自 NamespaceHandlerSupport,目的是将组件注册到 Spring 容器
- 编写 spring.handlers 和 spring.schemas 文件
接下就按照上面的步骤来看看 spring 自定义标签的整个过程。
1.解析自定义标签
为了扩展 xml 配置文件的标签,spring 提供了一个机制用于对自定义的元素进行解析。
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
// 通过 w3c 获取节点所在的 namespace URI
String namespaceUri = getNamespaceURI(ele);
// 解析 namespace URI,获取 NamespaceHandler
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 调用自定义的 NamespaceHandler 进行解析
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
方法只做了三件事:
- 获取节点所属的 namespace URI
- 解析 namespace URI 得到 NamespaceHandler
- 调用 NamespaceHandler.parse() 方法解析自定义标签
那么问题来了,NamespaceHandler 是什么?官方文档是这样定义的:NamespaceHandler 是 spring 解析在 xml 配置文件配置的自定义标签的基本接口。因此可以知道我们遇到如 <context: componenet-scan /> 这样的标签时,会实现 NamespaceHandler 接口,可是 spring 怎么知道我自己的 NamespaceHandler 呢?并且 spring 在解析时怎么验证标签配置是否正确呢?是的,我们需要在 META-INF 目录添加两个文件:spring.schemas 和 spring.handlers。spring.schemas 告知 spring 标签的 xsd 文件在哪;spring.handlers 告知 spring 解析标签的 NamespaceHandler 的全路径名。形式分别如下:
http://www.springframework.org/schema/context/spring-context-4.3.xsd=org/springframework/context/config/spring-context-4.3.xsd
http://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
从名字上看 NamespaceHandler 是处理 namespace 的,它并不直接用于解析自定义标签,它底层会委托给 BeanDefinitionParser 接口。那么 BeanDefinitionParser 又是在哪注册的呢?我们在看 NamespaceHandler 接口的文档时,注意到它的 init() 方法,**init() 方法是在解析自定义标签之前被调用。**因此我们可以在 init() 方法里面注册我们自定义的 BeanDefinitionParser 接口。
我们拿 <context: component-scan /> 举例子,看 spring 本身怎么做的。在上面说明 spring.schemas 和 spring.handlers 文件时就是拿它作为示例,因此就不再说了,同时也可以看到,此标签的 NamespaceHandler 是 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());
// 对 componenet-scan 的解析
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());
}
}
public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
// ...省略字段定义
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
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);
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
return null;
}
// ...省略其他方法
}
所以了解其工作原理之后,发现也并不负载,主要 Spring 帮我们定义好了开发标准,我们只需要按照它的约定去实现我们自己的逻辑就可以。如果看过 dubbo 源码的肯定也知道,dubbo 跟 spring 集成,也是因为 dubbo 定义了自己的 DubboNamespaceHandler,注册了对各个标签的解析器。