创意电子

标题: Spring 源码学习(三)-自定义标签 [打印本页]

作者: 科技伍小黑    时间: 2020-1-22 14:59
标题: Spring 源码学习(三)-自定义标签
又来填坑啦,上一篇讲完默认标签的解析,这篇笔记记录一下自定义标签的解析吧。
我们知道,Spring 源码的核心模块是 Spring-core 和 Spring-beans,在此底子上衍生出其他模块,比方 context、 cache、 tx 等模块,都是根据这两个底子模块举行扩展的。
聪明如你,应该想到我们代码中常用的缓存注解 @Cacheable、事务注解 @Transaction,另有阿里巴巴的 RPC 中间件 Dubbo,在配置文件中通过  或者  举行服务注册和订阅,这些都都属于 Spring 的自定义标签的实现,通过自定义标签可以实现更加强大的功能!
作为一个有寻求的程序员,固然不能满足于框架自带默认的标签,为了扩展性和配置化要求,这时间就必要学习自定义标签和利用自定义标签~
<hr/>Table of Contents generated with DocToc
<hr/>又来填坑啦,上一篇讲完默认标签的解析,这篇笔记记录一下自定义标签的解析吧。
我们知道,Spring 源码的核心模块是 Spring-core 和 Spring-beans,在此底子上衍生出其他模块,比方 context、 cache、 tx 等模块,都是根据这两个底子模块举行扩展的。
聪明如你,应该想到我们代码中常用的缓存注解 @Cacheable、事务注解 @Transaction,另有阿里巴巴的 RPC 中间件 Dubbo,在配置文件中通过  或者  举行服务注册和订阅,这些都都属于 Spring 的自定义标签的实现,通过自定义标签可以实现更加强大的功能!
作为一个有寻求的程序员,固然不能满足于框架自带默认的标签,为了扩展性和配置化要求,这时间就必要学习自定义标签和利用自定义标签~
<hr/>官方例子

先来看一张源码图片(红框框圈着是重点哟)



                               
登录/注册后可看大图



刚才说了缓存和事务,那就拿这两个举例,另有一个标签 (这个我也不太清晰,网上查的资料也不多,所以按照我的理解大家跟说下)
起首我们看到,   和  都是自定义标签,左一是配置文件,举行 bean 的定义,顶部的 xmlns 是命名空间,表示标签所属的定义文件,像事务、缓存、MVC 的命名空间都是固定的。
而 myname 相当于万金油,既可以定义为事务,又可以定义为缓存,只要我们在命名空间中举行相应的定义就能精确的识别。这个就是我们待会要利用到的自定义标签,通过命名空间定位到我们想要的处置惩罚逻辑。
中间的是缓存定义的 xsd 文件,通过  定义元素, 区间内定义属性列表, 定义单个属性,详细分析可以看下解释~
右边的是事务定义的 xsd 文件,大体内容的跟中间一样,虽然元素名称  有相同的,但是下面的属性定义是有所区别的。
所以我们对自定义注解有个大概的了解,xsd 描述文件是个其中一个关键,在配置文件顶部的命名空间是标签举行解析时,举行定位的配置,固然另有处置惩罚器,下面利用时举行介绍。
不知道理解的对不对,如果有误的话请大佬们指出,我会举行修改的!
<hr/>自定义标签利用

Spring 提供了可扩展的 Schema 的支持,扩展 Spring 自定义标签配置必要以下几个步骤:
刚开始看到这些流程时,我还是有点慌的,毕竟从一个利用默认标签的萌新小白,突然要我本身定义,感觉到很新鲜,所以请各位跟着下面的流程一起来看吧~
<hr/>定义普通的 POJO 组件

这个没啥好说的,就是一个普通的类:
public class Product {        private Integer productId;        private String unit;        private String name;}<hr/>定义 XSD 描述文件

