在博客 【死磕Spring】----- IOC 之 注册 BeanDefinition 中提到:获取 Document 对象后,会根据该对象和 Resource 资源对象调用 registerBeanDefinitions()
方法,开始注册 BeanDefinitions 之旅。在注册 BeanDefinitions 过程中会调用 parseBeanDefinitions()
开启 BeanDefinition 的解析过程。在该方法中,它会根据命名空间的不同调用不同的方法进行解析,如果是默认的命名空间,则调用 parseDefaultElement()
进行默认标签解析,否则调用 parseCustomElement()
方法进行自定义标签解析。
使用自定义标签
扩展 Spring 自定义标签配置一般需要以下几个步骤:
- 创建一个需要扩展的组件
- 定义一个 XSD 文件,用于描述组件内容
- 创建一个实现 AbstractSingleBeanDefinitionParser 接口的类,用来解析 XSD 文件中的定义和组件定义
- 创建一个 Handler,继承 NamespaceHandlerSupport ,用于将组件注册到 Spring 容器
- 编写 Spring.handlers 和 Spring.schemas 文件
下面就按照上面的步骤来实现一个自定义标签组件。 创建组件 该组件就是一个普通的 JavaBean,没有任何特别之处。
public class User {
private String id;
private String userName;
private String email;
}
定义 XSD 文件
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.cmsblogs.com/schema/user" targetNamespace="http://www.cmsblogs.com/schema/user"
elementFormDefault="qualified">
<xsd:element name="user">
<xsd:complexType>
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="userName" type="xsd:string" />
<xsd:attribute name="email" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:schema>
上面除了对 User 这个 JavaBean 进行了描述外,还定义了 xmlns="http://www.cmsblogs.com/schema/user" targetNamespace="http://www.cmsblogs.com/schema/user"
这两个值,这两个值在后面是有大作用的。 Parser 类 定义一个 Parser 类,该类继承 AbstractSingleBeanDefinitionParser ,并实现 getBeanClass()
和 doParse()
两个方法。主要是用于解析 XSD 文件中的定义和组件定义。
public class UserDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return User.class;
}
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
String id = element.getAttribute("id");
String userName=element.getAttribute("userName");
String email=element.getAttribute("email");
if(StringUtils.hasText(id)){
builder.addPropertyValue("id",id);
}
if(StringUtils.hasText(userName)){
builder.addPropertyValue("userName", userName);
}
if(StringUtils.hasText(email)){
builder.addPropertyValue("email", email);
}
}
}
Handler 类 定义 Handler 类,继承 NamespaceHandlerSupport ,主要目的是将组件注册到 Spring 容器中。
public class UserNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("user",new UserDefinitionParser());
}
}
Spring.handlers
http\://www.cmsblogs.com/schema/user=org.springframework.core.customelement.UserNamespaceHandler
Spring.schemas
http\://www.cmsblogs.com/schema/user.xsd=user.xsd
经过上面几个步骤,就可以使用自定义的标签了。在 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"
xmlns:myTag="http://www.cmsblogs.com/schema/user"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.cmsblogs.com/schema/user http://www.cmsblogs.com/schema/user.xsd">
<myTag:user id="user" email="12233445566@qq.com" userName="chenssy" />
</beans>
测试:
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
User user = (User) context.getBean("user");
System.out.println(user.getUserName() + "----" + user.getEmail());
}
}
运行结果:
解析自定义标签
上面已经演示了 Spring 自定义标签的使用,下面就来分析自定义标签的解析过程。 DefaultBeanDefinitionDocumentReader.parseBeanDefinitions()
负责标签的解析工作,其中它根据命名空间的不同进行不同标签的解析,其中自定义标签由 delegate.parseCustomElement()
实现。如下:
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}
调用 parseCustomElement()
方法,如下:
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取 namespaceUri
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 根据 namespaceUri 获取相应的 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 处理
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
处理过程分为三步:
- 获取 namespaceUri
- 根据 namespaceUri 获取相应的 Handler
- 调用自定义的 Handler 处理
这个处理过程很简单明了,根据 namespaceUri 获取 Handler,这个映射关系我们在 Spring.handlers 中已经定义了,所以只需要找到该类,然后初始化返回,最后调用该 Handler 对象的 parse()
方法处理,该方法我们也提供了实现。所以上面的核心就在于怎么找到该 Handler 类。调用方法为:this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)
getNamespaceHandlerResolver()
方法返回的命名空间的解析器,该解析定义在 XmlReaderContext
中,如下:
public final NamespaceHandlerResolver getNamespaceHandlerResolver() {
return this.namespaceHandlerResolver;
}
这里直接返回,那是在什么时候初始化的呢?这里需要回退到博文:【死磕Spring】----- IOC 之 注册 BeanDefinition ,在这篇博客中提到在注册 BeanDefinition 时,首先是通过 createBeanDefinitionDocumentReader()
获取 Document 解析器 BeanDefinitionDocumentReader 实例,然后调用该实例 registerBeanDefinitions()
方法进行注册。registerBeanDefinitions()
方法需要提供两个参数,一个是 Document 实例 doc,一个是 XmlReaderContext 实例 readerContext,readerContext 实例对象由 createReaderContext()
方法提供。namespaceHandlerResolver 实例对象就是在这个时候初始化的。如下:
public XmlReaderContext createReaderContext(Resource resource) {
return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
this.sourceExtractor, this, getNamespaceHandlerResolver());
}
XmlReaderContext 构造函数中最后一个参数就是 NamespaceHandlerResolver 对象,该对象由 getNamespaceHandlerResolver()
提供,如下:
public NamespaceHandlerResolver getNamespaceHandlerResolver() {
if (this.namespaceHandlerResolver == null) {
this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
}
return this.namespaceHandlerResolver;
}
protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
return new DefaultNamespaceHandlerResolver(cl);
}
所以 getNamespaceHandlerResolver().resolve(namespaceUri)
调用的就是 DefaultNamespaceHandlerResolver 的 resolve()
。如下:
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有已经配置的 Handler 映射
Map<String, Object> handlerMappings = getHandlerMappings();
// 根据 namespaceUri 获取 handler的信息:这里一般都是类路径
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
// 如果已经做过解析,直接返回
return (NamespaceHandler) handlerOrClassName;
}
else {
String className = (String) handlerOrClassName;
try {
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
// 初始化类
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 调用 init() 方法
namespaceHandler.init();
// 记录在缓存
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
"] for namespace [" + namespaceUri + "]", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
className + "] for namespace [" + namespaceUri + "]", err);
}
}
}
首先调用 getHandlerMappings()
获取所有配置文件中的映射关系 handlerMappings ,该关系为 <命名空间,类路径>,然后根据命名空间 namespaceUri 从映射关系中获取相应的信息,如果为空或者已经初始化了就直接返回,否则根据反射对其进行初始化,同时调用其 init()
方法,最后将该 Handler 对象缓存。 init()
方法主要是将自定义标签解析器进行注册,如我们自定义的 init()
:
@Override
public void init() {
registerBeanDefinitionParser("user",new UserDefinitionParser());
}
直接调用父类的 registerBeanDefinitionParser()
方法进行注册:
protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
this.parsers.put(elementName, parser);
}
其实就是将映射关系放在一个 Map 结构的 parsers 对象中:private final Map<String, BeanDefinitionParser> parsers
。 完成后返回 NamespaceHandler 对象,然后调用其 parse()
方法开始自定义标签的解析,如下:
public BeanDefinition parse(Element element, ParserContext parserContext) {
BeanDefinitionParser parser = findParserForElement(element, parserContext);
return (parser != null ? parser.parse(element, parserContext) : null);
}
调用 findParserForElement()
方法获取 BeanDefinitionParser 实例,其实就是获取在 init()
方法里面注册的实例对象。如下:
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
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;
}
获取 localName,在上面的例子中就是 : user,然后从 Map 实例 parsers 中获取 BeanDefinitionParser 对象。返回 BeanDefinitionParser 对象后,调用其 parse()
,该方法在 AbstractBeanDefinitionParser 中实现:
public final BeanDefinition parse(Element element, ParserContext parserContext) {
AbstractBeanDefinition definition = parseInternal(element, parserContext);
if (definition != null && !parserContext.isNested()) {
try {
String id = resolveId(element, definition, parserContext);
if (!StringUtils.hasText(id)) {
parserContext.getReaderContext().error(
"Id is required for element '" + parserContext.getDelegate().getLocalName(element)
+ "' when used as a top-level tag", element);
}
String[] aliases = null;
if (shouldParseNameAsAliases()) {
String name = element.getAttribute(NAME_ATTRIBUTE);
if (StringUtils.hasLength(name)) {
aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
registerBeanDefinition(holder, parserContext.getRegistry());
if (shouldFireEvents()) {
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
postProcessComponentDefinition(componentDefinition);
parserContext.registerComponent(componentDefinition);
}
}
catch (BeanDefinitionStoreException ex) {
String msg = ex.getMessage();
parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element);
return null;
}
}
return definition;
}
核心在方法 parseInternal()
为什么这么说,以为该方法返回的是 AbstractBeanDefinition 对象,从前面默认标签的解析工作中我们就可以判断该方法就是将标签解析为 AbstractBeanDefinition ,且后续代码都是将 AbstractBeanDefinition 转换为 BeanDefinitionHolder,所以真正的解析工作都交由 parseInternal()
实现,如下:
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
// 获取
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
// 获取父类元素
String parentName = getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}
// 获取自定义标签中的 class,这个时候会去调用自定义解析中的 getBeanClass()
Class<?> beanClass = getBeanClass(element);
if (beanClass != null) {
builder.getRawBeanDefinition().setBeanClass(beanClass);
}
else {
// beanClass 为 null,意味着子类并没有重写 getBeanClass() 方法,则尝试去判断是否重写了 getBeanClassName()
String beanClassName = getBeanClassName(element);
if (beanClassName != null) {
builder.getRawBeanDefinition().setBeanClassName(beanClassName);
}
}
builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
if (containingBd != null) {
// Inner bean definition must receive same scope as containing bean.
builder.setScope(containingBd.getScope());
}
if (parserContext.isDefaultLazyInit()) {
// Default-lazy-init applies to custom bean definitions as well.
builder.setLazyInit(true);
}
// 调用子类的 doParse() 进行解析
doParse(element, parserContext, builder);
return builder.getBeanDefinition();
}
在该方法中我们主要关注两个方法:getBeanClass()
、doParse()
。对于 getBeanClass()
方法,AbstractSingleBeanDefinitionParser 类并没有提供具体实现,而是直接返回 null,意味着它希望子类能够重写该方法,当然如果没有重写该方法,这会去调用 getBeanClassName()
,判断子类是否已经重写了该方法。对于 doParse()
则是直接空实现。所以对于 parseInternal()
而言它总是期待它的子类能够实现 getBeanClass()
、doParse()
,其中 doParse()
尤为重要,如果你不提供实现,怎么来解析自定义标签呢?最后将自定义的解析器:UserDefinitionParser 再次回观。
public class UserDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return User.class;
}
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
String id = element.getAttribute("id");
String userName=element.getAttribute("userName");
String email=element.getAttribute("email");
if(StringUtils.hasText(id)){
builder.addPropertyValue("id",id);
}
if(StringUtils.hasText(userName)){
builder.addPropertyValue("userName", userName);
}
if(StringUtils.hasText(email)){
builder.addPropertyValue("email", email);
}
}
}
至此,自定义标签的解析过程已经分析完成了。其实整个过程还是较为简单:首先会加载 handlers 文件,将其中内容进行一个解析,形成 <namespaceUri,类路径> 这样的一个映射,然后根据获取的 namespaceUri 就可以得到相应的类路径,对其进行初始化等到相应的 Handler 对象,调用 parse()
方法,在该方法中根据标签的 localName 得到相应的 BeanDefinitionParser 实例对象,调用 parse()
,该方法定义在 AbstractBeanDefinitionParser 抽象类中,核心逻辑封装在其 parseInternal()
中,该方法返回一个 AbstractBeanDefinition 实例对象,其主要是在 AbstractSingleBeanDefinitionParser 中实现,对于自定义的 Parser 类,其需要实现 getBeanClass()
或者 getBeanClassName()
和 doParse()
。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。