custom-product.xsd


                                                                                                                                                                                            我在上面的描述文件中,定义了一个新的 targetNamespace,同时定义了一个 叫 product 的新元素,并且将组件中的属性都列在  中。XSD 文件是 XML DTD 的替换者,具体就不多深入,感爱好的同学可以继续深入了解。
<hr/>定义组件解析器

base.label.custom.ProductBeanDefinitionParser


public class ProductBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {        @Override        protected Class getBeanClass(Element element) {                // 返回对应的类型                return Product.class;        }        // 从 element 中解析并提取对应的元素        @Override        protected void doParse(Element element, BeanDefinitionBuilder builder) {                String productId = element.getAttribute("productId");                String productName = element.getAttribute("name");                String productUnit = element.getAttribute("unit");                // 将提取到的数据放入 BeanDefinitionBuilder 中,比及完成全部 bean 的解析之后统一注册到 beanFactory 中                if (productId != null) {                        // element.getAttribute("") 方法取出来的都是 string 类型,利用时记得手动转换                        builder.addPropertyValue("productId", Integer.valueOf(productId));                }                if (StringUtils.hasText(productName)) {                        builder.addPropertyValue("name", productName);                }                if (StringUtils.hasText(productUnit)) {                        builder.addPropertyValue("unit", productUnit);                }        }}关键点在于,我们的解析器是继续于 AbstractSingleBeanDefinitionParser,重载了两个方法,详细用途请看解释~
<hr/>创建处置惩罚类的注册器

base.label.custom.ProductBeanHandler


public class ProductBeanHandler extends NamespaceHandlerSupport {        @Override        public void init() {                // 将组件解析器举行注册到 `Spring` 容器                registerBeanDefinitionParser("product", new ProductBeanDefinitionParser());        }}这个类也比较简单,关键是继续了 NamespaceHandlerSupport,对他举行了扩展,在该类初始化时将组件解析器举行注册到 Spring 容器中。
<hr/>编写 spring.hanlders 和 spring.schemas 文件

我将文件位置放在 resources -> META-INF 目次下:
spring.handlers
1http\://vip-augus.github.io/schema/product=base.label.custom.ProductBeanHandler
spring.schemas
1http\://vip-augus.github.io/schema/product.xsd=custom/custom-product.xsd
到了这一步,自定义的配置就结束了。下面是如何利用
<hr/>利用 Demo

配置文件



                测试代码

public class ProductBootstrap {        public static void main(String[] args) {                ApplicationContext context = new ClassPathXmlApplicationContext("custom/custom-label.xml");                Product product = (Product) context.getBean("product");                // 输出 Product{, productId =&#39;1&#39;, unit=&#39;台&#39;, name=&#39;Apple&#39;}                System.out.println(product.toString());        }}<hr/>小结

现在来回首一下,Spring 遇到自定义标签是,加载自定义的大抵流程:
上面已经将自定义注解的利用讲了,接下来讲的是源码中如何对自定义标签举行解析。
<hr/>自定义标签解析

在上一篇笔记中,讲了如何解析默认标签,Spring 判断一个标签不是默认标签的话,就会将这个标签解析交给自定义标签的解析方法
直接定位到解析自定义标签的方法吧:
org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinition)


public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {                // 解释 3.8 ① 找到命名空间                String namespaceUri = getNamespaceURI(ele);                // ② 根据命名空间找到对应的 NamespaceHandler                NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);                // ③ 调用自定义的 NamespaceHandler 举行解析                return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));        }看着流程是不是以为很熟悉,我们刚才在自定义标签利用时,定义的文件顺序是一样的,下面来讲下这三个方法,具体代码不会贴太多,紧张记录一些关键方法和流程,详细代码和流程请下载我上传的工程~
<hr/>① 获取标签的命名空间



public String getNamespaceURI(Node node) {                return node.getNamespaceURI();        }这个方法具体做的事情很简单,而且传参的类型 org.w3c.dom.Node,已经提供了现成的方法,所以我们只必要调用即可。
<hr/>② 根据命名空间找到对应的 NamespaceHandler

具体解析方法这这个类中:
org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver#resolve


public NamespaceHandler resolve(String namespaceUri) {        // 解释 3.9 获取全部已经配置的 handler 映射        Map handlerMappings = getHandlerMappings();        // 从 map 中取出命名空间对应的 NamespaceHandler 的 className        // 这个映射 map 值,没有的话,会举行实例化类,然后放入 map,等下次同样命名空间进来就能直接利用了        Object handlerOrClassName = handlerMappings.get(namespaceUri);        if (handlerOrClassName == null) {                return null;        }        else if (handlerOrClassName instanceof NamespaceHandler) {                return (NamespaceHandler) handlerOrClassName;        }        else {                String className = (String) handlerOrClassName;                                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);                // 调用 handler 的 init() 方法                namespaceHandler.init();                // 放入 handler 映射中                handlerMappings.put(namespaceUri, namespaceHandler);                return namespaceHandler;        }}找对应的 NamespaceHandler,关键方法在于 getHandlerMappings():
private Map getHandlerMappings() {        Map handlerMappings = this.handlerMappings;        // 如果没有缓存,举行缓存加载,公共变量,加锁举行操作,细节好评        if (handlerMappings == null) {                synchronized (this) {                        handlerMappings = this.handlerMappings;                        if (handlerMappings == null) {                                Properties mappings =                                                PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);                                handlerMappings = new ConcurrentHashMap(mappings.size());                                CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);                                this.handlerMappings = handlerMappings;                        }                }        }        return handlerMappings;}所以我们能看到,找 Handler 时,利用的计谋是延迟加载,在 map 缓存中找到了直接返回,没找到对应的 Handler,将处置惩罚器实例化,实行 init() 方法,接着将 Handler 放入 map 缓存中,等待下一个利用。
<hr/>③ 调用自定义的 NamespaceHandler 举行解析

回忆一下,我们在自定义标签解析的时间,是没有重载 parse() 方法,所以定位进去,看到现实调用方法是这两行:
org.springframework.beans.factory.xml.NamespaceHandlerSupport#parse


public BeanDefinition parse(Element element, ParserContext parserContext) {                // 寻找解析器并举行解析操作                BeanDefinitionParser parser = findParserForElement(element, parserContext);                // 真正解析调用调用的方法                return (parser != null ? parser.parse(element, parserContext) : null);        }第一步获取解析器,就是我们之前在 init() 方法中,注册到 Spring 容器的解析器。
第二步才是解析器举行解析的方法,我们的解析器扩展的是 AbstractSingleBeanDefinitionParser,所以现实是调用了我们解析器父类的父类 AbstractBeanDefinitionParser 的 parse 方法:
org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#parse


public final BeanDefinition parse(Element element, ParserContext parserContext) {                // 解释 3.10 现实自定义标签解析器调用的方法,在 parseInternal 方法中,调用了我们重载的方法                AbstractBeanDefinition definition = parseInternal(element, parserContext);    ...    return definition;}解析关键方法
org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#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 beanClass = getBeanClass(element);        if (beanClass != null) {                builder.getRawBeanDefinition().setBeanClass(beanClass);        }        else {                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);        }        // 解释 3.11 在这里调用了我们写的解析方法        doParse(element, parserContext, builder);        return builder.getBeanDefinition();}这里我要倒着讲,在第二步解析时,不是直接调用了自定义的 doParse 方法,而是举行了一系列的数据准备,包括了 beanClass、 class、 lazyInit 等属性的准备。
第一步解析,在我省略的代码中,是将第二步解析后的结果举行包装,从 AbstractBeanDefinition 转换成 BeanDefinitionHolder ,然后举行注册。转换和注册流程在第一篇笔记已经介绍过了,不再赘述。
到这里为止,我们自定义标签的解析就完成了~




欢迎光临 创意电子 (https://wxcydz.cc/) Powered by Discuz! X3.4